DATAREDIS-481 - Polishing.

Reduce visibility of DefaultRedisCacheWriter as trivial implementation. Replace ConnectionCallback with Java 8 functions. Increase visibility of the RedisCache constructor to allow subclassing. Move factory methods to create DefaultRedisCacheWriter to RedisCacheWriter interface.

Refactor RedisCacheManagerConfigurator to stateful RedisCacheManagerBuilder. Remove RedisCache.setTransactionAware override in RedisCacheManagerConfigurator removing behavior not compliant with AbstractTransactionSupportingCacheManager. Terminate interrupted cache wait loop with exception. Javadoc, formatting, reorder methods.

Allow ConversionService configuration via RedisCacheConfiguration. Accept cache keys that are either convertible to String or that override toString to obtain the String representation of the cache key.

Original pull request: #252.
This commit is contained in:
Mark Paluch
2017-07-10 14:52:32 +02:00
parent a9c2b1cdda
commit 36f680ff5b
10 changed files with 503 additions and 249 deletions

View File

@@ -16,12 +16,15 @@
package org.springframework.data.redis.cache;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.redis.cache.DefaultRedisCacheWriter.*;
import static org.springframework.data.redis.cache.RedisCacheWriter.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import org.junit.AfterClass;
@@ -39,6 +42,7 @@ import org.springframework.data.redis.core.types.Expiration;
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
@RunWith(Parameterized.class)
public class DefaultRedisCacheWriterTests {
@@ -47,9 +51,9 @@ public class DefaultRedisCacheWriterTests {
String key = "key-1";
String cacheKey = CACHE_NAME + "::" + key;
byte[] binaryCacheKey = cacheKey.getBytes(Charset.forName("UTF-8"));
byte[] binaryCacheValue = "value".getBytes(Charset.forName("UTF-8"));
byte[] binaryCacheKey = cacheKey.getBytes(StandardCharsets.UTF_8);
byte[] binaryCacheValue = "value".getBytes(StandardCharsets.UTF_8);
RedisConnectionFactory connectionFactory;
@@ -211,7 +215,7 @@ public class DefaultRedisCacheWriterTests {
@Test // DATAREDIS-481
public void nonLockingCacheWriterShouldIgnoreExistingLock() {
lockingRedisCacheWriter(connectionFactory).lock(CACHE_NAME);
((DefaultRedisCacheWriter) lockingRedisCacheWriter(connectionFactory)).lock(CACHE_NAME);
nonLockingRedisCacheWriter(connectionFactory).put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO);
@@ -223,7 +227,7 @@ public class DefaultRedisCacheWriterTests {
@Test // DATAREDIS-481
public void lockingCacheWriterShouldIgnoreExistingLockOnDifferenceCache() {
lockingRedisCacheWriter(connectionFactory).lock(CACHE_NAME);
((DefaultRedisCacheWriter) lockingRedisCacheWriter(connectionFactory)).lock(CACHE_NAME);
lockingRedisCacheWriter(connectionFactory).put(CACHE_NAME + "-no-the-other-cache", binaryCacheKey, binaryCacheValue,
Duration.ZERO);
@@ -236,15 +240,25 @@ public class DefaultRedisCacheWriterTests {
@Test // DATAREDIS-481
public void lockingCacheWriterShouldWaitForLockRelease() throws InterruptedException {
DefaultRedisCacheWriter cw = lockingRedisCacheWriter(connectionFactory);
DefaultRedisCacheWriter cw = (DefaultRedisCacheWriter) lockingRedisCacheWriter(connectionFactory);
cw.lock(CACHE_NAME);
CountDownLatch beforeWrite = new CountDownLatch(1);
CountDownLatch afterWrite = new CountDownLatch(1);
Thread th = new Thread(() -> {
lockingRedisCacheWriter(connectionFactory).put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO);
RedisCacheWriter writer = lockingRedisCacheWriter(connectionFactory);
beforeWrite.countDown();
writer.put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO);
afterWrite.countDown();
});
th.start();
try {
beforeWrite.await();
Thread.sleep(200);
doWithConnection(connection -> {
@@ -253,8 +267,7 @@ public class DefaultRedisCacheWriterTests {
cw.unlock(CACHE_NAME);
Thread.sleep(200);
afterWrite.await();
doWithConnection(connection -> {
assertThat(connection.exists(binaryCacheKey)).isTrue();
});
@@ -263,7 +276,49 @@ public class DefaultRedisCacheWriterTests {
}
}
void doWithConnection(Consumer<RedisConnection> callback) {
@Test // DATAREDIS-481
public void lockingCacheWriterShouldExitWhenInterruptedWaitForLockRelease() throws InterruptedException {
DefaultRedisCacheWriter cw = (DefaultRedisCacheWriter) lockingRedisCacheWriter(connectionFactory);
cw.lock(CACHE_NAME);
CountDownLatch beforeWrite = new CountDownLatch(1);
CountDownLatch afterWrite = new CountDownLatch(1);
AtomicReference<Exception> exceptionRef = new AtomicReference<>();
Thread th = new Thread(() -> {
DefaultRedisCacheWriter writer = new DefaultRedisCacheWriter(connectionFactory, Duration.ofMillis(50)) {
@Override
boolean doCheckLock(String name, RedisConnection connection) {
beforeWrite.countDown();
return super.doCheckLock(name, connection);
}
};
try {
writer.put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO);
} catch (Exception e) {
exceptionRef.set(e);
} finally {
afterWrite.countDown();
}
});
th.start();
beforeWrite.await();
th.interrupt();
afterWrite.await();
assertThat(exceptionRef.get()).hasMessageContaining("Interrupted while waiting to unlock")
.hasCauseInstanceOf(InterruptedException.class);
}
private void doWithConnection(Consumer<RedisConnection> callback) {
RedisConnection connection = connectionFactory.getConnection();
try {
callback.accept(connection);
@@ -271,5 +326,4 @@ public class DefaultRedisCacheWriterTests {
connection.close();
}
}
}

View File

@@ -25,12 +25,13 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.cache.Cache;
import org.springframework.cache.transaction.TransactionAwareCacheDecorator;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.test.util.ReflectionTestUtils;
/**
* Unit tests for {@link RedisCacheManager}.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.class)
public class RedisCacheManagerUnitTests {
@@ -42,17 +43,39 @@ public class RedisCacheManagerUnitTests {
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().disableKeyPrefix();
RedisCacheManager cm = RedisCacheManager.usingCacheWriter(cacheWriter).withCacheDefaults(configuration).createAndGet();
RedisCacheManager cm = RedisCacheManager.builder(cacheWriter).cacheDefaults(configuration).build();
cm.afterPropertiesSet();
assertThat(cm.getMissingCache("new-cache").getCacheConfiguration()).isEqualTo(configuration);
}
@Test // DATAREDIS-481
public void appliesDefaultConfigurationToInitialCache() {
RedisCacheConfiguration withPrefix = RedisCacheConfiguration.defaultCacheConfig().disableKeyPrefix();
RedisCacheConfiguration withoutPrefix = RedisCacheConfiguration.defaultCacheConfig().disableKeyPrefix();
RedisCacheManager cm = RedisCacheManager.builder(cacheWriter).cacheDefaults(withPrefix) //
.initialCacheNames(Collections.singleton("first-cache")) //
.cacheDefaults(withoutPrefix) //
.initialCacheNames(Collections.singleton("second-cache")) //
.build();
cm.afterPropertiesSet();
assertThat(((RedisCache) cm.getCache("first-cache")).getCacheConfiguration()).isEqualTo(withPrefix);
assertThat(((RedisCache) cm.getCache("second-cache")).getCacheConfiguration()).isEqualTo(withoutPrefix);
assertThat(((RedisCache) cm.getCache("other-cache")).getCacheConfiguration()).isEqualTo(withoutPrefix);
}
@Test // DATAREDIS-481
public void predefinedCacheShouldBeCreatedWithSpecificConfig() {
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().disableKeyPrefix();
RedisCacheManager cm = RedisCacheManager.usingCacheWriter(cacheWriter)
.withInitialCacheConfigurations(Collections.singletonMap("predefined-cache", configuration)).createAndGet();
RedisCacheManager cm = RedisCacheManager.builder(cacheWriter)
.withInitialCacheConfigurations(Collections.singletonMap("predefined-cache", configuration)).build();
cm.afterPropertiesSet();
assertThat(((RedisCache) cm.getCache("predefined-cache")).getCacheConfiguration()).isEqualTo(configuration);
assertThat(cm.getMissingCache("new-cache").getCacheConfiguration()).isNotEqualTo(configuration);
@@ -61,20 +84,9 @@ public class RedisCacheManagerUnitTests {
@Test // DATAREDIS-481
public void transactionAwareCacheManagerShouldDecoracteCache() {
Cache cache = RedisCacheManager.usingCacheWriter(cacheWriter).transactionAware().createAndGet()
.getCache("decoracted-cache");
Cache cache = RedisCacheManager.builder(cacheWriter).transactionAware().build().getCache("decoracted-cache");
assertThat(cache).isInstanceOfAny(TransactionAwareCacheDecorator.class);
assertThat(ReflectionTestUtils.getField(cache, "targetCache")).isInstanceOf(RedisCache.class);
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager.usingRawConnectionFactory(connectionFactory)
.withCacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
.withInitialCacheConfigurations(Collections.singletonMap("predefined", RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()))
.transactionAware()
.createAndGet();
}
}

View File

@@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import java.io.Serializable;
import java.nio.charset.Charset;
@@ -34,6 +35,7 @@ import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.cache.Cache.ValueWrapper;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.cache.support.NullValue;
import org.springframework.data.redis.ConnectionFactoryTracker;
import org.springframework.data.redis.connection.RedisConnection;
@@ -41,13 +43,13 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.test.util.ReflectionTestUtils;
/**
* Tests for {@link RedisCache} with {@link DefaultRedisCacheWriter} using different {@link RedisSerializer} and
* {@link RedisConnectionFactory} pairs.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
@RunWith(Parameterized.class)
public class RedisCacheTests {
@@ -59,8 +61,7 @@ public class RedisCacheTests {
Person sample = new Person("calmity", new Date());
byte[] binarySample;
NullValue nullValue = NullValue.class.cast(ReflectionTestUtils.getField(NullValue.class, "INSTANCE"));
byte[] binaryNullValue = new JdkSerializationRedisSerializer().serialize(nullValue);
byte[] binaryNullValue = new JdkSerializationRedisSerializer().serialize(NullValue.INSTANCE);
RedisConnectionFactory connectionFactory;
RedisSerializer serializer;
@@ -179,6 +180,38 @@ public class RedisCacheTests {
assertThat(result.get()).isEqualTo(sample);
}
@Test // DATAREDIS-481
public void shouldReadAndWriteSimpleCacheKey() {
SimpleKey key = new SimpleKey("param-1", "param-2");
cache.put(key, sample);
ValueWrapper result = cache.get(key);
assertThat(result).isNotNull();
assertThat(result.get()).isEqualTo(sample);
}
@Test(expected = IllegalStateException.class) // DATAREDIS-481
public void shouldRejectNonInvalidKey() {
InvalidKey key = new InvalidKey(sample.getFirstame(), sample.getBirthdate());
cache.put(key, sample);
}
@Test // DATAREDIS-481
public void shouldAllowComplexKeyWithToStringMethod() {
ComplexKey key = new ComplexKey(sample.getFirstame(), sample.getBirthdate());
cache.put(key, sample);
ValueWrapper result = cache.get(key);
assertThat(result).isNotNull();
assertThat(result.get()).isEqualTo(sample);
}
@Test // DATAREDIS-481
public void getShouldReturnNullWhenKeyDoesNotExist() {
assertThat(cache.get(key)).isNull();
@@ -253,4 +286,16 @@ public class RedisCacheTests {
Date birthdate;
}
@RequiredArgsConstructor // toString not overridden
static class InvalidKey implements Serializable {
final String firstame;
final Date birthdate;
}
@Data
@RequiredArgsConstructor
static class ComplexKey implements Serializable {
final String firstame;
final Date birthdate;
}
}