Polishing.

Replace blocking lock wait with non-blocking flow. Add support for asynchronous storage to persist the cache value after retrieval from the value supplier. Introduce AsyncCacheWriter abstraction to improve functional guards.

Reformat code. Remove redundant tests. Revisit deprecation notices with consistent mention of the version in which the deprecation was introduced.

Refine exception messages when RedisCache does not support async retrieval.

See #2650
Original pull request: #2717
This commit is contained in:
Mark Paluch
2023-10-10 13:51:53 +02:00
parent a72c4268a3
commit efc9ecf695
10 changed files with 731 additions and 828 deletions

View File

@@ -0,0 +1,118 @@
/*
* 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.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.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;
import org.springframework.data.redis.core.types.Expiration;
/**
* Unit tests for {@link DefaultRedisCacheWriter}
*
* @author John Blum
*/
@ExtendWith(MockitoExtension.class)
class DefaultRedisCacheWriterUnitTests {
@Mock private CacheStatisticsCollector mockCacheStatisticsCollector = mock(CacheStatisticsCollector.class);
@Mock private RedisConnection mockConnection;
@Mock(strictness = Mock.Strictness.LENIENT) private RedisConnectionFactory mockConnectionFactory;
@Mock private ReactiveRedisConnection mockReactiveConnection;
@Mock(strictness = Mock.Strictness.LENIENT) private TestReactiveRedisConnectionFactory mockReactiveConnectionFactory;
@BeforeEach
void setup() {
doReturn(this.mockConnection).when(this.mockConnectionFactory).getConnection();
doReturn(this.mockConnection).when(this.mockReactiveConnectionFactory).getConnection();
doReturn(this.mockReactiveConnection).when(this.mockReactiveConnectionFactory).getReactiveConnection();
}
private RedisCacheWriter newRedisCacheWriter() {
return spy(new DefaultRedisCacheWriter(this.mockConnectionFactory, mock(BatchStrategy.class))
.withStatisticsCollector(this.mockCacheStatisticsCollector));
}
private RedisCacheWriter newReactiveRedisCacheWriter() {
return spy(new DefaultRedisCacheWriter(this.mockReactiveConnectionFactory, Duration.ZERO, mock(BatchStrategy.class))
.withStatisticsCollector(this.mockCacheStatisticsCollector));
}
@Test // GH-2351
void getWithNonNullTtl() {
byte[] key = "TestKey".getBytes();
byte[] value = "TestValue".getBytes();
Duration ttl = Duration.ofSeconds(15);
Expiration expiration = Expiration.from(ttl);
RedisStringCommands mockStringCommands = mock(RedisStringCommands.class);
doReturn(mockStringCommands).when(this.mockConnection).stringCommands();
doReturn(value).when(mockStringCommands).getEx(any(), any());
RedisCacheWriter cacheWriter = newRedisCacheWriter();
assertThat(cacheWriter.get("TestCache", key, ttl)).isEqualTo(value);
verify(this.mockConnection).stringCommands();
verify(mockStringCommands).getEx(eq(key), eq(expiration));
verify(this.mockConnection).close();
verifyNoMoreInteractions(this.mockConnection, mockStringCommands);
}
@Test // GH-2351
void getWithNullTtl() {
byte[] key = "TestKey".getBytes();
byte[] value = "TestValue".getBytes();
RedisStringCommands mockStringCommands = mock(RedisStringCommands.class);
doReturn(mockStringCommands).when(this.mockConnection).stringCommands();
doReturn(value).when(mockStringCommands).get(any());
RedisCacheWriter cacheWriter = newRedisCacheWriter();
assertThat(cacheWriter.get("TestCache", key, null)).isEqualTo(value);
verify(this.mockConnection).stringCommands();
verify(mockStringCommands).get(eq(key));
verify(this.mockConnection).close();
verifyNoMoreInteractions(this.mockConnection, mockStringCommands);
}
interface TestReactiveRedisConnectionFactory extends ReactiveRedisConnectionFactory, RedisConnectionFactory {}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.redis.cache;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assumptions.*;
import static org.springframework.data.redis.cache.RedisCacheWriter.*;
import java.nio.charset.Charset;
@@ -23,6 +24,7 @@ import java.nio.charset.StandardCharsets;
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 +33,8 @@ import org.junit.jupiter.api.BeforeEach;
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.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.test.extension.RedisStanalone;
import org.springframework.data.redis.test.extension.parametrized.MethodSource;
import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest;
@@ -67,12 +67,6 @@ public class DefaultRedisCacheWriterTests {
@BeforeEach
void setUp() {
JedisConnectionFactory connectionFactory =
JedisConnectionFactoryExtension.getConnectionFactory(RedisStanalone.class);
this.connectionFactory = connectionFactory;
doWithConnection(RedisConnection::flushAll);
}
@@ -152,6 +146,49 @@ public class DefaultRedisCacheWriterTests {
assertThat(nonLockingRedisCacheWriter(connectionFactory).get(CACHE_NAME, binaryCacheKey)).isNull();
}
@ParameterizedRedisTest // GH-2650
void cacheHitRetrieveShouldIncrementStatistics() throws ExecutionException, InterruptedException {
assumeThat(connectionFactory).isInstanceOf(LettuceConnectionFactory.class);
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
void storeShouldIncrementStatistics() throws ExecutionException, InterruptedException {
assumeThat(connectionFactory).isInstanceOf(LettuceConnectionFactory.class);
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
void cacheMissRetrieveWithLoaderAsyncShouldIncrementStatistics() throws ExecutionException, InterruptedException {
assumeThat(connectionFactory).isInstanceOf(LettuceConnectionFactory.class);
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 +290,8 @@ public class DefaultRedisCacheWriterTests {
((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 +378,7 @@ public class DefaultRedisCacheWriterTests {
afterWrite.await();
assertThat(exceptionRef.get()).hasMessageContaining("Interrupted while waiting to unlock")
.hasCauseInstanceOf(InterruptedException.class);
assertThat(exceptionRef.get()).hasRootCauseInstanceOf(InterruptedException.class);
}
@ParameterizedRedisTest // GH-2300

View File

@@ -1,324 +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.assertj.core.api.Assertions.assertThatIllegalArgumentException;
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.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
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.ReactiveRedisConnection;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.connection.ReactiveStringCommands;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisKeyCommands;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.types.Expiration;
import reactor.core.publisher.Mono;
/**
* Unit tests for {@link DefaultRedisCacheWriter}
*
* @author John Blum
*/
@ExtendWith(MockitoExtension.class)
class DefaultRedisCacheWriterUnitTests {
@Mock
private CacheStatisticsCollector mockCacheStatisticsCollector = mock(CacheStatisticsCollector.class);
@Mock
private RedisConnection mockConnection;
@Mock(strictness = Mock.Strictness.LENIENT)
private RedisConnectionFactory mockConnectionFactory;
@Mock
private ReactiveRedisConnection mockReactiveConnection;
@Mock(strictness = Mock.Strictness.LENIENT)
private TestReactiveRedisConnectionFactory mockReactiveConnectionFactory;
@BeforeEach
void setup() {
doReturn(this.mockConnection).when(this.mockConnectionFactory).getConnection();
doReturn(this.mockConnection).when(this.mockReactiveConnectionFactory).getConnection();
doReturn(this.mockReactiveConnection).when(this.mockReactiveConnectionFactory).getReactiveConnection();
}
private RedisCacheWriter newRedisCacheWriter() {
return new DefaultRedisCacheWriter(this.mockConnectionFactory, mock(BatchStrategy.class))
.withStatisticsCollector(this.mockCacheStatisticsCollector);
}
private RedisCacheWriter newReactiveRedisCacheWriter() {
return newReactiveRedisCacheWriter(Duration.ZERO);
}
private RedisCacheWriter newReactiveRedisCacheWriter(Duration sleepTime) {
return new DefaultRedisCacheWriter(this.mockReactiveConnectionFactory, sleepTime, mock(BatchStrategy.class))
.withStatisticsCollector(this.mockCacheStatisticsCollector);
}
@Test // GH-2351
void getWithNonNullTtl() {
byte[] key = "TestKey".getBytes();
byte[] value = "TestValue".getBytes();
Duration ttl = Duration.ofSeconds(15);
Expiration expiration = Expiration.from(ttl);
RedisStringCommands mockStringCommands = mock(RedisStringCommands.class);
doReturn(mockStringCommands).when(this.mockConnection).stringCommands();
doReturn(value).when(mockStringCommands).getEx(any(), any());
RedisCacheWriter cacheWriter = newRedisCacheWriter();
assertThat(cacheWriter.get("TestCache", key, ttl)).isEqualTo(value);
verify(this.mockConnection, times(1)).stringCommands();
verify(mockStringCommands, times(1)).getEx(eq(key), eq(expiration));
verify(this.mockConnection).close();
verifyNoMoreInteractions(this.mockConnection, mockStringCommands);
}
@Test // GH-2351
void getWithNullTtl() {
byte[] key = "TestKey".getBytes();
byte[] value = "TestValue".getBytes();
RedisStringCommands mockStringCommands = mock(RedisStringCommands.class);
doReturn(mockStringCommands).when(this.mockConnection).stringCommands();
doReturn(value).when(mockStringCommands).get(any());
RedisCacheWriter cacheWriter = newRedisCacheWriter();
assertThat(cacheWriter.get("TestCache", key, null)).isEqualTo(value);
verify(this.mockConnection, times(1)).stringCommands();
verify(mockStringCommands, times(1)).get(eq(key));
verify(this.mockConnection).close();
verifyNoMoreInteractions(this.mockConnection, mockStringCommands);
}
@Test // GH-2650
@SuppressWarnings("all")
void retrieveWithNoCacheName() {
byte[] key = "TestKey".getBytes();
RedisCacheWriter cacheWriter = newReactiveRedisCacheWriter();
assertThatIllegalArgumentException()
.isThrownBy(() -> cacheWriter.retrieve(null, key))
.withMessage("Name must not be null")
.withNoCause();
verifyNoInteractions(this.mockReactiveConnectionFactory);
}
@Test // GH-2650
@SuppressWarnings("all")
void retrieveWithNoKey() {
RedisCacheWriter cacheWriter = newReactiveRedisCacheWriter();
assertThatIllegalArgumentException()
.isThrownBy(() -> cacheWriter.retrieve("TestCacheName", null))
.withMessage("Key must not be null")
.withNoCause();
verifyNoInteractions(this.mockReactiveConnectionFactory);
}
@Test // GH-2650
void retrieveReturnsAsyncFutureWithValue() throws Exception {
byte[] key = "TestKey".getBytes();
RedisStringCommands mockStringCommands = mock(RedisStringCommands.class);
doReturn(mockStringCommands).when(this.mockConnection).stringCommands();
doReturn("test".getBytes()).when(mockStringCommands).get(any(byte[].class));
RedisCacheWriter cacheWriter = newRedisCacheWriter();
CompletableFuture<byte[]> result = cacheWriter.retrieve("TestCacheName", key);
assertThat(result).isNotNull();
verifyNoInteractions(this.mockCacheStatisticsCollector);
byte[] value = result.get();
assertThat(value).isNotNull();
assertThat(new String(value)).isEqualTo("test");
verify(mockStringCommands, times(1)).get(eq(key));
verify(this.mockCacheStatisticsCollector, times(1)).incGets(eq("TestCacheName"));
verify(this.mockCacheStatisticsCollector, times(1)).incHits(eq("TestCacheName"));
verifyNoMoreInteractions(mockStringCommands, this.mockCacheStatisticsCollector);
}
@Test // GH-2650
void retrieveWithExpirationReturnsAsyncFutureWithValue() throws Exception {
byte[] key = "TestKey".getBytes();
Duration thirtySeconds = Duration.ofSeconds(30L);
RedisStringCommands mockStringCommands = mock(RedisStringCommands.class);
doReturn(mockStringCommands).when(this.mockConnection).stringCommands();
doReturn("test".getBytes()).when(mockStringCommands).getEx(any(byte[].class), any(Expiration.class));
RedisCacheWriter cacheWriter = newRedisCacheWriter();
CompletableFuture<byte[]> result = cacheWriter.retrieve("TestCacheName", key, thirtySeconds);
assertThat(result).isNotNull();
byte[] value = result.get();
assertThat(value).isNotNull();
assertThat(new String(value)).isEqualTo("test");
verify(mockStringCommands, times(1)).getEx(eq(key), eq(Expiration.from(thirtySeconds)));
verify(this.mockCacheStatisticsCollector, times(1)).incGets(eq("TestCacheName"));
verify(this.mockCacheStatisticsCollector, times(1)).incHits(eq("TestCacheName"));
verifyNoMoreInteractions(mockStringCommands, this.mockCacheStatisticsCollector);
}
@Test // GH-2650
void retrieveReturnsReactiveFutureWithValue() throws Exception {
byte[] key = "TestKey".getBytes();
Duration sixtySeconds = Duration.ofMillis(60L);
RedisKeyCommands mockKeyCommands = mock(RedisKeyCommands.class);
ReactiveStringCommands mockStringCommands = mock(ReactiveStringCommands.class);
doReturn(mockKeyCommands).when(this.mockConnection).keyCommands();
doReturn(false).when(mockKeyCommands).exists(any(byte[].class));
doReturn(mockStringCommands).when(this.mockReactiveConnection).stringCommands();
doReturn(Mono.just(ByteBuffer.wrap("test".getBytes()))).when(mockStringCommands).get(any(ByteBuffer.class));
RedisCacheWriter cacheWriter = newReactiveRedisCacheWriter(sixtySeconds);
CompletableFuture<byte[]> result = cacheWriter.retrieve("TestCacheName", key);
assertThat(result).isNotNull();
byte[] value = result.get();
assertThat(value).isNotNull();
assertThat(new String(value)).isEqualTo("test");
verify(mockKeyCommands, times(1)).exists(any(byte[].class));
verify(mockStringCommands, times(1)).get(eq(ByteBuffer.wrap(key)));
verify(this.mockCacheStatisticsCollector, times(1)).incGets(eq("TestCacheName"));
verify(this.mockCacheStatisticsCollector, times(1)).incHits(eq("TestCacheName"));
verifyNoMoreInteractions(mockKeyCommands, mockStringCommands, this.mockCacheStatisticsCollector);
}
@Test // GH-2650
void retrieveReturnsReactiveFutureWithNoValue() throws Exception {
byte[] key = "TestKey".getBytes();
RedisKeyCommands mockKeyCommands = mock(RedisKeyCommands.class);
ReactiveStringCommands mockStringCommands = mock(ReactiveStringCommands.class);
doReturn(mockKeyCommands).when(this.mockConnection).keyCommands();
doReturn(false).when(mockKeyCommands).exists(any(byte[].class));
doReturn(mockStringCommands).when(this.mockReactiveConnection).stringCommands();
doReturn(Mono.empty()).when(mockStringCommands).get(any(ByteBuffer.class));
RedisCacheWriter cacheWriter = newReactiveRedisCacheWriter();
CompletableFuture<byte[]> result = cacheWriter.retrieve("TestCacheName", key);
assertThat(result).isNotNull();
byte[] value = result.get();
assertThat(value).isNull();
verify(mockKeyCommands, times(1)).exists(any(byte[].class));
verify(mockStringCommands, times(1)).get(eq(ByteBuffer.wrap(key)));
verify(this.mockCacheStatisticsCollector, times(1)).incGets(eq("TestCacheName"));
verify(this.mockCacheStatisticsCollector, times(1)).incMisses(eq("TestCacheName"));
verifyNoMoreInteractions(mockKeyCommands, mockStringCommands, this.mockCacheStatisticsCollector);
}
@Test // GH-2650
void retrieveWithExpirationReturnsReactiveFutureWithValue() throws Exception {
byte[] key = "TestKey".getBytes();
Duration twoMinutes = Duration.ofMinutes(2L);
RedisKeyCommands mockKeyCommands = mock(RedisKeyCommands.class);
ReactiveStringCommands mockStringCommands = mock(ReactiveStringCommands.class);
doReturn(mockKeyCommands).when(this.mockConnection).keyCommands();
doReturn(false).when(mockKeyCommands).exists(any(byte[].class));
doReturn(mockStringCommands).when(this.mockReactiveConnection).stringCommands();
doReturn(Mono.just(ByteBuffer.wrap("test".getBytes()))).when(mockStringCommands).getEx(any(ByteBuffer.class), any());
RedisCacheWriter cacheWriter = newReactiveRedisCacheWriter();
CompletableFuture<byte[]> result = cacheWriter.retrieve("TestCacheName", key, twoMinutes);
assertThat(result).isNotNull();
byte[] value = result.get();
assertThat(value).isNotNull();
assertThat(new String(value)).isEqualTo("test");
verify(mockKeyCommands, times(1)).exists(any(byte[].class));
verify(mockStringCommands, times(1)).getEx(eq(ByteBuffer.wrap(key)), eq(Expiration.from(twoMinutes)));
verify(this.mockCacheStatisticsCollector, times(1)).incGets(eq("TestCacheName"));
verify(this.mockCacheStatisticsCollector, times(1)).incHits(eq("TestCacheName"));
verifyNoMoreInteractions(mockKeyCommands, mockStringCommands, this.mockCacheStatisticsCollector);
}
interface TestReactiveRedisConnectionFactory extends ReactiveRedisConnectionFactory, RedisConnectionFactory { }
}

View File

@@ -15,18 +15,16 @@
*/
package org.springframework.data.redis.cache;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.awaitility.Awaitility.await;
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;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneOffset;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@@ -45,7 +43,6 @@ import java.util.function.Supplier;
import java.util.stream.IntStream;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.cache.Cache.ValueWrapper;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.cache.interceptor.SimpleKeyGenerator;
@@ -57,14 +54,10 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
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.RedisDriver;
import org.springframework.data.redis.test.extension.parametrized.MethodSource;
import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest;
import org.springframework.lang.Nullable;
import io.netty.util.concurrent.DefaultThreadFactory;
/**
* Tests for {@link RedisCache} with {@link DefaultRedisCacheWriter} using different {@link RedisSerializer} and
* {@link RedisConnectionFactory} pairs.
@@ -353,8 +346,9 @@ public class RedisCacheTests {
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
@@ -366,8 +360,9 @@ public class RedisCacheTests {
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
@@ -429,8 +424,8 @@ public class RedisCacheTests {
.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);
}
@@ -481,6 +476,11 @@ public class RedisCacheTests {
return CompletableFuture.completedFuture(value);
}
@Override
public CompletableFuture<Void> 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);
@@ -572,28 +572,18 @@ public class RedisCacheTests {
assertThat(cache.get(this.cacheKey, Person.class)).isNull();
}
@ParameterizedRedisTest
@ParameterizedRedisTest // GH-2650
void retrieveCacheValueUsingJedis() {
// TODO: Is there a better way to do this? @EnableOnRedisDriver(RedisDriver.JEDIS) does not work!
assumeThat(this.connectionFactory instanceof JedisConnectionFactory).isTrue();
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> this.cache.retrieve(this.binaryCacheKey))
.withMessageContaining(RedisCache.class.getName())
.withNoCause();
}
@ParameterizedRedisTest
void retrieveCacheValueWithLoaderUsingJedis() {
// TODO: Is there a better way to do this? @EnableOnRedisDriver(RedisDriver.JEDIS) does not work!
assumeThat(this.connectionFactory instanceof JedisConnectionFactory).isTrue();
.isThrownBy(() -> this.cache.retrieve(this.binaryCacheKey)).withMessageContaining("RedisCache");
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> this.cache.retrieve(this.binaryCacheKey, () -> CompletableFuture.completedFuture("TEST")))
.withMessageContaining(RedisCache.class.getName())
.withNoCause();
.isThrownBy(() -> this.cache.retrieve(this.binaryCacheKey, () -> CompletableFuture.completedFuture("TEST")))
.withMessageContaining("RedisCache");
}
@ParameterizedRedisTest // GH-2650
@@ -631,19 +621,13 @@ public class RedisCacheTests {
RedisCache cache = new RedisCache("cache", usingLockingRedisCacheWriter(Duration.ofMillis(5L)),
usingRedisCacheConfiguration());
RedisCacheWriter cacheWriter = cache.getCacheWriter();
assertThat(cacheWriter).isInstanceOf(DefaultRedisCacheWriter.class);
((DefaultRedisCacheWriter) cacheWriter).lock("cache");
DefaultRedisCacheWriter cacheWriter = (DefaultRedisCacheWriter) cache.getCacheWriter();
cacheWriter.lock("cache");
CompletableFuture<String> value = (CompletableFuture<String>) cache.retrieve(this.key);
assertThat(value).isNotNull();
assertThat(value.getNow(mockValue)).isEqualTo(mockValue);
assertThat(value).isNotDone();
((DefaultRedisCacheWriter) cacheWriter).unlock("cache");
cacheWriter.unlock("cache");
assertThat(value.get(15L, TimeUnit.MILLISECONDS)).isEqualTo(testValue);
assertThat(value).isDone();
@@ -656,14 +640,8 @@ public class RedisCacheTests {
assumeThat(this.connectionFactory instanceof LettuceConnectionFactory).isTrue();
RedisCache cache = new RedisCache("cache", usingLockingRedisCacheWriter(), usingRedisCacheConfiguration());
AtomicBoolean loaded = new AtomicBoolean(false);
Date birthdate = Date.from(LocalDateTime.of(2023, Month.SEPTEMBER, 22, 17, 3)
.toInstant(ZoneOffset.UTC));
Person jon = new Person("Jon", birthdate);
Person jon = new Person("Jon", Date.from(Instant.now()));
CompletableFuture<Person> valueLoader = CompletableFuture.completedFuture(jon);
Supplier<CompletableFuture<Person>> valueLoaderSupplier = () -> {
@@ -673,13 +651,29 @@ public class RedisCacheTests {
CompletableFuture<Person> value = cache.retrieve(this.key, valueLoaderSupplier);
assertThat(value).isNotNull();
assertThat(loaded.get()).isFalse();
assertThat(value.get()).isEqualTo(jon);
assertThat(loaded.get()).isTrue();
assertThat(value).isDone();
}
@ParameterizedRedisTest // GH-2650
void retrieveStoresLoadedValue() throws Exception {
// TODO: Is there a better way to do this? @EnableOnRedisDriver(RedisDriver.LETTUCE) does not work!
assumeThat(this.connectionFactory instanceof LettuceConnectionFactory).isTrue();
RedisCache cache = new RedisCache("cache", usingLockingRedisCacheWriter(), usingRedisCacheConfiguration());
Person jon = new Person("Jon", Date.from(Instant.now()));
Supplier<CompletableFuture<Person>> 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
void retrieveReturnsNull() throws Exception {
@@ -732,8 +726,8 @@ public class RedisCacheTests {
private Function<RedisCacheConfiguration, RedisCacheConfiguration> withTtiExpiration() {
Function<RedisCacheConfiguration, RedisCacheConfiguration> entryTtlFunction =
cacheConfiguration -> cacheConfiguration.entryTtl(Duration.ofMillis(100));
Function<RedisCacheConfiguration, RedisCacheConfiguration> entryTtlFunction = cacheConfiguration -> cacheConfiguration
.entryTtl(Duration.ofMillis(100));
return entryTtlFunction.andThen(RedisCacheConfiguration::enableTimeToIdle);
}
@@ -752,7 +746,7 @@ public class RedisCacheTests {
private String firstname;
private Date birthdate;
public Person() { }
public Person() {}
public Person(String firstname, Date birthdate) {
this.firstname = firstname;
@@ -787,7 +781,7 @@ public class RedisCacheTests {
}
return Objects.equals(this.getFirstname(), that.getFirstname())
&& Objects.equals(this.getBirthdate(), that.getBirthdate());
&& Objects.equals(this.getBirthdate(), that.getBirthdate());
}
@Override
@@ -797,8 +791,7 @@ public class RedisCacheTests {
@Override
public String toString() {
return "RedisCacheTests.Person(firstname=" + this.getFirstname()
+ ", birthdate=" + this.getBirthdate() + ")";
return "RedisCacheTests.Person(firstname=" + this.getFirstname() + ", birthdate=" + this.getBirthdate() + ")";
}
}
@@ -844,7 +837,7 @@ public class RedisCacheTests {
}
return Objects.equals(this.getFirstname(), that.getFirstname())
&& Objects.equals(this.getBirthdate(), that.getBirthdate());
&& Objects.equals(this.getBirthdate(), that.getBirthdate());
}
@Override
@@ -854,8 +847,7 @@ public class RedisCacheTests {
@Override
public String toString() {
return "RedisCacheTests.ComplexKey(firstame=" + this.getFirstname()
+ ", birthdate=" + this.getBirthdate() + ")";
return "RedisCacheTests.ComplexKey(firstame=" + this.getFirstname() + ", birthdate=" + this.getBirthdate() + ")";
}
}
}

View File

@@ -15,29 +15,20 @@
*/
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.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
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.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
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.cache.support.NullValue;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
/**
* Unit tests for {@link RedisCache}.
*
* @author John Blum
* @author Mark Paluch
*/
class RedisCacheUnitTests {
@@ -46,76 +37,21 @@ class RedisCacheUnitTests {
RedisCacheWriter mockCacheWriter = mock(RedisCacheWriter.class);
doReturn(CompletableFuture.completedFuture("TEST".getBytes()))
.when(mockCacheWriter).retrieve(anyString(), any(byte[].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());
RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(SerializationPair.byteArray()));
CompletableFuture<byte[]> value = cache.retrieveValue("TestKey");
CompletableFuture<byte[]> value = (CompletableFuture<byte[]>) 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);
}
@Test // GH-2650
void nullSafeDeserializedStoreValueWithNullValueIsNullSafe() {
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
RedisCacheWriter mockCacheWriter = mock(RedisCacheWriter.class);
RedisCache cache = new RedisCache("TestCache", mockCacheWriter, cacheConfiguration);
assertThat(cache.nullSafeDeserializedStoreValue(null)).isNull();
verifyNoInteractions(mockCacheWriter);
}
@Test // GH-2650
void nullSafeDeserializedStoreValueWithBinaryNullValueAllowingNullValues() {
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
RedisCacheWriter mockCacheWriter = mock(RedisCacheWriter.class);
RedisCache cache = new RedisCache("TestCache", mockCacheWriter, cacheConfiguration);
assertThat(cacheConfiguration.getAllowCacheNullValues()).isTrue();
assertThat(cache.nullSafeDeserializedStoreValue(RedisCache.BINARY_NULL_VALUE)).isNull();
verifyNoInteractions(mockCacheWriter);
}
@Test // GH-2650
void nullSafeDeserializedStoreValueWithBinaryNullValueDisablingNullValues() {
RedisCacheConfiguration cacheConfiguration =
RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues();
RedisCacheWriter mockCacheWriter = mock(RedisCacheWriter.class);
RedisCache cache = new RedisCache("TestCache", mockCacheWriter, cacheConfiguration);
assertThat(cacheConfiguration.getAllowCacheNullValues()).isFalse();
assertThat(cache.nullSafeDeserializedStoreValue(RedisCache.BINARY_NULL_VALUE)).isEqualTo(NullValue.INSTANCE);
verifyNoInteractions(mockCacheWriter);
}
@Test // GH-2650
void nullSafeDeserializedStoreValueWithNonNullValue() {
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
byte[] serializedValue = ByteUtils.getBytes(cacheConfiguration.getValueSerializationPair()
.write("TestValue"));
RedisCacheWriter mockCacheWriter = mock(RedisCacheWriter.class);
RedisCache cache = new RedisCache("TestCache", mockCacheWriter, cacheConfiguration);
assertThat(cache.nullSafeDeserializedStoreValue(serializedValue)).isEqualTo("TestValue");
verifyNoInteractions(mockCacheWriter);
}
}