Skip to content

Commit 044ab7f

Browse files
committed
Polishing.
Replace BiFunction with TtlFunction for a more concise definition. Deprecate RedisCacheConfiguration.getTtl in favor of Ttlfunction. Enhance tests. See spring-projects#1433 Original pull request: spring-projects#2587
1 parent aa8c5a5 commit 044ab7f

File tree

4 files changed

+114
-57
lines changed

4 files changed

+114
-57
lines changed

src/main/java/org/springframework/data/redis/cache/RedisCache.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.core.convert.ConversionFailedException;
3232
import org.springframework.core.convert.ConversionService;
3333
import org.springframework.core.convert.TypeDescriptor;
34+
import org.springframework.data.redis.cache.RedisCacheConfiguration.TtlFunction;
3435
import org.springframework.data.redis.serializer.RedisSerializationContext;
3536
import org.springframework.data.redis.serializer.RedisSerializer;
3637
import org.springframework.data.redis.util.ByteUtils;
@@ -63,6 +64,8 @@ public class RedisCache extends AbstractValueAdaptingCache {
6364

6465
private final String name;
6566

67+
private final TtlFunction ttlFunction;
68+
6669
/**
6770
* Create a new {@link RedisCache}.
6871
*
@@ -85,6 +88,7 @@ protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfig
8588
this.name = name;
8689
this.cacheWriter = cacheWriter;
8790
this.cacheConfiguration = cacheConfiguration;
91+
this.ttlFunction = cacheConfiguration.getTtlFunction();
8892
}
8993

9094

@@ -185,7 +189,7 @@ public void put(Object key, @Nullable Object value) {
185189
}
186190

187191
getCacheWriter().put(getName(), createAndConvertCacheKey(key), serializeCacheValue(cacheValue),
188-
getCacheConfiguration().getTtl(key, value));
192+
ttlFunction.getTimeToLive(key, value));
189193
}
190194

191195
@Override
@@ -198,7 +202,7 @@ public ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
198202
}
199203

200204
byte[] result = getCacheWriter().putIfAbsent(getName(), createAndConvertCacheKey(key),
201-
serializeCacheValue(cacheValue), getCacheConfiguration().getTtl(key, value));
205+
serializeCacheValue(cacheValue), ttlFunction.getTimeToLive(key, value));
202206

203207
return result != null ? new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result))) : null;
204208
}

src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java

Lines changed: 69 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import java.nio.charset.StandardCharsets;
1919
import java.time.Duration;
20-
import java.util.function.BiFunction;
2120
import java.util.function.Consumer;
2221

2322
import org.springframework.cache.Cache;
@@ -35,8 +34,8 @@
3534
* Immutable {@link RedisCacheConfiguration} used to customize {@link RedisCache} behaviour, such as caching
3635
* {@literal null} values, computing cache key prefixes and handling binary serialization.
3736
* <p>
38-
* Start with {@link RedisCacheConfiguration#defaultCacheConfig()} and customize {@link RedisCache} behaviour
39-
* from that point on.
37+
* Start with {@link RedisCacheConfiguration#defaultCacheConfig()} and customize {@link RedisCache} behaviour from that
38+
* point on.
4039
*
4140
* @author Christoph Strobl
4241
* @author Mark Paluch
@@ -109,8 +108,7 @@ public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader c
109108
registerDefaultConverters(conversionService);
110109

111110
return new RedisCacheConfiguration((k, v) -> Duration.ZERO, DEFAULT_CACHE_NULL_VALUES, DEFAULT_USE_PREFIX,
112-
CacheKeyPrefix.simple(),
113-
SerializationPair.fromSerializer(RedisSerializer.string()),
111+
CacheKeyPrefix.simple(), SerializationPair.fromSerializer(RedisSerializer.string()),
114112
SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService);
115113
}
116114

@@ -121,17 +119,17 @@ public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader c
121119

122120
private final ConversionService conversionService;
123121

124-
private final BiFunction<Object, Object, Duration> ttlProvider;
122+
private final TtlFunction ttlFunction;
125123

126124
private final SerializationPair<String> keySerializationPair;
127125
private final SerializationPair<Object> valueSerializationPair;
128126

129127
@SuppressWarnings("unchecked")
130-
private RedisCacheConfiguration(BiFunction<Object, Object, Duration> ttlProvider, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix,
131-
SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair,
132-
ConversionService conversionService) {
128+
private RedisCacheConfiguration(TtlFunction ttlFunction, Boolean cacheNullValues, Boolean usePrefix,
129+
CacheKeyPrefix keyPrefix, SerializationPair<String> keySerializationPair,
130+
SerializationPair<?> valueSerializationPair, ConversionService conversionService) {
133131

134-
this.ttlProvider = ttlProvider;
132+
this.ttlFunction = ttlFunction;
135133
this.cacheNullValues = cacheNullValues;
136134
this.usePrefix = usePrefix;
137135
this.keyPrefix = keyPrefix;
@@ -167,7 +165,7 @@ public RedisCacheConfiguration computePrefixWith(CacheKeyPrefix cacheKeyPrefix)
167165

168166
Assert.notNull(cacheKeyPrefix, "Function for computing prefix must not be null");
169167

170-
return new RedisCacheConfiguration(ttlProvider, cacheNullValues, DEFAULT_USE_PREFIX, cacheKeyPrefix,
168+
return new RedisCacheConfiguration(ttlFunction, cacheNullValues, DEFAULT_USE_PREFIX, cacheKeyPrefix,
171169
keySerializationPair, valueSerializationPair, conversionService);
172170
}
173171

@@ -180,8 +178,8 @@ public RedisCacheConfiguration computePrefixWith(CacheKeyPrefix cacheKeyPrefix)
180178
* @return new {@link RedisCacheConfiguration}.
181179
*/
182180
public RedisCacheConfiguration disableCachingNullValues() {
183-
return new RedisCacheConfiguration(ttlProvider, DO_NOT_CACHE_NULL_VALUES, usePrefix, keyPrefix, keySerializationPair,
184-
valueSerializationPair, conversionService);
181+
return new RedisCacheConfiguration(ttlFunction, DO_NOT_CACHE_NULL_VALUES, usePrefix, keyPrefix,
182+
keySerializationPair, valueSerializationPair, conversionService);
185183
}
186184

187185
/**
@@ -193,7 +191,7 @@ public RedisCacheConfiguration disableCachingNullValues() {
193191
*/
194192
public RedisCacheConfiguration disableKeyPrefix() {
195193

196-
return new RedisCacheConfiguration(ttlProvider, cacheNullValues, DO_NOT_USE_PREFIX, keyPrefix, keySerializationPair,
194+
return new RedisCacheConfiguration(ttlFunction, cacheNullValues, DO_NOT_USE_PREFIX, keyPrefix, keySerializationPair,
197195
valueSerializationPair, conversionService);
198196
}
199197

@@ -207,22 +205,22 @@ public RedisCacheConfiguration entryTtl(Duration ttl) {
207205

208206
Assert.notNull(ttl, "TTL duration must not be null");
209207

210-
return new RedisCacheConfiguration((k, v) -> ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
211-
valueSerializationPair, conversionService);
208+
return entryTtl(new SingletonTtlFunction(ttl));
212209
}
213210

214211
/**
215-
* Set the ttl Provider, which can dynamic provide ttl to apply for cache entries.
216-
* @param ttlProvider {@link BiFunction} calculate ttl with the actual original cache key and value,
217-
* which must not be {@literal null}, and the ttl must not be {@literal null} either.
212+
* Set the {@link TtlFunction TTL function} to compute the time to live for cache entries.
218213
*
214+
* @param ttlFunction the {@link TtlFunction} to compute the time to live for cache entries, must not be
215+
* {@literal null}.
219216
* @return new {@link RedisCacheConfiguration}.
217+
* @since 3.2
220218
*/
221-
public RedisCacheConfiguration entryTtlProvider(BiFunction<Object, Object, Duration> ttlProvider) {
219+
public RedisCacheConfiguration entryTtl(TtlFunction ttlFunction) {
222220

223-
Assert.notNull(ttlProvider, "ttlProvider must not be null");
221+
Assert.notNull(ttlFunction, "TtlFunction must not be null");
224222

225-
return new RedisCacheConfiguration(ttlProvider, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
223+
return new RedisCacheConfiguration(ttlFunction, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
226224
valueSerializationPair, conversionService);
227225
}
228226

@@ -236,7 +234,7 @@ public RedisCacheConfiguration serializeKeysWith(SerializationPair<String> keySe
236234

237235
Assert.notNull(keySerializationPair, "KeySerializationPair must not be null");
238236

239-
return new RedisCacheConfiguration(ttlProvider, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
237+
return new RedisCacheConfiguration(ttlFunction, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
240238
valueSerializationPair, conversionService);
241239
}
242240

@@ -250,7 +248,7 @@ public RedisCacheConfiguration serializeValuesWith(SerializationPair<?> valueSer
250248

251249
Assert.notNull(valueSerializationPair, "ValueSerializationPair must not be null");
252250

253-
return new RedisCacheConfiguration(ttlProvider, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
251+
return new RedisCacheConfiguration(ttlFunction, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
254252
valueSerializationPair, conversionService);
255253
}
256254

@@ -264,8 +262,8 @@ public RedisCacheConfiguration withConversionService(ConversionService conversio
264262

265263
Assert.notNull(conversionService, "ConversionService must not be null");
266264

267-
return new RedisCacheConfiguration(ttlProvider, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
268-
valueSerializationPair, conversionService);
265+
return new RedisCacheConfiguration(ttlFunction, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
266+
valueSerializationPair, conversionService);
269267
}
270268

271269
/**
@@ -319,26 +317,26 @@ public SerializationPair<Object> getValueSerializationPair() {
319317

320318
/**
321319
* @return The constant expiration time (ttl) for cache entries. Never {@literal null}.
320+
* @deprecated since 3.2, use {@link #getTtlFunction()} instead.
322321
*/
322+
@Deprecated(since = "3.2", forRemoval = true)
323323
public Duration getTtl() {
324-
Duration ttl = ttlProvider.apply(Object.class, Object.class);
325-
Assert.notNull(ttl, "TTL duration must not be null");
326-
return ttl;
324+
return getTtlFunction().getTimeToLive(Object.class, null);
327325
}
328326

329327
/**
330-
* @return The expiration time (ttl) for cache entries with original key and value. Never {@literal null}.
328+
* Returns the function to compute the time to live.
329+
*
330+
* @return the function to compute the time to live.
331+
* @since 3.2
331332
*/
332-
public Duration getTtl(Object key, Object val) {
333-
Duration ttl = ttlProvider.apply(key, val);
334-
Assert.notNull(ttl, "TTL duration must not be null");
335-
336-
return ttl;
333+
public TtlFunction getTtlFunction() {
334+
return ttlFunction;
337335
}
338336

339337
/**
340-
* Adds a {@link Converter} to extract the {@link String} representation of a {@literal cache key}
341-
* if no suitable {@link Object#toString()} method is present.
338+
* Adds a {@link Converter} to extract the {@link String} representation of a {@literal cache key} if no suitable
339+
* {@link Object#toString()} method is present.
342340
*
343341
* @param cacheKeyConverter {@link Converter} used to convert a {@literal cache key} into a {@link String}.
344342
* @throws IllegalStateException if {@link #getConversionService()} does not allow {@link Converter} registration.
@@ -352,8 +350,8 @@ public void addCacheKeyConverter(Converter<?, String> cacheKeyConverter) {
352350
/**
353351
* Configure the underlying {@link ConversionService} used to extract the {@literal cache key}.
354352
*
355-
* @param registryConsumer {@link Consumer} used to register a {@link Converter}
356-
* with the configured {@link ConverterRegistry}; never {@literal null}.
353+
* @param registryConsumer {@link Consumer} used to register a {@link Converter} with the configured
354+
* {@link ConverterRegistry}; never {@literal null}.
357355
* @throws IllegalStateException if {@link #getConversionService()} does not allow {@link Converter} registration.
358356
* @see org.springframework.core.convert.converter.ConverterRegistry
359357
* @since 2.2
@@ -381,8 +379,8 @@ public void configureKeyConverters(Consumer<ConverterRegistry> registryConsumer)
381379
* <li>{@link SimpleKey} to {@link String}</li>
382380
* </ul>
383381
*
384-
* @param registry {@link ConverterRegistry} in which the {@link Converter key converters} are registered;
385-
* must not be {@literal null}.
382+
* @param registry {@link ConverterRegistry} in which the {@link Converter key converters} are registered; must not be
383+
* {@literal null}.
386384
* @see org.springframework.core.convert.converter.ConverterRegistry
387385
*/
388386
public static void registerDefaultConverters(ConverterRegistry registry) {
@@ -392,4 +390,34 @@ public static void registerDefaultConverters(ConverterRegistry registry) {
392390
registry.addConverter(String.class, byte[].class, source -> source.getBytes(StandardCharsets.UTF_8));
393391
registry.addConverter(SimpleKey.class, String.class, SimpleKey::toString);
394392
}
393+
394+
/**
395+
* Function to compute the time to live from the cache {@code key} and {@code value}.
396+
*
397+
* @author Mark Paluch
398+
* @since 3.2
399+
*/
400+
@FunctionalInterface
401+
interface TtlFunction {
402+
403+
/**
404+
* Compute a {@link Duration time to live duration} using the cache {@code key} and {@code value}. The time to live
405+
* is computed on each write operation. Redis uses milliseconds granularity for timeouts. Any more granular values
406+
* (e.g. micros or nanos) are not considered and are truncated due to rounding. Returning {@link Duration#ZERO} (or
407+
* a value less than {@code Duration.ofMillis(1)}) results in a persistent value that does not expire.
408+
*
409+
* @param key the cache key.
410+
* @param value the cache value. Can be {@code null} if the cache supports {@code null} value caching.
411+
* @return the time to live. Can be {@link Duration#ZERO} for persistent values (i.e. cache entry does not expire).
412+
*/
413+
Duration getTimeToLive(Object key, @Nullable Object value);
414+
}
415+
416+
private record SingletonTtlFunction(Duration duration) implements TtlFunction {
417+
418+
@Override
419+
public Duration getTimeToLive(Object key, @Nullable Object value) {
420+
return this.duration;
421+
}
422+
}
395423
}

src/test/java/org/springframework/data/redis/cache/RedisCacheConfigurationUnitTests.java

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,21 @@
1515
*/
1616
package org.springframework.data.redis.cache;
1717

18-
import org.junit.jupiter.api.Test;
19-
2018
import static org.assertj.core.api.Assertions.*;
2119

20+
import java.time.Duration;
21+
22+
import org.junit.jupiter.api.Test;
2223
import org.springframework.beans.DirectFieldAccessor;
2324
import org.springframework.core.convert.converter.Converter;
2425
import org.springframework.instrument.classloading.ShadowingClassLoader;
2526
import org.springframework.lang.Nullable;
2627

27-
import java.time.Duration;
28-
import java.util.function.BiFunction;
29-
3028
/**
3129
* Unit tests for {@link RedisCacheConfiguration}.
3230
*
3331
* @author Mark Paluch
32+
* @author Koy Zhuang
3433
*/
3534
class RedisCacheConfigurationUnitTests {
3635

@@ -59,19 +58,28 @@ void shouldAllowConverterRegistration() {
5958
assertThat(config.getConversionService().canConvert(DomainType.class, String.class)).isTrue();
6059
}
6160

61+
@Test // GH-1433
62+
void shouldApplySingletonTtlFunction() {
6263

63-
@Test
64-
void shouldGetDynamicTtlGivenTtlProvider() {
64+
RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
65+
.entryTtl(Duration.ofSeconds(10));
66+
67+
assertThat(defaultCacheConfiguration.getTtlFunction().getTimeToLive(Object.class, null))
68+
.isEqualTo(Duration.ofSeconds(10));
69+
assertThat(defaultCacheConfiguration.getTtlFunction().getTimeToLive(Object.class, null))
70+
.isEqualTo(Duration.ofSeconds(10));
71+
}
6572

66-
BiFunction<Object, Object, Duration> ttlProvider = (key, val) ->
67-
Duration.ofSeconds(Integer.parseInt(key + String.valueOf(val)));
73+
@Test // GH-1433
74+
void shouldApplyTtlFunction() {
6875

6976
RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
70-
.entryTtlProvider(ttlProvider);
77+
.entryTtl((key, value) -> Duration.ofSeconds((Integer) value + 10));
7178

72-
assertThat(defaultCacheConfiguration.getTtl(1, 12)).isEqualTo(Duration.ofSeconds(112));
73-
assertThat(defaultCacheConfiguration.getTtl(15, 22)).isEqualTo(Duration.ofSeconds(1522));
74-
assertThat(defaultCacheConfiguration.getTtl(77, 0)).isEqualTo(Duration.ofSeconds(770));
79+
assertThat(defaultCacheConfiguration.getTtlFunction().getTimeToLive(Object.class, 10))
80+
.isEqualTo(Duration.ofSeconds(20));
81+
assertThat(defaultCacheConfiguration.getTtlFunction().getTimeToLive(Object.class, 20))
82+
.isEqualTo(Duration.ofSeconds(30));
7583
}
7684

7785
private static class DomainType {

src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import java.util.stream.IntStream;
4141

4242
import org.junit.jupiter.api.BeforeEach;
43-
4443
import org.springframework.cache.Cache.ValueWrapper;
4544
import org.springframework.cache.interceptor.SimpleKey;
4645
import org.springframework.cache.interceptor.SimpleKeyGenerator;
@@ -446,6 +445,24 @@ void cacheShouldFailOnNonConvertibleCacheKey() {
446445
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> cache.put(key, sample));
447446
}
448447

448+
@ParameterizedRedisTest // GH-1433
449+
void shouldApplyTimeToLive() {
450+
451+
SerializationPair<?> serializer = SerializationPair.fromSerializer(this.serializer);
452+
RedisCache cache = new RedisCache("cache", RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory),
453+
RedisCacheConfiguration.defaultCacheConfig().entryTtl((k, v) -> Duration.ofSeconds(60))
454+
.serializeValuesWith(serializer));
455+
456+
Object key = SimpleKeyGenerator.generateKey(Collections.singletonList("my-ttl-cache"));
457+
cache.put(key, sample);
458+
459+
try (RedisConnection connection = connectionFactory.getConnection()) {
460+
461+
Long ttl = connection.keyCommands().ttl("cache::my-ttl-cache".getBytes(), TimeUnit.SECONDS);
462+
assertThat(ttl).isBetween(40L, 80L);
463+
}
464+
}
465+
449466
@ParameterizedRedisTest // GH-2079
450467
void multipleThreadsLoadValueOnce() throws InterruptedException {
451468

0 commit comments

Comments
 (0)