diff --git a/pom.xml b/pom.xml index 97ebc2565e..15290c39d5 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 3.2.0-SNAPSHOT + 3.2.0-GH-2650-SNAPSHOT Spring Data Redis Spring Data module for Redis diff --git a/src/main/java/org/springframework/data/redis/aot/RedisRuntimeHints.java b/src/main/java/org/springframework/data/redis/aot/RedisRuntimeHints.java index 127b0d76e7..639d1906ba 100644 --- a/src/main/java/org/springframework/data/redis/aot/RedisRuntimeHints.java +++ b/src/main/java/org/springframework/data/redis/aot/RedisRuntimeHints.java @@ -106,6 +106,7 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) TypeReference.of(ReactiveClusterScriptingCommands.class), TypeReference.of(ReactiveClusterGeoCommands.class), TypeReference.of(ReactiveClusterHyperLogLogCommands.class), TypeReference.of(ReactiveRedisOperations.class), + TypeReference.of(ReactiveRedisConnectionFactory.class), TypeReference.of(ReactiveRedisTemplate.class), TypeReference.of(RedisOperations.class), TypeReference.of(RedisTemplate.class), TypeReference.of(StringRedisTemplate.class), TypeReference.of(KeyspaceConfiguration.class), TypeReference.of(MappingConfiguration.class), diff --git a/src/main/java/org/springframework/data/redis/cache/BatchStrategies.java b/src/main/java/org/springframework/data/redis/cache/BatchStrategies.java index f77b454715..fa4c78e865 100644 --- a/src/main/java/org/springframework/data/redis/cache/BatchStrategies.java +++ b/src/main/java/org/springframework/data/redis/cache/BatchStrategies.java @@ -28,18 +28,15 @@ import org.springframework.util.Assert; /** - * A collection of predefined {@link BatchStrategy} implementations using {@code KEYS} or {@code SCAN} command. + * Collection of predefined {@link BatchStrategy} implementations using the Redis {@code KEYS} or {@code SCAN} command. * * @author Mark Paluch * @author Christoph Strobl + * @author John Blum * @since 2.6 */ public abstract class BatchStrategies { - private BatchStrategies() { - // can't touch this - oh-oh oh oh oh-oh-oh - } - /** * A {@link BatchStrategy} using a single {@code KEYS} and {@code DEL} command to remove all matching keys. * {@code KEYS} scans the entire keyspace of the Redis database and can block the Redis worker thread for a long time @@ -68,6 +65,10 @@ public static BatchStrategy scan(int batchSize) { return new Scan(batchSize); } + private BatchStrategies() { + // can't touch this - oh-oh oh oh oh-oh-oh + } + /** * {@link BatchStrategy} using {@code KEYS}. */ @@ -108,9 +109,11 @@ public long cleanCache(RedisConnection connection, String name, byte[] pattern) long count = 0; PartitionIterator partitions = new PartitionIterator<>(cursor, batchSize); + while (partitions.hasNext()) { List keys = partitions.next(); + count += keys.size(); if (keys.size() > 0) { @@ -141,7 +144,7 @@ static class PartitionIterator implements Iterator> { @Override public boolean hasNext() { - return iterator.hasNext(); + return this.iterator.hasNext(); } @Override @@ -151,9 +154,10 @@ public List next() { throw new NoSuchElementException(); } - List list = new ArrayList<>(size); - while (list.size() < size && iterator.hasNext()) { - list.add(iterator.next()); + List list = new ArrayList<>(this.size); + + while (list.size() < this.size && this.iterator.hasNext()) { + list.add(this.iterator.next()); } return list; diff --git a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java index 36121249d6..37195ef05d 100644 --- a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java @@ -15,19 +15,29 @@ */ package org.springframework.data.redis.cache; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import org.springframework.dao.PessimisticLockingFailureException; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.util.ByteUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; /** * {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to Redis in {@literal standalone} @@ -35,8 +45,8 @@ * {@link RedisConnection}. *

* {@link DefaultRedisCacheWriter} can be used in - * {@link RedisCacheWriter#lockingRedisCacheWriter(RedisConnectionFactory) locking} - * or {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While + * {@link RedisCacheWriter#lockingRedisCacheWriter(RedisConnectionFactory) locking} or + * {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While * {@literal non-locking} aims for maximum performance it may result in overlapping, non-atomic, command execution for * operations spanning multiple Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents * command overlap by setting an explicit lock key and checking against presence of this key which leads to additional @@ -50,6 +60,9 @@ */ class DefaultRedisCacheWriter implements RedisCacheWriter { + private static final boolean REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT = ClassUtils + .isPresent("org.springframework.data.redis.connection.ReactiveRedisConnectionFactory", null); + private final BatchStrategy batchStrategy; private final CacheStatisticsCollector statistics; @@ -60,6 +73,8 @@ class DefaultRedisCacheWriter implements RedisCacheWriter { private final TtlFunction lockTtl; + private final AsyncCacheWriter asyncCacheWriter; + /** * @param connectionFactory must not be {@literal null}. * @param batchStrategy must not be {@literal null}. @@ -100,6 +115,64 @@ class DefaultRedisCacheWriter implements RedisCacheWriter { this.lockTtl = lockTtl; this.statistics = cacheStatisticsCollector; this.batchStrategy = batchStrategy; + + if (REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT && this.connectionFactory instanceof ReactiveRedisConnectionFactory) { + asyncCacheWriter = new AsynchronousCacheWriterDelegate(); + } else { + asyncCacheWriter = UnsupportedAsyncCacheWriter.INSTANCE; + } + } + + @Override + public byte[] get(String name, byte[] key) { + return get(name, key, null); + } + + @Override + public byte[] get(String name, byte[] key, @Nullable Duration ttl) { + + Assert.notNull(name, "Name must not be null"); + Assert.notNull(key, "Key must not be null"); + + byte[] result = shouldExpireWithin(ttl) + ? execute(name, connection -> connection.stringCommands().getEx(key, Expiration.from(ttl))) + : execute(name, connection -> connection.stringCommands().get(key)); + + statistics.incGets(name); + + if (result != null) { + statistics.incHits(name); + } else { + statistics.incMisses(name); + } + + return result; + } + + @Override + public boolean supportsAsyncRetrieve() { + return asyncCacheWriter.isSupported(); + } + + @Override + public CompletableFuture retrieve(String name, byte[] key, @Nullable Duration ttl) { + + Assert.notNull(name, "Name must not be null"); + Assert.notNull(key, "Key must not be null"); + + return asyncCacheWriter.retrieve(name, key, ttl) // + .thenApply(cachedValue -> { + + statistics.incGets(name); + + if (cachedValue != null) { + statistics.incHits(name); + } else { + statistics.incMisses(name); + } + + return cachedValue; + }); } @Override @@ -112,9 +185,10 @@ public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { execute(name, connection -> { if (shouldExpireWithin(ttl)) { - connection.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert()); + connection.stringCommands().set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), + SetOption.upsert()); } else { - connection.set(key, value); + connection.stringCommands().set(key, value); } return "OK"; @@ -124,29 +198,14 @@ public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { } @Override - public byte[] get(String name, byte[] key) { - return get(name, key, null); - } - - @Override - public byte[] get(String name, byte[] key, @Nullable Duration ttl) { + public CompletableFuture store(String name, byte[] key, byte[] value, @Nullable Duration ttl) { Assert.notNull(name, "Name must not be null"); Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); - byte[] result = shouldExpireWithin(ttl) - ? execute(name, connection -> connection.getEx(key, Expiration.from(ttl))) - : execute(name, connection -> connection.get(key)); - - statistics.incGets(name); - - if (result != null) { - statistics.incHits(name); - } else { - statistics.incMisses(name); - } - - return result; + return asyncCacheWriter.store(name, key, value, ttl) // + .thenRun(() -> statistics.incPuts(name)); } @Override @@ -167,9 +226,10 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Durat boolean put; if (shouldExpireWithin(ttl)) { - put = connection.set(key, value, Expiration.from(ttl), SetOption.ifAbsent()); + put = ObjectUtils.nullSafeEquals( + connection.stringCommands().set(key, value, Expiration.from(ttl), SetOption.ifAbsent()), true); } else { - put = connection.setNX(key, value); + put = ObjectUtils.nullSafeEquals(connection.stringCommands().setNX(key, value), true); } if (put) { @@ -177,7 +237,7 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Durat return null; } - return connection.get(key); + return connection.stringCommands().get(key); } finally { if (isLockingCacheWriter()) { @@ -193,7 +253,7 @@ public void remove(String name, byte[] key) { Assert.notNull(name, "Name must not be null"); Assert.notNull(key, "Key must not be null"); - execute(name, connection -> connection.del(key)); + execute(name, connection -> connection.keyCommands().del(key)); statistics.incDeletes(name); } @@ -257,6 +317,15 @@ void lock(String name) { execute(name, connection -> doLock(name, name, null, connection)); } + @Nullable + private Boolean doLock(String name, Object contextualKey, @Nullable Object contextualValue, + RedisConnection connection) { + + Expiration expiration = Expiration.from(this.lockTtl.getTimeToLive(contextualKey, contextualValue)); + + return connection.stringCommands().set(createCacheLockKey(name), new byte[0], expiration, SetOption.SET_IF_ABSENT); + } + /** * Explicitly remove a write lock from a cache. * @@ -266,52 +335,35 @@ void unlock(String name) { executeLockFree(connection -> doUnlock(name, connection)); } - private Boolean doLock(String name, Object contextualKey, Object contextualValue, RedisConnection connection) { - - Expiration expiration = lockTtl == null ? Expiration.persistent() - : Expiration.from(lockTtl.getTimeToLive(contextualKey, contextualValue)); - - return connection.set(createCacheLockKey(name), new byte[0], expiration, SetOption.SET_IF_ABSENT); - } - + @Nullable private Long doUnlock(String name, RedisConnection connection) { - return connection.del(createCacheLockKey(name)); - } - - boolean doCheckLock(String name, RedisConnection connection) { - return connection.exists(createCacheLockKey(name)); - } - - /** - * @return {@literal true} if {@link RedisCacheWriter} uses locks. - */ - private boolean isLockingCacheWriter() { - return !sleepTime.isZero() && !sleepTime.isNegative(); + return connection.keyCommands().del(createCacheLockKey(name)); } private T execute(String name, Function callback) { - RedisConnection connection = connectionFactory.getConnection(); - - try { + try (RedisConnection connection = this.connectionFactory.getConnection()) { checkAndPotentiallyWaitUntilUnlocked(name, connection); return callback.apply(connection); - } finally { - connection.close(); } } - private void executeLockFree(Consumer callback) { + private T executeLockFree(Function callback) { - RedisConnection connection = connectionFactory.getConnection(); - - try { - callback.accept(connection); - } finally { - connection.close(); + try (RedisConnection connection = this.connectionFactory.getConnection()) { + return callback.apply(connection); } } + /** + * Determines whether this {@link RedisCacheWriter} uses locks during caching operations. + * + * @return {@literal true} if {@link RedisCacheWriter} uses locks. + */ + private boolean isLockingCacheWriter() { + return !this.sleepTime.isZero() && !this.sleepTime.isNegative(); + } + private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) { if (!isLockingCacheWriter()) { @@ -322,26 +374,199 @@ private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection c try { while (doCheckLock(name, connection)) { - Thread.sleep(sleepTime.toMillis()); + Thread.sleep(this.sleepTime.toMillis()); } } catch (InterruptedException cause) { - // Re-interrupt current thread, to allow other participants to react. + // Re-interrupt current Thread to allow other participants to react. Thread.currentThread().interrupt(); - String message = String.format("Interrupted while waiting to unlock cache %s", name); - - throw new PessimisticLockingFailureException(message, cause); + throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name), + cause); } finally { - statistics.incLockTime(name, System.nanoTime() - lockWaitTimeNs); + this.statistics.incLockTime(name, System.nanoTime() - lockWaitTimeNs); } } + boolean doCheckLock(String name, RedisConnection connection) { + return ObjectUtils.nullSafeEquals(connection.keyCommands().exists(createCacheLockKey(name)), true); + } + + byte[] createCacheLockKey(String name) { + return (name + "~lock").getBytes(StandardCharsets.UTF_8); + } + private static boolean shouldExpireWithin(@Nullable Duration ttl) { return ttl != null && !ttl.isZero() && !ttl.isNegative(); } - private static byte[] createCacheLockKey(String name) { - return (name + "~lock").getBytes(StandardCharsets.UTF_8); + /** + * Interface for asynchronous cache retrieval. + * + * @since 3.2 + */ + interface AsyncCacheWriter { + + /** + * @return {@code true} if async cache operations are supported; {@code false} otherwise. + */ + boolean isSupported(); + + /** + * Retrieve a cache entry asynchronously. + * + * @param name the cache name from which to retrieve the cache entry. + * @param key the cache entry key. + * @param ttl optional TTL to set for Time-to-Idle eviction. + * @return a future that completes either with a value if the value exists or completing with {@code null} if the + * cache does not contain an entry. + */ + CompletableFuture retrieve(String name, byte[] key, @Nullable Duration ttl); + + /** + * Store a cache entry asynchronously. + * + * @param name the cache name which to store the cache entry to. + * @param key the key for the cache entry. Must not be {@literal null}. + * @param value the value stored for the key. Must not be {@literal null}. + * @param ttl optional expiration time. Can be {@literal null}. + * @return a future that signals completion. + */ + CompletableFuture store(String name, byte[] key, byte[] value, @Nullable Duration ttl); + } + + /** + * Unsupported variant of a {@link AsyncCacheWriter}. + * + * @since 3.2 + */ + enum UnsupportedAsyncCacheWriter implements AsyncCacheWriter { + INSTANCE; + + @Override + public boolean isSupported() { + return false; + } + + @Override + public CompletableFuture retrieve(String name, byte[] key, @Nullable Duration ttl) { + throw new UnsupportedOperationException("async retrieve not supported"); + } + + @Override + public CompletableFuture store(String name, byte[] key, byte[] value, @Nullable Duration ttl) { + throw new UnsupportedOperationException("async store not supported"); + } + } + + /** + * Delegate implementing {@link AsyncCacheWriter} to provide asynchronous cache retrieval and storage operations using + * {@link ReactiveRedisConnectionFactory}. + * + * @since 3.2 + */ + class AsynchronousCacheWriterDelegate implements AsyncCacheWriter { + + @Override + public boolean isSupported() { + return true; + } + + @Override + public CompletableFuture retrieve(String name, byte[] key, @Nullable Duration ttl) { + + return doWithConnection(connection -> { + + ByteBuffer wrappedKey = ByteBuffer.wrap(key); + Mono cacheLockCheckFlux; + + if (isLockingCacheWriter()) + cacheLockCheckFlux = waitForLock(connection, name); + else { + cacheLockCheckFlux = Mono.empty(); + } + + Mono get = shouldExpireWithin(ttl) + ? connection.stringCommands().getEx(wrappedKey, Expiration.from(ttl)) + : connection.stringCommands().get(wrappedKey); + + return cacheLockCheckFlux.then(get).map(ByteUtils::getBytes).toFuture(); + }); + } + + @Override + public CompletableFuture store(String name, byte[] key, byte[] value, @Nullable Duration ttl) { + + return doWithConnection(connection -> { + + Mono mono; + + if (isLockingCacheWriter()) { + + mono = Mono.usingWhen(doLock(name, key, value, connection), unused -> doStore(key, value, ttl, connection), + unused -> doUnlock(name, connection)); + } else { + mono = doStore(key, value, ttl, connection); + } + + return mono.then().toFuture(); + }); + } + + private Mono doStore(byte[] cacheKey, byte[] value, @Nullable Duration ttl, + ReactiveRedisConnection connection) { + + ByteBuffer wrappedKey = ByteBuffer.wrap(cacheKey); + ByteBuffer wrappedValue = ByteBuffer.wrap(value); + + if (shouldExpireWithin(ttl)) { + return connection.stringCommands().set(wrappedKey, wrappedValue, + Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert()); + } else { + return connection.stringCommands().set(wrappedKey, wrappedValue); + } + } + + private Mono doLock(String name, Object contextualKey, @Nullable Object contextualValue, + ReactiveRedisConnection connection) { + + Expiration expiration = Expiration.from(lockTtl.getTimeToLive(contextualKey, contextualValue)); + + return connection.stringCommands() + .set(ByteBuffer.wrap(createCacheLockKey(name)), ByteBuffer.wrap(new byte[0]), expiration, + SetOption.SET_IF_ABSENT) // + .thenReturn(new Object()); // Ensure we emit an object, otherwise, the Mono.usingWhen operator doesn't run + // the inner resource function. + } + + private Mono doUnlock(String name, ReactiveRedisConnection connection) { + return connection.keyCommands().del(ByteBuffer.wrap(createCacheLockKey(name))).then(); + } + + private Mono waitForLock(ReactiveRedisConnection connection, String cacheName) { + + AtomicLong lockWaitTimeNs = new AtomicLong(); + byte[] cacheLockKey = createCacheLockKey(cacheName); + + Flux wait = Flux.interval(Duration.ZERO, sleepTime); + Mono exists = connection.keyCommands().exists(ByteBuffer.wrap(cacheLockKey)).filter(it -> !it); + + return wait.doOnSubscribe(subscription -> lockWaitTimeNs.set(System.nanoTime())) // + .flatMap(it -> exists) // + .doFinally(signalType -> statistics.incLockTime(cacheName, System.nanoTime() - lockWaitTimeNs.get())) // + .next() // + .then(); + } + + private CompletableFuture doWithConnection( + Function> callback) { + + ReactiveRedisConnectionFactory cf = (ReactiveRedisConnectionFactory) connectionFactory; + + return Mono.usingWhen(Mono.fromSupplier(cf::getReactiveConnection), // + it -> Mono.fromCompletionStage(callback.apply(it)), // + ReactiveRedisConnection::closeLater) // + .toFuture(); + } } } diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCache.java b/src/main/java/org/springframework/data/redis/cache/RedisCache.java index bc17d7b7be..22e1cb9912 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCache.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCache.java @@ -24,8 +24,10 @@ import java.util.Map.Entry; import java.util.StringJoiner; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; import org.springframework.cache.Cache; import org.springframework.cache.support.AbstractValueAdaptingCache; @@ -37,13 +39,14 @@ import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.util.ByteUtils; +import org.springframework.data.redis.util.RedisAssertions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; /** - * {@link org.springframework.cache.Cache} implementation using for Redis as the underlying store for cache data. + * {@link AbstractValueAdaptingCache Cache} implementation using Redis as the underlying store for cache data. *

* Use {@link RedisCacheManager} to create {@link RedisCache} instances. * @@ -52,13 +55,12 @@ * @author Piotr Mionskowski * @author Jos Roseboom * @author John Blum - * @see org.springframework.cache.support.AbstractValueAdaptingCache * @since 2.0 */ @SuppressWarnings("unused") public class RedisCache extends AbstractValueAdaptingCache { - private static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE); + static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE); private final Lock lock = new ReentrantLock(); @@ -69,23 +71,24 @@ public class RedisCache extends AbstractValueAdaptingCache { private final String name; /** - * Create a new {@link RedisCache} with the given {@link String name}. + * Create a new {@link RedisCache} with the given {@link String name} and {@link RedisCacheConfiguration}, using the + * {@link RedisCacheWriter} to execute Redis commands supporting the cache operations. * * @param name {@link String name} for this {@link Cache}; must not be {@literal null}. - * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by - * executing the necessary Redis commands; must not be {@literal null}. - * @param cacheConfiguration {@link RedisCacheConfiguration} applied to this {@link RedisCache} on creation; - * must not be {@literal null}. + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing the + * necessary Redis commands; must not be {@literal null}. + * @param cacheConfiguration {@link RedisCacheConfiguration} applied to this {@link RedisCache} on creation; must not + * be {@literal null}. * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} - * are {@literal null} or the given {@link String} name for this {@link RedisCache} is {@literal null}. + * are {@literal null} or the given {@link String} name for this {@link RedisCache} is {@literal null}. */ protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfiguration) { - super(cacheConfiguration.getAllowCacheNullValues()); + super(RedisAssertions.requireNonNull(cacheConfiguration, "CacheConfiguration must not be null") + .getAllowCacheNullValues()); Assert.notNull(name, "Name must not be null"); Assert.notNull(cacheWriter, "CacheWriter must not be null"); - Assert.notNull(cacheConfiguration, "CacheConfiguration must not be null"); this.name = name; this.cacheWriter = cacheWriter; @@ -163,7 +166,6 @@ private T getSynchronized(Object key, Callable valueLoader) { try { ValueWrapper result = get(key); - return result != null ? (T) result.get() : loadCacheValue(key, valueLoader); } finally { lock.unlock(); @@ -215,16 +217,7 @@ private Duration getTimeToLive(Object key, @Nullable Object value) { @Override public void put(Object key, @Nullable Object value) { - Object cacheValue = preProcessCacheValue(value); - - if (nullCacheValueIsNotAllowed(cacheValue)) { - - String message = String.format("Cache '%s' does not allow 'null' values; Avoid storing null" - + " via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null'" - + " via RedisCacheConfiguration", getName()); - - throw new IllegalArgumentException(message); - } + Object cacheValue = processAndCheckValue(value); getCacheWriter().put(getName(), createAndConvertCacheKey(key), serializeCacheValue(cacheValue), getTimeToLive(key, value)); @@ -276,6 +269,61 @@ public void evict(Object key) { getCacheWriter().remove(getName(), createAndConvertCacheKey(key)); } + @Override + public CompletableFuture retrieve(Object key) { + + if (!getCacheWriter().supportsAsyncRetrieve()) { + throw new UnsupportedOperationException( + "The Redis driver configured with RedisCache through RedisCacheWriter does not support CompletableFuture-based retrieval"); + } + + return retrieveValue(key).thenApply(this::nullSafeDeserializedStoreValue); + } + + @Override + @SuppressWarnings("unchecked") + public CompletableFuture retrieve(Object key, Supplier> valueLoader) { + + if (!getCacheWriter().supportsAsyncRetrieve()) { + throw new UnsupportedOperationException( + "The Redis driver configured with RedisCache through RedisCacheWriter does not support CompletableFuture-based retrieval"); + } + + return retrieveValue(key) // + .thenCompose(bytes -> { + + if (bytes != null) { + return CompletableFuture.completedFuture((T) nullSafeDeserializedStoreValue(bytes)); + } + + return valueLoader.get().thenCompose(value -> { + + Object cacheValue = processAndCheckValue(value); + + return getCacheWriter() + .store(getName(), createAndConvertCacheKey(key), serializeCacheValue(cacheValue), + getTimeToLive(key, cacheValue)) // + .thenApply(v -> value); + }); + }); + } + + private Object processAndCheckValue(@Nullable Object value) { + + Object cacheValue = preProcessCacheValue(value); + + if (nullCacheValueIsNotAllowed(cacheValue)) { + + String message = String.format("Cache '%s' does not allow 'null' values; Avoid storing null" + + " via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null'" + + " via RedisCacheConfiguration", getName()); + + throw new IllegalArgumentException(message); + } + + return cacheValue; + } + /** * Customization hook called before passing object to * {@link org.springframework.data.redis.serializer.RedisSerializer}. @@ -285,7 +333,6 @@ public void evict(Object key) { */ @Nullable protected Object preProcessCacheValue(@Nullable Object value) { - return value != null ? value : isAllowNullValues() ? NullValue.INSTANCE : null; } @@ -390,6 +437,15 @@ protected String convertKey(Object key) { throw new IllegalStateException(message); } + private CompletableFuture retrieveValue(Object key) { + return getCacheWriter().retrieve(getName(), createAndConvertCacheKey(key)); + } + + @Nullable + private Object nullSafeDeserializedStoreValue(@Nullable byte[] value) { + return value != null ? fromStoreValue(deserializeCacheValue(value)) : null; + } + private boolean hasToStringMethod(Object target) { return hasToStringMethod(target.getClass()); } diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java index a46eb63f79..202ddee446 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java @@ -376,16 +376,16 @@ public SerializationPair getValueSerializationPair() { } /** - * Returns a computed {@link Duration TTL expiration timeout} based on cache entry key/value - * if a {@link TtlFunction} was confiugred using {@link #entryTtl(TtlFunction)}. + * Returns a computed {@link Duration TTL expiration timeout} based on cache entry key/value if a {@link TtlFunction} + * was confiugred using {@link #entryTtl(TtlFunction)}. *

- * Otherwise, returns the user-provided, fixed {@link Duration} if {@link #entryTtl(Duration)} - * was called during cache configuration. + * Otherwise, returns the user-provided, fixed {@link Duration} if {@link #entryTtl(Duration)} was called during cache + * configuration. * * @return the configured {@link Duration TTL expiration}. - * @deprecated since 3.2.0. Use {@link #getTtlFunction()} instead. + * @deprecated since 3.2. Use {@link #getTtlFunction()} instead. */ - @Deprecated(since = "3.2.0") + @Deprecated(since = "3.2") public Duration getTtl() { return getTtlFunction().getTimeToLive(Object.class, null); } diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java index 9f0d42fa33..84237293e1 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java @@ -32,20 +32,23 @@ import org.springframework.util.Assert; /** - * {@link CacheManager} backed by a {@link RedisCache}. + * {@link CacheManager} implementation for Redis backed by {@link RedisCache}. *

- * This {@link CacheManager} creates {@link Cache caches} by default upon first write. Empty {@link Cache caches} - * are not visible in Redis due to how Redis represents empty data structures. + * This {@link CacheManager} creates {@link Cache caches} on first write, by default. Empty {@link Cache caches} are not + * visible in Redis due to how Redis represents empty data structures. *

- * {@link Cache Caches} requiring a different {@link RedisCacheConfiguration} than the default cache configuration - * can be specified via {@link RedisCacheManagerBuilder#withInitialCacheConfigurations(Map)} or individually - * using {@link RedisCacheManagerBuilder#withCacheConfiguration(String, RedisCacheConfiguration)}. + * {@link Cache Caches} requiring a different {@link RedisCacheConfiguration cache configuration} than the + * {@link RedisCacheConfiguration#defaultCacheConfig() default cache configuration} can be specified via + * {@link RedisCacheManagerBuilder#withInitialCacheConfigurations(Map)} or individually using + * {@link RedisCacheManagerBuilder#withCacheConfiguration(String, RedisCacheConfiguration)}. * * @author Christoph Strobl * @author Mark Paluch * @author Yanming Zhou * @author John Blum - * @see org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager + * @see RedisCache + * @see org.springframework.cache.CacheManager + * @see org.springframework.data.redis.connection.RedisConnectionFactory * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter * @since 2.0 @@ -54,83 +57,6 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager protected static final boolean DEFAULT_ALLOW_RUNTIME_CACHE_CREATION = true; - /** - * Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager}. - * - * @return new {@link RedisCacheManagerBuilder}. - * @since 2.3 - */ - public static RedisCacheManagerBuilder builder() { - return new RedisCacheManagerBuilder(); - } - - /** - * Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager} - * initialized with the given {@link RedisCacheWriter}. - * - * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations - * by executing appropriate Redis commands; must not be {@literal null}. - * @return new {@link RedisCacheManagerBuilder}. - * @throws IllegalArgumentException if the given {@link RedisCacheWriter} is {@literal null}. - * @see org.springframework.data.redis.cache.RedisCacheWriter - */ - public static RedisCacheManagerBuilder builder(RedisCacheWriter cacheWriter) { - - Assert.notNull(cacheWriter, "CacheWriter must not be null"); - - return RedisCacheManagerBuilder.fromCacheWriter(cacheWriter); - } - - /** - * Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager} - * initialized with the given {@link RedisConnectionFactory}. - * - * @param connectionFactory {@link RedisConnectionFactory} used by the {@link RedisCacheManager} - * to acquire connections to Redis when performing {@link RedisCache} operations; must not be {@literal null}. - * @return new {@link RedisCacheManagerBuilder}. - * @throws IllegalArgumentException if the given {@link RedisConnectionFactory} is {@literal null}. - * @see org.springframework.data.redis.connection.RedisConnectionFactory - */ - public static RedisCacheManagerBuilder builder(RedisConnectionFactory connectionFactory) { - - Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); - - return RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory); - } - - /** - * Factory method used to construct a new {@link RedisCacheManager} initialized with - * the given {@link RedisConnectionFactory} and using the defaults for caching. - * - *

- *
locking
- *
disabled
- *
batch strategy
- *
{@link BatchStrategies#keys()}
- *
cache configuration
- *
{@link RedisCacheConfiguration#defaultCacheConfig()}
- *
initial caches
- *
none
- *
transaction aware
- *
no
- *
in-flight cache creation
- *
enabled
- *
- * - * @param connectionFactory {@link RedisConnectionFactory} used by the {@link RedisCacheManager} - * to acquire connections to Redis when performing {@link RedisCache} operations; must not be {@literal null}. - * @return new {@link RedisCacheManager}. - * @throws IllegalArgumentException if the given {@link RedisConnectionFactory} is {@literal null}. - * @see org.springframework.data.redis.connection.RedisConnectionFactory - */ - public static RedisCacheManager create(RedisConnectionFactory connectionFactory) { - - Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); - - return new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory), - RedisCacheConfiguration.defaultCacheConfig()); - } - private final boolean allowRuntimeCacheCreation; private final RedisCacheConfiguration defaultCacheConfiguration; @@ -140,17 +66,17 @@ public static RedisCacheManager create(RedisConnectionFactory connectionFactory) private final Map initialCacheConfiguration; /** - * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} - * and a default {@link RedisCacheConfiguration}. + * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} and a default + * {@link RedisCacheConfiguration}. *

* Allows {@link RedisCache cache} creation at runtime. * - * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations - * by executing appropriate Redis commands; must not be {@literal null}. - * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} - * by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing appropriate + * Redis commands; must not be {@literal null}. + * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} by + * default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} - * are {@literal null}. + * are {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter */ @@ -159,17 +85,17 @@ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration d } /** - * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} - * and default {@link RedisCacheConfiguration}, and whether to allow cache creation at runtime. + * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} and default + * {@link RedisCacheConfiguration}, and whether to allow cache creation at runtime. * - * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations - * by executing appropriate Redis commands; must not be {@literal null}. - * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} - * by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing appropriate + * Redis commands; must not be {@literal null}. + * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} by + * default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. * @param allowRuntimeCacheCreation boolean specifying whether to allow creation of undeclared caches at runtime; - * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. + * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} - * are {@literal null}. + * are {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter * @since 2.0.4 @@ -186,20 +112,20 @@ private RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration } /** - * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} - * and a default {@link RedisCacheConfiguration}, along with an optional, initial set of {@link String cache names} - * used to create {@link RedisCache Redis caches} on startup. + * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} and a default + * {@link RedisCacheConfiguration}, along with an optional, initial set of {@link String cache names} used to create + * {@link RedisCache Redis caches} on startup. *

* Allows {@link RedisCache cache} creation at runtime. * - * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations - * by executing appropriate Redis commands; must not be {@literal null}. - * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} - * by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing appropriate + * Redis commands; must not be {@literal null}. + * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} by + * default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. * @param initialCacheNames optional set of {@link String cache names} used to create {@link RedisCache Redis caches} - * on startup. The default {@link RedisCacheConfiguration} will be applied to each cache. + * on startup. The default {@link RedisCacheConfiguration} will be applied to each cache. * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} - * are {@literal null}. + * are {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter */ @@ -210,22 +136,22 @@ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration d } /** - * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} - * and default {@link RedisCacheConfiguration}, and whether to allow cache creation at runtime. + * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} and default + * {@link RedisCacheConfiguration}, and whether to allow cache creation at runtime. *

- * Additionally, the optional, initial set of {@link String cache names} will be used to create - * {@link RedisCache Redis caches} on startup. + * Additionally, the optional, initial set of {@link String cache names} will be used to create {@link RedisCache + * Redis caches} on startup. * - * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations - * by executing appropriate Redis commands; must not be {@literal null}. - * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} - * by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing appropriate + * Redis commands; must not be {@literal null}. + * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} by + * default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. * @param allowRuntimeCacheCreation boolean specifying whether to allow creation of undeclared caches at runtime; - * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. + * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. * @param initialCacheNames optional set of {@link String cache names} used to create {@link RedisCache Redis caches} - * on startup. The default {@link RedisCacheConfiguration} will be applied to each cache. + * on startup. The default {@link RedisCacheConfiguration} will be applied to each cache. * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} - * are {@literal null}. + * are {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter * @since 2.0.4 @@ -249,15 +175,15 @@ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration d *

* Allows {@link RedisCache cache} creation at runtime. * - * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations - * by executing appropriate Redis commands; must not be {@literal null}. - * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} - * by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing appropriate + * Redis commands; must not be {@literal null}. + * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} by + * default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. * @param initialCacheConfigurations {@link Map} of declared, known {@link String cache names} along with associated - * {@link RedisCacheConfiguration} used to create and configure {@link RedisCache Reds caches} on startup; - * must not be {@literal null}. + * {@link RedisCacheConfiguration} used to create and configure {@link RedisCache Reds caches} on startup; + * must not be {@literal null}. * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} - * are {@literal null}. + * are {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter */ @@ -268,23 +194,23 @@ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration d } /** - * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} - * and a default {@link RedisCacheConfiguration}, and whether to allow {@link RedisCache} creation at runtime. + * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} and a default + * {@link RedisCacheConfiguration}, and whether to allow {@link RedisCache} creation at runtime. *

* Additionally, an initial {@link RedisCache} will be created and configured using the associated * {@link RedisCacheConfiguration} for each {@link String named} {@link RedisCache} in the given {@link Map}. * - * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations - * by executing appropriate Redis commands; must not be {@literal null}. - * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} - * by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing appropriate + * Redis commands; must not be {@literal null}. + * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} by + * default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. * @param allowRuntimeCacheCreation boolean specifying whether to allow creation of undeclared caches at runtime; - * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. - * @param initialCacheConfigurations {@link Map} of declared, known {@link String cache names} along with - * the associated {@link RedisCacheConfiguration} used to create and configure {@link RedisCache Redis caches} - * on startup; must not be {@literal null}. + * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. + * @param initialCacheConfigurations {@link Map} of declared, known {@link String cache names} along with the + * associated {@link RedisCacheConfiguration} used to create and configure {@link RedisCache Redis caches} on + * startup; must not be {@literal null}. * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} - * are {@literal null}. + * are {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter * @since 2.0.4 @@ -300,16 +226,93 @@ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration d } /** - * @deprecated use {@link org.springframework.data.redis.cache.RedisCacheManager#RedisCacheManager(RedisCacheWriter, RedisCacheConfiguration, boolean, Map)} - * instead. + * @deprecated since 3.2. Use + * {@link RedisCacheManager#RedisCacheManager(RedisCacheWriter, RedisCacheConfiguration, boolean, Map)} + * instead. */ - @Deprecated + @Deprecated(since = "3.2") public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map initialCacheConfigurations, boolean allowRuntimeCacheCreation) { - this(cacheWriter, defaultCacheConfiguration, allowRuntimeCacheCreation, initialCacheConfigurations); } + /** + * Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager}. + * + * @return new {@link RedisCacheManagerBuilder}. + * @since 2.3 + */ + public static RedisCacheManagerBuilder builder() { + return new RedisCacheManagerBuilder(); + } + + /** + * Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager} + * initialized with the given {@link RedisCacheWriter}. + * + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing appropriate + * Redis commands; must not be {@literal null}. + * @return new {@link RedisCacheManagerBuilder}. + * @throws IllegalArgumentException if the given {@link RedisCacheWriter} is {@literal null}. + * @see org.springframework.data.redis.cache.RedisCacheWriter + */ + public static RedisCacheManagerBuilder builder(RedisCacheWriter cacheWriter) { + + Assert.notNull(cacheWriter, "CacheWriter must not be null"); + + return RedisCacheManagerBuilder.fromCacheWriter(cacheWriter); + } + + /** + * Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager} + * initialized with the given {@link RedisConnectionFactory}. + * + * @param connectionFactory {@link RedisConnectionFactory} used by the {@link RedisCacheManager} to acquire + * connections to Redis when performing {@link RedisCache} operations; must not be {@literal null}. + * @return new {@link RedisCacheManagerBuilder}. + * @throws IllegalArgumentException if the given {@link RedisConnectionFactory} is {@literal null}. + * @see org.springframework.data.redis.connection.RedisConnectionFactory + */ + public static RedisCacheManagerBuilder builder(RedisConnectionFactory connectionFactory) { + + Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); + + return RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory); + } + + /** + * Factory method used to construct a new {@link RedisCacheManager} initialized with the given + * {@link RedisConnectionFactory} and using {@link RedisCacheConfiguration#defaultCacheConfig() defaults} for caching. + *

+ *
locking
+ *
disabled
+ *
batch strategy
+ *
{@link BatchStrategies#keys()}
+ *
cache configuration
+ *
{@link RedisCacheConfiguration#defaultCacheConfig()}
+ *
initial caches
+ *
none
+ *
transaction aware
+ *
no
+ *
in-flight cache creation
+ *
enabled
+ *
+ * + * @param connectionFactory {@link RedisConnectionFactory} used by the {@link RedisCacheManager} to acquire + * connections to Redis when performing {@link RedisCache} operations; must not be {@literal null}. + * @return new {@link RedisCacheManager}. + * @throws IllegalArgumentException if the given {@link RedisConnectionFactory} is {@literal null}. + * @see org.springframework.data.redis.connection.RedisConnectionFactory + */ + public static RedisCacheManager create(RedisConnectionFactory connectionFactory) { + + Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); + + return new RedisCacheManager( + org.springframework.data.redis.cache.RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory), + RedisCacheConfiguration.defaultCacheConfig()); + } + /** * Determines whether {@link RedisCache Redis caches} are allowed to be created at runtime. * @@ -320,11 +323,11 @@ public boolean isAllowRuntimeCacheCreation() { } /** - * Return an {@link Collections#unmodifiableMap(Map) unmodifiable Map} containing {@link String caches name} - * mapped to the {@link RedisCache} {@link RedisCacheConfiguration configuration}. + * Return an {@link Collections#unmodifiableMap(Map) unmodifiable Map} containing {@link String caches name} mapped to + * the {@link RedisCache} {@link RedisCacheConfiguration configuration}. * - * @return unmodifiable {@link Map} containing {@link String cache name} - * / {@link RedisCacheConfiguration configuration} pairs. + * @return unmodifiable {@link Map} containing {@link String cache name} / {@link RedisCacheConfiguration + * configuration} pairs. */ public Map getCacheConfigurations() { @@ -340,8 +343,8 @@ public Map getCacheConfigurations() { } /** - * Gets the default {@link RedisCacheConfiguration} applied to new {@link RedisCache} instances on creation - * when custom, non-specific {@link RedisCacheConfiguration} was not provided. + * Gets the default {@link RedisCacheConfiguration} applied to new {@link RedisCache} instances on creation when + * custom, non-specific {@link RedisCacheConfiguration} was not provided. * * @return the default {@link RedisCacheConfiguration}. */ @@ -350,8 +353,8 @@ protected RedisCacheConfiguration getDefaultCacheConfiguration() { } /** - * Gets a {@link Map} of {@link String cache names} to {@link RedisCacheConfiguration} objects as the initial set - * of {@link RedisCache Redis caches} to create on startup. + * Gets a {@link Map} of {@link String cache names} to {@link RedisCacheConfiguration} objects as the initial set of + * {@link RedisCache Redis caches} to create on startup. * * @return a {@link Map} of {@link String cache names} to {@link RedisCacheConfiguration} objects. */ @@ -359,6 +362,17 @@ protected Map getInitialCacheConfiguration() { return Collections.unmodifiableMap(this.initialCacheConfiguration); } + /** + * Returns a reference to the configured {@link RedisCacheWriter} used to perform {@link RedisCache} operations, such + * as reading from and writing to the cache. + * + * @return a reference to the configured {@link RedisCacheWriter}. + * @see org.springframework.data.redis.cache.RedisCacheWriter + */ + protected RedisCacheWriter getCacheWriter() { + return this.cacheWriter; + } + @Override protected RedisCache getMissingCache(String name) { return isAllowRuntimeCacheCreation() ? createRedisCache(name, getDefaultCacheConfiguration()) : null; @@ -368,20 +382,19 @@ protected RedisCache getMissingCache(String name) { * Creates a new {@link RedisCache} with given {@link String name} and {@link RedisCacheConfiguration}. * * @param name {@link String name} for the {@link RedisCache}; must not be {@literal null}. - * @param cacheConfiguration {@link RedisCacheConfiguration} used to configure the {@link RedisCache}; - * resolves to the {@link #getDefaultCacheConfiguration()} if {@literal null}. + * @param cacheConfiguration {@link RedisCacheConfiguration} used to configure the {@link RedisCache}; resolves to the + * {@link #getDefaultCacheConfiguration()} if {@literal null}. * @return a new {@link RedisCache} instance; never {@literal null}. */ protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfiguration) { - return new RedisCache(name, cacheWriter, resolveCacheConfiguration(cacheConfiguration)); + return new RedisCache(name, getCacheWriter(), resolveCacheConfiguration(cacheConfiguration)); } @Override protected Collection loadCaches() { return getInitialCacheConfiguration().entrySet().stream() - .map(entry -> createRedisCache(entry.getKey(), entry.getValue())) - .toList(); + .map(entry -> createRedisCache(entry.getKey(), entry.getValue())).toList(); } private RedisCacheConfiguration resolveCacheConfiguration(@Nullable RedisCacheConfiguration cacheConfiguration) { @@ -400,26 +413,25 @@ private RedisCacheConfiguration resolveCacheConfiguration(@Nullable RedisCacheCo public static class RedisCacheManagerBuilder { /** - * Factory method returning a new {@literal Builder} used to create and configure a {@link RedisCacheManager} - * using the given {@link RedisCacheWriter}. + * Factory method returning a new {@literal Builder} used to create and configure a {@link RedisCacheManager} using + * the given {@link RedisCacheWriter}. * * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing - * appropriate Redis commands; must not be {@literal null}. + * appropriate Redis commands; must not be {@literal null}. * @return new {@link RedisCacheManagerBuilder}. * @throws IllegalArgumentException if the given {@link RedisCacheWriter} is {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheWriter */ public static RedisCacheManagerBuilder fromCacheWriter(RedisCacheWriter cacheWriter) { - return new RedisCacheManagerBuilder(RedisAssertions.requireNonNull(cacheWriter, - "CacheWriter must not be null")); + return new RedisCacheManagerBuilder(RedisAssertions.requireNonNull(cacheWriter, "CacheWriter must not be null")); } /** - * Factory method returning a new {@literal Builder} used to create and configure a {@link RedisCacheManager} - * using the given {@link RedisConnectionFactory}. + * Factory method returning a new {@literal Builder} used to create and configure a {@link RedisCacheManager} using + * the given {@link RedisConnectionFactory}. * - * @param connectionFactory {@link RedisConnectionFactory} used by the {@link RedisCacheManager} - * to acquire connections to Redis when performing {@link RedisCache} operations; must not be {@literal null}. + * @param connectionFactory {@link RedisConnectionFactory} used by the {@link RedisCacheManager} to acquire + * connections to Redis when performing {@link RedisCache} operations; must not be {@literal null}. * @return new {@link RedisCacheManagerBuilder}. * @throws IllegalArgumentException if the given {@link RedisConnectionFactory} is {@literal null}. * @see org.springframework.data.redis.connection.RedisConnectionFactory @@ -452,8 +464,8 @@ private RedisCacheManagerBuilder(RedisCacheWriter cacheWriter) { /** * Configure whether to allow cache creation at runtime. * - * @param allowRuntimeCacheCreation boolean to allow creation of undeclared caches at runtime; - * {@literal true} by default. + * @param allowRuntimeCacheCreation boolean to allow creation of undeclared caches at runtime; {@literal true} by + * default. * @return this {@link RedisCacheManagerBuilder}. */ public RedisCacheManagerBuilder allowCreateOnMissingCache(boolean allowRuntimeCacheCreation) { @@ -561,8 +573,8 @@ public RedisCacheManagerBuilder transactionAware() { } /** - * Registers the given {@link String cache name} and {@link RedisCacheConfiguration} used to - * create and configure a {@link RedisCache} on startup. + * Registers the given {@link String cache name} and {@link RedisCacheConfiguration} used to create and configure a + * {@link RedisCache} on startup. * * @param cacheName {@link String name} of the cache to register for creation on startup. * @param cacheConfiguration {@link RedisCacheConfiguration} used to configure the new cache on startup. @@ -628,21 +640,21 @@ public Set getConfiguredCaches() { public RedisCacheManager build() { Assert.state(cacheWriter != null, "CacheWriter must not be null;" - + " You can provide one via 'RedisCacheManagerBuilder#cacheWriter(RedisCacheWriter)'"); + + " You can provide one via 'RedisCacheManagerBuilder#cacheWriter(RedisCacheWriter)'"); - RedisCacheWriter resolvedCacheWriter = !CacheStatisticsCollector.none().equals(statisticsCollector) - ? cacheWriter.withStatisticsCollector(statisticsCollector) - : cacheWriter; + RedisCacheWriter resolvedCacheWriter = !CacheStatisticsCollector.none().equals(this.statisticsCollector) + ? this.cacheWriter.withStatisticsCollector(this.statisticsCollector) + : this.cacheWriter; RedisCacheManager cacheManager = newRedisCacheManager(resolvedCacheWriter); - cacheManager.setTransactionAware(enableTransactions); + cacheManager.setTransactionAware(this.enableTransactions); return cacheManager; } private RedisCacheManager newRedisCacheManager(RedisCacheWriter cacheWriter) { - return new RedisCacheManager(cacheWriter, cacheDefaults(), allowRuntimeCacheCreation, initialCaches); + return new RedisCacheManager(cacheWriter, cacheDefaults(), this.allowRuntimeCacheCreation, this.initialCaches); } } } diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java index 3ad8426780..f6da33eaf7 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java @@ -16,6 +16,8 @@ package org.springframework.data.redis.cache; import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.lang.Nullable; @@ -86,8 +88,7 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory, BatchStrategy batchStrategy) { - return lockingRedisCacheWriter(connectionFactory, Duration.ofMillis(50), TtlFunction.persistent(), - batchStrategy); + return lockingRedisCacheWriter(connectionFactory, Duration.ofMillis(50), TtlFunction.persistent(), batchStrategy); } /** @@ -106,8 +107,8 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); - return new DefaultRedisCacheWriter(connectionFactory, sleepTime, lockTtlFunction, - CacheStatisticsCollector.none(), batchStrategy); + return new DefaultRedisCacheWriter(connectionFactory, sleepTime, lockTtlFunction, CacheStatisticsCollector.none(), + batchStrategy); } /** @@ -122,8 +123,8 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio byte[] get(String name, byte[] key); /** - * Get the binary value representation from Redis stored for the given key and set the given - * {@link Duration TTL expiration} for the cache entry. + * Get the binary value representation from Redis stored for the given key and set the given {@link Duration TTL + * expiration} for the cache entry. * * @param name must not be {@literal null}. * @param key must not be {@literal null}. @@ -135,6 +136,52 @@ default byte[] get(String name, byte[] key, @Nullable Duration ttl) { return get(name, key); } + /** + * Determines whether the asynchronous {@link #retrieve(String, byte[])} and + * {@link #retrieve(String, byte[], Duration)} cache operations are supported by the implementation. + *

+ * The main factor for whether the {@literal retrieve} operation can be supported will primarily be determined by the + * Redis driver in use at runtime. + *

+ * Returns {@literal false} by default. This will have an effect of {@link RedisCache#retrieve(Object)} and + * {@link RedisCache#retrieve(Object, Supplier)} throwing an {@link UnsupportedOperationException}. + * + * @return {@literal true} if asynchronous {@literal retrieve} operations are supported by the implementation. + * @since 3.2 + */ + default boolean supportsAsyncRetrieve() { + return false; + } + + /** + * Returns the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@link byte[] key}. + *

+ * This operation is non-blocking. + * + * @param name {@link String} with the name of the {@link RedisCache}. + * @param key {@link byte[] key} mapped to the {@link CompletableFuture value} in the {@link RedisCache}. + * @return the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@link byte[] key}. + * @see #retrieve(String, byte[], Duration) + * @since 3.2 + */ + default CompletableFuture retrieve(String name, byte[] key) { + return retrieve(name, key, null); + } + + /** + * Returns the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@link byte[] key} + * setting the {@link Duration TTL expiration} for the cache entry. + *

+ * This operation is non-blocking. + * + * @param name {@link String} with the name of the {@link RedisCache}. + * @param key {@link byte[] key} mapped to the {@link CompletableFuture value} in the {@link RedisCache}. + * @param ttl {@link Duration} specifying the {@literal expiration timeout} for the cache entry. + * @return the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@link byte[] key}. + * @since 3.2 + */ + CompletableFuture retrieve(String name, byte[] key, @Nullable Duration ttl); + /** * Write the given key/value pair to Redis and set the expiration time if defined. * @@ -145,6 +192,19 @@ default byte[] get(String name, byte[] key, @Nullable Duration ttl) { */ void put(String name, byte[] key, byte[] value, @Nullable Duration ttl); + /** + * Store the given key/value pair asynchronously to Redis and set the expiration time if defined. + *

+ * This operation is non-blocking. + * + * @param name The cache name must not be {@literal null}. + * @param key The key for the cache entry. Must not be {@literal null}. + * @param value The value stored for the key. Must not be {@literal null}. + * @param ttl Optional expiration time. Can be {@literal null}. + * @since 3.2 + */ + CompletableFuture store(String name, byte[] key, byte[] value, @Nullable Duration ttl); + /** * Write the given value to Redis if the key does not already exist. * @@ -225,15 +285,15 @@ static TtlFunction persistent() { /** * Compute a {@link Duration time-to-live (TTL)} using the cache {@code key} and {@code value}. *

- * The {@link Duration time-to-live (TTL)} is computed on each write operation. Redis uses millisecond - * granularity for timeouts. Any more granular values (e.g. micros or nanos) are not considered - * and will be truncated due to rounding. Returning {@link Duration#ZERO}, or a value less than - * {@code Duration.ofMillis(1)}, results in a persistent value that does not expire. + * The {@link Duration time-to-live (TTL)} is computed on each write operation. Redis uses millisecond granularity + * for timeouts. Any more granular values (e.g. micros or nanos) are not considered and will be truncated due to + * rounding. Returning {@link Duration#ZERO}, or a value less than {@code Duration.ofMillis(1)}, results in a + * persistent value that does not expire. * * @param key the cache key. * @param value the cache value. Can be {@code null} if the cache supports {@code null} value caching. * @return the computed {@link Duration time-to-live (TTL)}. Can be {@link Duration#ZERO} for persistent values - * (i.e. cache entry does not expire). + * (i.e. cache entry does not expire). */ Duration getTimeToLive(Object key, @Nullable Object value); diff --git a/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementReader.java b/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementReader.java index bff07ac46a..972360985d 100644 --- a/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementReader.java +++ b/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementReader.java @@ -43,7 +43,7 @@ public T read(ByteBuffer buffer) { return (T) buffer; } - return serializer.deserialize(ByteUtils.extractBytes(buffer)); + return serializer.deserialize(ByteUtils.getBytes(buffer)); } } diff --git a/src/main/java/org/springframework/data/redis/util/ByteUtils.java b/src/main/java/org/springframework/data/redis/util/ByteUtils.java index d3775d1220..f6ad110bee 100644 --- a/src/main/java/org/springframework/data/redis/util/ByteUtils.java +++ b/src/main/java/org/springframework/data/redis/util/ByteUtils.java @@ -31,6 +31,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author Guy Korland + * @author John Blum * @since 1.7 */ public final class ByteUtils { @@ -251,13 +252,10 @@ public static ByteBuffer getByteBuffer(String theString, Charset charset) { * @param buffer must not be {@literal null}. * @return the extracted bytes. * @since 2.1 + * @deprecated Since 3.2. Use {@link #getBytes(ByteBuffer)} instead. */ + @Deprecated(since = "3.2") public static byte[] extractBytes(ByteBuffer buffer) { - - ByteBuffer duplicate = buffer.duplicate(); - byte[] bytes = new byte[duplicate.remaining()]; - duplicate.get(bytes); - - return bytes; + return getBytes(buffer); } } diff --git a/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java index 7ec58b7c03..f8c98b6f45 100644 --- a/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java +++ b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java @@ -23,6 +23,7 @@ import java.time.Duration; import java.util.Collection; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -31,10 +32,10 @@ import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; -import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; -import org.springframework.data.redis.connection.jedis.extension.JedisConnectionFactoryExtension; import org.springframework.data.redis.core.types.Expiration; -import org.springframework.data.redis.test.extension.RedisStanalone; +import org.springframework.data.redis.test.condition.EnabledOnRedisDriver; +import org.springframework.data.redis.test.condition.EnabledOnRedisDriver.DriverQualifier; +import org.springframework.data.redis.test.condition.RedisDriver; import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; @@ -55,7 +56,7 @@ public class DefaultRedisCacheWriterTests { private byte[] binaryCacheKey = cacheKey.getBytes(StandardCharsets.UTF_8); private byte[] binaryCacheValue = "value".getBytes(StandardCharsets.UTF_8); - private RedisConnectionFactory connectionFactory; + private final @DriverQualifier RedisConnectionFactory connectionFactory; public DefaultRedisCacheWriterTests(RedisConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; @@ -67,12 +68,6 @@ public static Collection testParams() { @BeforeEach void setUp() { - - JedisConnectionFactory connectionFactory = - JedisConnectionFactoryExtension.getConnectionFactory(RedisStanalone.class); - - this.connectionFactory = connectionFactory; - doWithConnection(RedisConnection::flushAll); } @@ -152,6 +147,46 @@ void getShouldReturnNullWhenKeyDoesNotExist() { assertThat(nonLockingRedisCacheWriter(connectionFactory).get(CACHE_NAME, binaryCacheKey)).isNull(); } + @ParameterizedRedisTest // GH-2650 + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + void cacheHitRetrieveShouldIncrementStatistics() throws ExecutionException, InterruptedException { + + doWithConnection(connection -> connection.set(binaryCacheKey, binaryCacheValue)); + + RedisCacheWriter writer = nonLockingRedisCacheWriter(connectionFactory) + .withStatisticsCollector(CacheStatisticsCollector.create()); + + writer.retrieve(CACHE_NAME, binaryCacheKey).get(); + + assertThat(writer.getCacheStatistics(CACHE_NAME).getGets()).isOne(); + assertThat(writer.getCacheStatistics(CACHE_NAME).getHits()).isOne(); + } + + @ParameterizedRedisTest // GH-2650 + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + void storeShouldIncrementStatistics() throws ExecutionException, InterruptedException { + + RedisCacheWriter writer = nonLockingRedisCacheWriter(connectionFactory) + .withStatisticsCollector(CacheStatisticsCollector.create()); + + writer.store(CACHE_NAME, binaryCacheKey, binaryCacheValue, null).get(); + + assertThat(writer.getCacheStatistics(CACHE_NAME).getPuts()).isOne(); + } + + @ParameterizedRedisTest // GH-2650 + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + void cacheMissRetrieveWithLoaderAsyncShouldIncrementStatistics() throws ExecutionException, InterruptedException { + + RedisCacheWriter writer = nonLockingRedisCacheWriter(connectionFactory) + .withStatisticsCollector(CacheStatisticsCollector.create()); + + writer.retrieve(CACHE_NAME, binaryCacheKey).get(); + + assertThat(writer.getCacheStatistics(CACHE_NAME).getGets()).isOne(); + assertThat(writer.getCacheStatistics(CACHE_NAME).getMisses()).isOne(); + } + @ParameterizedRedisTest // DATAREDIS-481, DATAREDIS-1082 void putIfAbsentShouldAddEternalEntryWhenKeyDoesNotExist() { @@ -253,8 +288,8 @@ void lockingCacheWriterShouldIgnoreExistingLockOnDifferenceCache() { ((DefaultRedisCacheWriter) lockingRedisCacheWriter(connectionFactory)).lock(CACHE_NAME); - lockingRedisCacheWriter(connectionFactory).put(CACHE_NAME + "-no-the-other-cache", binaryCacheKey, - binaryCacheValue, Duration.ZERO); + lockingRedisCacheWriter(connectionFactory).put(CACHE_NAME + "-no-the-other-cache", binaryCacheKey, binaryCacheValue, + Duration.ZERO); doWithConnection(connection -> { assertThat(connection.exists(binaryCacheKey)).isTrue(); @@ -341,8 +376,7 @@ boolean doCheckLock(String name, RedisConnection connection) { afterWrite.await(); - assertThat(exceptionRef.get()).hasMessageContaining("Interrupted while waiting to unlock") - .hasCauseInstanceOf(InterruptedException.class); + assertThat(exceptionRef.get()).hasRootCauseInstanceOf(InterruptedException.class); } @ParameterizedRedisTest // GH-2300 diff --git a/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterUnitTests.java b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterUnitTests.java deleted file mode 100644 index 47f79b7710..0000000000 --- a/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterUnitTests.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.redis.cache; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -import java.time.Duration; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import org.springframework.data.redis.connection.RedisConnection; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.types.Expiration; - -/** - * Unit tests for {@link DefaultRedisCacheWriter} - * - * @author John Blum - */ -@ExtendWith(MockitoExtension.class) -class DefaultRedisCacheWriterUnitTests { - - @Mock - private RedisConnection mockConnection; - - @Mock - private RedisConnectionFactory mockConnectionFactory; - - @BeforeEach - void setup() { - doReturn(this.mockConnection).when(this.mockConnectionFactory).getConnection(); - } - - private RedisCacheWriter newRedisCacheWriter() { - return new DefaultRedisCacheWriter(this.mockConnectionFactory, mock(BatchStrategy.class)) - .withStatisticsCollector(mock(CacheStatisticsCollector.class)); - } - - @Test // GH-2351 - void getWithNonNullTtl() { - - byte[] key = "TestKey".getBytes(); - byte[] value = "TestValue".getBytes(); - - Duration ttl = Duration.ofSeconds(15); - Expiration expiration = Expiration.from(ttl); - - doReturn(value).when(this.mockConnection).getEx(any(), any()); - - RedisCacheWriter cacheWriter = newRedisCacheWriter(); - - assertThat(cacheWriter.get("TestCache", key, ttl)).isEqualTo(value); - - verify(this.mockConnection, times(1)).getEx(eq(key), eq(expiration)); - verify(this.mockConnection).close(); - verifyNoMoreInteractions(this.mockConnection); - } - - @Test // GH-2351 - void getWithNullTtl() { - - byte[] key = "TestKey".getBytes(); - byte[] value = "TestValue".getBytes(); - - doReturn(value).when(this.mockConnection).get(any()); - - RedisCacheWriter cacheWriter = newRedisCacheWriter(); - - assertThat(cacheWriter.get("TestCache", key, null)).isEqualTo(value); - - verify(this.mockConnection, times(1)).get(eq(key)); - verify(this.mockConnection).close(); - verifyNoMoreInteractions(this.mockConnection); - } -} diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java index 5d4268966b..1cf09b3928 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java @@ -16,7 +16,6 @@ package org.springframework.data.redis.cache; import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.Assumptions.*; import static org.awaitility.Awaitility.*; import io.netty.util.concurrent.DefaultThreadFactory; @@ -24,18 +23,22 @@ import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.time.Instant; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Objects; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; @@ -45,10 +48,12 @@ import org.springframework.cache.support.NullValue; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.test.condition.EnabledOnCommand; +import org.springframework.data.redis.test.condition.EnabledOnRedisDriver; +import org.springframework.data.redis.test.condition.EnabledOnRedisDriver.DriverQualifier; +import org.springframework.data.redis.test.condition.RedisDriver; import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; import org.springframework.lang.Nullable; @@ -75,7 +80,7 @@ public class RedisCacheTests { private byte[] binaryNullValue = RedisSerializer.java().serialize(NullValue.INSTANCE); - private RedisConnectionFactory connectionFactory; + private final @DriverQualifier RedisConnectionFactory connectionFactory; private RedisSerializer serializer; private RedisCache cache; @@ -103,9 +108,7 @@ void putShouldAddEntry() { cache.put("key-1", sample); - doWithConnection(connection -> { - assertThat(connection.exists(binaryCacheKey)).isTrue(); - }); + doWithConnection(connection -> assertThat(connection.exists(binaryCacheKey)).isTrue()); } @ParameterizedRedisTest // GH-2379 @@ -116,9 +119,7 @@ void cacheShouldBeClearedByPattern() { String keyPattern = "*" + key.substring(1); cache.clear(keyPattern); - doWithConnection(connection -> { - assertThat(connection.exists(binaryCacheKey)).isFalse(); - }); + doWithConnection(connection -> assertThat(connection.exists(binaryCacheKey)).isFalse()); } @ParameterizedRedisTest // GH-2379 @@ -129,9 +130,7 @@ void cacheShouldNotBeClearedIfNoPatternMatch() { String keyPattern = "*" + key.substring(1) + "tail"; cache.clear(keyPattern); - doWithConnection(connection -> { - assertThat(connection.exists(binaryCacheKey)).isTrue(); - }); + doWithConnection(connection -> assertThat(connection.exists(binaryCacheKey)).isTrue()); } @ParameterizedRedisTest // DATAREDIS-481 @@ -177,9 +176,7 @@ void putIfAbsentShouldReturnExistingIfExists() { assertThat(result).isNotNull(); assertThat(result.get()).isEqualTo(sample); - doWithConnection(connection -> { - assertThat(connection.get(binaryCacheKey)).isEqualTo(binarySample); - }); + doWithConnection(connection -> assertThat(connection.get(binaryCacheKey)).isEqualTo(binarySample)); } @ParameterizedRedisTest // DATAREDIS-481 @@ -192,17 +189,13 @@ void putIfAbsentShouldReturnExistingNullValueIfExists() { assertThat(result).isNotNull(); assertThat(result.get()).isNull(); - doWithConnection(connection -> { - assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryNullValue); - }); + doWithConnection(connection -> assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryNullValue)); } @ParameterizedRedisTest // DATAREDIS-481 void getShouldRetrieveEntry() { - doWithConnection(connection -> { - connection.set(binaryCacheKey, binarySample); - }); + doWithConnection(connection -> connection.set(binaryCacheKey, binarySample)); ValueWrapper result = cache.get(key); assertThat(result).isNotNull(); @@ -237,6 +230,7 @@ void shouldAllowComplexKeyWithToStringMethod() { cache.put(key, sample); ValueWrapper result = cache.get(key); + assertThat(result).isNotNull(); assertThat(result.get()).isEqualTo(sample); } @@ -249,11 +243,10 @@ void getShouldReturnNullWhenKeyDoesNotExist() { @ParameterizedRedisTest // DATAREDIS-481 void getShouldReturnValueWrapperHoldingNullIfNullValueStored() { - doWithConnection(connection -> { - connection.set(binaryCacheKey, binaryNullValue); - }); + doWithConnection(connection -> connection.set(binaryCacheKey, binaryNullValue)); ValueWrapper result = cache.get(key); + assertThat(result).isNotNull(); assertThat(result.get()).isEqualTo(null); } @@ -291,13 +284,9 @@ void clearShouldClearCache() { } @ParameterizedRedisTest // GH-1721 + @EnabledOnRedisDriver(RedisDriver.LETTUCE) // SCAN not supported via Jedis Cluster. void clearWithScanShouldClearCache() { - // SCAN not supported via Jedis Cluster. - if (connectionFactory instanceof JedisConnectionFactory) { - assumeThat(((JedisConnectionFactory) connectionFactory).isRedisClusterAware()).isFalse(); - } - RedisCache cache = new RedisCache("cache", RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(25)), RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(SerializationPair.fromSerializer(serializer))); @@ -353,11 +342,9 @@ void computePrefixCreatesCacheKeyCorrectly() { cacheWithCustomPrefix.put("key-1", sample); - doWithConnection(connection -> { - - assertThat(connection.stringCommands().get("_cache_key-1".getBytes(StandardCharsets.UTF_8))) - .isEqualTo(binarySample); - }); + doWithConnection( + connection -> assertThat(connection.stringCommands().get("_cache_key-1".getBytes(StandardCharsets.UTF_8))) + .isEqualTo(binarySample)); } @ParameterizedRedisTest // DATAREDIS-1041 @@ -369,11 +356,9 @@ void prefixCacheNameCreatesCacheKeyCorrectly() { cacheWithCustomPrefix.put("key-1", sample); - doWithConnection(connection -> { - - assertThat(connection.stringCommands().get("redis::cache::key-1".getBytes(StandardCharsets.UTF_8))) - .isEqualTo(binarySample); - }); + doWithConnection(connection -> assertThat( + connection.stringCommands().get("redis::cache::key-1".getBytes(StandardCharsets.UTF_8))) + .isEqualTo(binarySample)); } @ParameterizedRedisTest // DATAREDIS-715 @@ -400,6 +385,7 @@ void cacheShouldAllowListKeyCacheKeysOfSimpleTypes() { ValueWrapper target = cache .get(SimpleKeyGenerator.generateKey(Collections.singletonList("my-cache-key-in-a-list"))); + assertThat(target.get()).isEqualTo(sample); } @@ -410,6 +396,7 @@ void cacheShouldAllowArrayKeyCacheKeysOfSimpleTypes() { cache.put(key, sample); ValueWrapper target = cache.get(SimpleKeyGenerator.generateKey("my-cache-key-in-an-array")); + assertThat(target.get()).isEqualTo(sample); } @@ -422,6 +409,7 @@ void cacheShouldAllowListCacheKeysOfComplexTypes() { ValueWrapper target = cache.get(SimpleKeyGenerator .generateKey(Collections.singletonList(new ComplexKey(sample.getFirstname(), sample.getBirthdate())))); + assertThat(target.get()).isEqualTo(sample); } @@ -432,8 +420,9 @@ void cacheShouldAllowMapCacheKeys() { .generateKey(Collections.singletonMap("map-key", new ComplexKey(sample.getFirstname(), sample.getBirthdate()))); cache.put(key, sample); - ValueWrapper target = cache.get(SimpleKeyGenerator - .generateKey(Collections.singletonMap("map-key", new ComplexKey(sample.getFirstname(), sample.getBirthdate())))); + ValueWrapper target = cache.get(SimpleKeyGenerator.generateKey( + Collections.singletonMap("map-key", new ComplexKey(sample.getFirstname(), sample.getBirthdate())))); + assertThat(target.get()).isEqualTo(sample); } @@ -442,6 +431,7 @@ void cacheShouldFailOnNonConvertibleCacheKey() { Object key = SimpleKeyGenerator .generateKey(Collections.singletonList(new InvalidKey(sample.getFirstname(), sample.getBirthdate()))); + assertThatIllegalStateException().isThrownBy(() -> cache.put(key, sample)); } @@ -458,11 +448,6 @@ void multipleThreadsLoadValueOnce() throws InterruptedException { cache = new RedisCache("foo", new RedisCacheWriter() { - @Override - public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { - storage.set(value); - } - @Override public byte[] get(String name, byte[] key) { return get(name, key, null); @@ -481,6 +466,22 @@ public byte[] get(String name, byte[] key, @Nullable Duration ttl) { return storage.get(); } + @Override + public CompletableFuture retrieve(String name, byte[] key, @Nullable Duration ttl) { + byte[] value = get(name, key); + return CompletableFuture.completedFuture(value); + } + + @Override + public CompletableFuture store(String name, byte[] key, byte[] value, @Nullable Duration ttl) { + return null; + } + + @Override + public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { + storage.set(value); + } + @Override public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) { return new byte[0]; @@ -567,13 +568,110 @@ void cacheGetWithTimeToIdleExpirationAfterEntryExpiresShouldReturnNull() { assertThat(cache.get(this.cacheKey, Person.class)).isNull(); } - @Nullable - private Object unwrap(@Nullable Object value) { - return value instanceof ValueWrapper wrapper ? wrapper.get() : value; + @ParameterizedRedisTest + @EnabledOnRedisDriver(RedisDriver.JEDIS) + void retrieveCacheValueUsingJedis() { + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.cache.retrieve(this.binaryCacheKey)).withMessageContaining("RedisCache"); + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.cache.retrieve(this.binaryCacheKey, () -> CompletableFuture.completedFuture("TEST"))) + .withMessageContaining("RedisCache"); } - private RedisCacheWriter usingRedisCacheWriter() { - return RedisCacheWriter.nonLockingRedisCacheWriter(this.connectionFactory); + @ParameterizedRedisTest // GH-2650 + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + @SuppressWarnings("unchecked") + void retrieveReturnsCachedValue() throws Exception { + + doWithConnection(connection -> connection.stringCommands().set(this.binaryCacheKey, this.binarySample)); + + RedisCache cache = new RedisCache("cache", usingLockingRedisCacheWriter(), usingRedisCacheConfiguration()); + + CompletableFuture value = (CompletableFuture) cache.retrieve(this.key); + + assertThat(value).isNotNull(); + assertThat(value.get()).isEqualTo(this.sample); + assertThat(value).isDone(); + } + + @ParameterizedRedisTest // GH-2650 + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + @SuppressWarnings("unchecked") + void retrieveReturnsCachedValueWhenLockIsReleased() throws Exception { + + String testValue = "TestValue"; + + byte[] binaryCacheValue = this.serializer.serialize(testValue); + + doWithConnection(connection -> connection.stringCommands().set(this.binaryCacheKey, binaryCacheValue)); + + RedisCache cache = new RedisCache("cache", usingLockingRedisCacheWriter(Duration.ofMillis(5L)), + usingRedisCacheConfiguration()); + + DefaultRedisCacheWriter cacheWriter = (DefaultRedisCacheWriter) cache.getCacheWriter(); + cacheWriter.lock("cache"); + + CompletableFuture value = (CompletableFuture) cache.retrieve(this.key); + assertThat(value).isNotDone(); + + cacheWriter.unlock("cache"); + + assertThat(value.get(15L, TimeUnit.MILLISECONDS)).isEqualTo(testValue); + assertThat(value).isDone(); + } + + @ParameterizedRedisTest // GH-2650 + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + void retrieveReturnsLoadedValue() throws Exception { + + RedisCache cache = new RedisCache("cache", usingLockingRedisCacheWriter(), usingRedisCacheConfiguration()); + AtomicBoolean loaded = new AtomicBoolean(false); + Person jon = new Person("Jon", Date.from(Instant.now())); + CompletableFuture valueLoader = CompletableFuture.completedFuture(jon); + + Supplier> valueLoaderSupplier = () -> { + loaded.set(true); + return valueLoader; + }; + + CompletableFuture value = cache.retrieve(this.key, valueLoaderSupplier); + + assertThat(loaded.get()).isFalse(); + assertThat(value.get()).isEqualTo(jon); + assertThat(loaded.get()).isTrue(); + assertThat(value).isDone(); + } + + @ParameterizedRedisTest // GH-2650 + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + void retrieveStoresLoadedValue() throws Exception { + + RedisCache cache = new RedisCache("cache", usingLockingRedisCacheWriter(), usingRedisCacheConfiguration()); + Person jon = new Person("Jon", Date.from(Instant.now())); + Supplier> valueLoaderSupplier = () -> CompletableFuture.completedFuture(jon); + + cache.retrieve(this.key, valueLoaderSupplier).get(); + + doWithConnection( + connection -> assertThat(connection.keyCommands().exists("cache::key-1".getBytes(StandardCharsets.UTF_8))) + .isTrue()); + } + + @ParameterizedRedisTest // GH-2650 + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + void retrieveReturnsNull() throws Exception { + + doWithConnection(connection -> connection.stringCommands().set(this.binaryCacheKey, this.binaryNullValue)); + + RedisCache cache = new RedisCache("cache", usingLockingRedisCacheWriter(), usingRedisCacheConfiguration()); + + CompletableFuture value = cache.retrieve(this.key); + + assertThat(value).isNotNull(); + assertThat(value.get()).isNull(); + assertThat(value).isDone(); } private RedisCacheConfiguration usingRedisCacheConfiguration() { @@ -587,20 +685,39 @@ private RedisCacheConfiguration usingRedisCacheConfiguration( .serializeValuesWith(SerializationPair.fromSerializer(this.serializer))); } + private RedisCacheWriter usingRedisCacheWriter() { + return usingNonLockingRedisCacheWriter(); + } + + private RedisCacheWriter usingLockingRedisCacheWriter() { + return RedisCacheWriter.lockingRedisCacheWriter(this.connectionFactory); + } + + private RedisCacheWriter usingLockingRedisCacheWriter(Duration sleepTime) { + return RedisCacheWriter.lockingRedisCacheWriter(this.connectionFactory, sleepTime, + RedisCacheWriter.TtlFunction.persistent(), BatchStrategies.keys()); + } + + private RedisCacheWriter usingNonLockingRedisCacheWriter() { + return RedisCacheWriter.nonLockingRedisCacheWriter(this.connectionFactory); + } + + @Nullable + private Object unwrap(@Nullable Object value) { + return value instanceof ValueWrapper wrapper ? wrapper.get() : value; + } + private Function withTtiExpiration() { - Function entryTtlFunction = - cacheConfiguration -> cacheConfiguration.entryTtl(Duration.ofMillis(100)); + Function entryTtlFunction = cacheConfiguration -> cacheConfiguration + .entryTtl(Duration.ofMillis(100)); return entryTtlFunction.andThen(RedisCacheConfiguration::enableTimeToIdle); } void doWithConnection(Consumer callback) { - RedisConnection connection = connectionFactory.getConnection(); - try { + try (RedisConnection connection = connectionFactory.getConnection()) { callback.accept(connection); - } finally { - connection.close(); } } @@ -609,7 +726,7 @@ static class Person implements Serializable { private String firstname; private Date birthdate; - public Person() { } + public Person() {} public Person(String firstname, Date birthdate) { this.firstname = firstname; @@ -644,7 +761,7 @@ public boolean equals(Object obj) { } return Objects.equals(this.getFirstname(), that.getFirstname()) - && Objects.equals(this.getBirthdate(), that.getBirthdate()); + && Objects.equals(this.getBirthdate(), that.getBirthdate()); } @Override @@ -654,8 +771,7 @@ public int hashCode() { @Override public String toString() { - return "RedisCacheTests.Person(firstname=" + this.getFirstname() - + ", birthdate=" + this.getBirthdate() + ")"; + return "RedisCacheTests.Person(firstname=" + this.getFirstname() + ", birthdate=" + this.getBirthdate() + ")"; } } @@ -701,7 +817,7 @@ public boolean equals(final Object obj) { } return Objects.equals(this.getFirstname(), that.getFirstname()) - && Objects.equals(this.getBirthdate(), that.getBirthdate()); + && Objects.equals(this.getBirthdate(), that.getBirthdate()); } @Override @@ -711,8 +827,7 @@ public int hashCode() { @Override public String toString() { - return "RedisCacheTests.ComplexKey(firstame=" + this.getFirstname() - + ", birthdate=" + this.getBirthdate() + ")"; + return "RedisCacheTests.ComplexKey(firstame=" + this.getFirstname() + ", birthdate=" + this.getBirthdate() + ")"; } } } diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java new file mode 100644 index 0000000000..17663734a3 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.cache; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair; + +/** + * Unit tests for {@link RedisCache}. + * + * @author John Blum + * @author Mark Paluch + */ +class RedisCacheUnitTests { + + @Test // GH-2650 + void cacheRetrieveValueCallsCacheWriterRetrieveCorrectly() throws Exception { + + RedisCacheWriter mockCacheWriter = mock(RedisCacheWriter.class); + + when(mockCacheWriter.supportsAsyncRetrieve()).thenReturn(true); + when(mockCacheWriter.retrieve(anyString(), any(byte[].class))) + .thenReturn(CompletableFuture.completedFuture("TEST".getBytes())); + + RedisCache cache = new RedisCache("TestCache", mockCacheWriter, + RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(SerializationPair.byteArray())); + + CompletableFuture value = (CompletableFuture) cache.retrieve("TestKey"); + + assertThat(value).isNotNull(); + assertThat(new String(value.get())).isEqualTo("TEST"); + + verify(mockCacheWriter, times(1)).retrieve(eq("TestCache"), isA(byte[].class)); + verify(mockCacheWriter).supportsAsyncRetrieve(); + verifyNoMoreInteractions(mockCacheWriter); + } + +} diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheWriterUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheWriterUnitTests.java index eb563e791c..b3f01a06a0 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheWriterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheWriterUnitTests.java @@ -19,6 +19,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -27,6 +28,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import java.time.Duration; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.Test; @@ -56,4 +58,23 @@ void defaultGetWithNameKeyAndTtlCallsGetWithNameAndKeyDiscardingTtl() { verify(cacheWriter, times(1)).get(eq("TestCacheName"), eq(key)); verifyNoMoreInteractions(cacheWriter); } + + @Test // GH-2650 + void defaultRetrieveWithNameAndKeyCallsRetrieveWithNameKeyAndTtl() throws Exception { + + byte[] key = "TestKey".getBytes(); + byte[] value = "TestValue".getBytes(); + + RedisCacheWriter cacheWriter = mock(RedisCacheWriter.class); + + doCallRealMethod().when(cacheWriter).retrieve(anyString(), any()); + doReturn(CompletableFuture.completedFuture(value)).when(cacheWriter).retrieve(anyString(), any(), any()); + + assertThat(cacheWriter.retrieve("TestCacheName", key).thenApply(String::new).get()) + .isEqualTo("TestValue"); + + verify(cacheWriter, times(1)).retrieve(eq("TestCacheName"), eq(key)); + verify(cacheWriter, times(1)).retrieve(eq("TestCacheName"), eq(key), isNull()); + verifyNoMoreInteractions(cacheWriter); + } } diff --git a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriverCondition.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriverCondition.java index c7143f46f4..fb85bfe3bc 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriverCondition.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriverCondition.java @@ -59,10 +59,15 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con if (annotatedFields.isEmpty()) { throw new IllegalStateException( - "@EnabledOnRedisDriver requires a field of type RedisConnectionFactory annotated with @DriverQualifier"); + "@EnabledOnRedisDriver requires a field of type \"RedisConnectionFactory\" annotated with @DriverQualifier"); + } + + if (context.getTestInstance().isEmpty()) { + return ENABLED_BY_DEFAULT; } for (Field field : annotatedFields) { + Try fieldValue = ReflectionUtils.tryToReadFieldValue(field, context.getRequiredTestInstance()); RedisConnectionFactory value = (RedisConnectionFactory) fieldValue @@ -76,7 +81,8 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con } if (!foundMatch) { - return disabled(String.format("Driver %s not supported; Supported driver(s): %s", value, + return disabled(String.format("Driver %s not supported; Supported driver(s): %s", + formatUnsupportedDriver(value), Arrays.toString(annotation.value()))); } } @@ -85,4 +91,14 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con } + private static String formatUnsupportedDriver(RedisConnectionFactory value) { + + for (RedisDriver redisDriver : RedisDriver.values()) { + if (redisDriver.matches(value)) { + return redisDriver.toString(); + } + } + + return value.toString(); + } }