DATAREDIS-246 - RedisCacheManager.getCache() does not match the logical of CompositeCacheManager.

By default RedisCacheManager lazily initializes RedisCaches when requested. To put it into static mode a defined set of cache names has to be passed to the CacheManager.

We changed the implementation so it  makes use of AbstractTransactionalCache introduced in Spring 3.2 which properly registers caches. Additionally the loadRemoteCachesOnStartup switch allows to retrieve and initialize existing caches by loading keys form redis server.

Further on RedisCacheManager can be set in transaction aware mode so that values are only put into cache after successful commit of surrounding transaction.

Original pull request: #33
This commit is contained in:
Christoph Strobl
2014-02-07 10:01:11 +01:00
committed by Thomas Darimont
parent 11f94b0810
commit 82578066e8
8 changed files with 669 additions and 29 deletions

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2014 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
*
* http://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.hamcrest.core.Is.*;
import static org.hamcrest.core.IsNull.*;
import static org.hamcrest.core.IsSame.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.junit.internal.matchers.IsCollectionContaining;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.cache.Cache;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
/**
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class RedisCacheManagerUnitTests {
private @Mock RedisConnection redisConnectionMock;
private @Mock RedisConnectionFactory redisConnectionFactoryMock;
@SuppressWarnings("rawtypes")//
private RedisTemplate redisTemplate;
private RedisCacheManager cacheManager;
@SuppressWarnings("rawtypes")
@Before
public void setUp() {
when(redisConnectionFactoryMock.getConnection()).thenReturn(redisConnectionMock);
redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactoryMock);
redisTemplate.afterPropertiesSet();
cacheManager = new RedisCacheManager(redisTemplate);
}
/**
* @see DATAREDIS-246
*/
@Test
public void testGetCacheReturnsNewCacheWhenRequestedCacheIsNotAvailable() {
Cache cache = cacheManager.getCache("not-available");
assertThat(cache, notNullValue());
}
/**
* @see DATAREDIS-246
*/
@Test
public void testGetCacheReturnsExistingCacheWhenRequested() {
Cache cache = cacheManager.getCache("cache");
assertThat(cacheManager.getCache("cache"), sameInstance(cache));
}
/**
* @see DATAREDIS-246
*/
@Test
public void testCacheInitSouldNotRequestRemoteKeysByDefault() {
cacheManager.afterPropertiesSet();
Mockito.verifyZeroInteractions(redisConnectionMock);
}
/**
* @see DATAREDIS-246
*/
@Test
public void testCacheInitShouldFetchAllCacheKeysWhenLoadingRemoteCachesOnStartupIsEnabled() {
cacheManager.setLoadRemoteCachesOnStartup(true);
cacheManager.afterPropertiesSet();
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
verify(redisConnectionMock, times(1)).keys(captor.capture());
assertThat(redisTemplate.getKeySerializer().deserialize(captor.getValue()).toString(), is("*~keys"));
}
/**
* @see DATAREDIS-246
*/
@SuppressWarnings("unchecked")
@Test
public void testCacheInitShouldInitializeRemoteCachesCorrectlyWhenLoadingRemoteCachesOnStartupIsEnabled() {
Set<byte[]> keys = new HashSet<byte[]>(Arrays.asList(redisTemplate.getKeySerializer()
.serialize("remote-cache~keys")));
when(redisConnectionMock.keys(any(byte[].class))).thenReturn(keys);
cacheManager.setLoadRemoteCachesOnStartup(true);
cacheManager.afterPropertiesSet();
assertThat(cacheManager.getCacheNames(), IsCollectionContaining.hasItem("remote-cache"));
}
/**
* @see DATAREDIS-246
*/
@Test
public void testCacheInitShouldNotInitialzeCachesWhenLoadingRemoteCachesOnStartupIsEnabledAndNoCachesAvailableOnRemoteServer() {
when(redisConnectionMock.keys(any(byte[].class))).thenReturn(Collections.<byte[]> emptySet());
cacheManager.setLoadRemoteCachesOnStartup(true);
cacheManager.afterPropertiesSet();
assertThat(cacheManager.getCacheNames().isEmpty(), is(true));
}
/**
* see DATAREDIS-246
*/
@Test
public void testCacheManagerShouldNotDynamicallyCreateCachesWhenInStaticMode() {
cacheManager.setCacheNames(Arrays.asList("spring", "data"));
assertThat(cacheManager.getCache("redis"), nullValue());
}
/**
* see DATAREDIS-246
*/
@Test
public void testCacheManagerShouldRetrunRegisteredCacheWhenInStaticMode() {
cacheManager.setCacheNames(Arrays.asList("spring", "data"));
assertThat(cacheManager.getCache("spring"), notNullValue());
}
/**
* see DATAREDIS-246
*/
@Test
public void testPuttingCacheManagerIntoStaticModeShouldNotRemoveAlreadyRegisteredCaches() {
cacheManager.getCache("redis");
cacheManager.setCacheNames(Arrays.asList("spring", "data"));
assertThat(cacheManager.getCache("redis"), notNullValue());
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2014 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
*
* http://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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.transaction.annotation.Transactional;
/**
* The idea would have been to provide {@link Configuration} here and just set {@link Rollback} accordingly in extending
* classes. Unfortunately this does not work when running build with gradle under java6. It was also not possible to add
* {@link Configuration} here and import it using {@link ContextConfiguration#classes()}. <br />
* <br />
* Therefore {@link ContextConfiguration} had to be duplicated in
* {@link TransactionalRedisCacheManagerWithCommitUnitTests} and
* {@link TransactionalRedisCacheManagerWithRollbackUnitTests}.
*
* @author Christoph Strobl
*/
public abstract class TransactionalRedisCacheManagerTestBase {
static class FooService {
private @Autowired BarRepository repo;
@Transactional
public String foo() {
return "foo" + repo.bar();
}
}
static class BarRepository {
@Cacheable("bar")
public String bar() {
return "bar";
}
}
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright 2014 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
*
* http://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.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.hamcrest.core.IsEqual;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.TransactionalRedisCacheManagerTestBase.BarRepository;
import org.springframework.data.redis.cache.TransactionalRedisCacheManagerTestBase.FooService;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.AfterTransaction;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Christoph Strobl
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@Transactional
@TransactionConfiguration(transactionManager = "transactionManager")
public class TransactionalRedisCacheManagerWithCommitUnitTests {
@SuppressWarnings("rawtypes")//
protected @Autowired RedisTemplate redisTemplate;
protected @Autowired FooService transactionalService;
@Configuration
@EnableCaching
public static class Config {
@Bean
public PlatformTransactionManager transactionManager() throws SQLException {
DataSourceTransactionManager txmgr = new DataSourceTransactionManager();
txmgr.setDataSource(dataSource());
txmgr.afterPropertiesSet();
return txmgr;
}
@Bean
public DataSource dataSource() throws SQLException {
DataSource dataSourceMock = mock(DataSource.class);
when(dataSourceMock.getConnection()).thenReturn(mock(Connection.class));
return dataSourceMock;
}
@Bean
public CacheManager cacheManager() {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate());
cacheManager.setTransactionAware(true);
return cacheManager;
}
@SuppressWarnings({ "rawtypes" })
@Bean
public RedisTemplate redisTemplate() {
RedisConnection connectionMock = mock(RedisConnection.class);
RedisConnectionFactory factoryMock = mock(RedisConnectionFactory.class);
when(factoryMock.getConnection()).thenReturn(connectionMock);
RedisTemplate template = new RedisTemplate();
template.setConnectionFactory(factoryMock);
return template;
}
@Bean
public FooService fooService() {
return new FooService();
}
@Bean
public BarRepository barRepository() {
return new BarRepository();
}
}
@AfterTransaction
public void assertThatValuesHaveBeenAddedToRedis() {
ArgumentCaptor<byte[]> keyCaptor = ArgumentCaptor.forClass(byte[].class);
ArgumentCaptor<byte[]> valueCaptor = ArgumentCaptor.forClass(byte[].class);
verify(redisTemplate.getConnectionFactory().getConnection(), times(1)).zAdd(keyCaptor.capture(), eq(0D),
valueCaptor.capture());
Assert.assertThat(new StringRedisSerializer().deserialize(keyCaptor.getValue()).toString(),
IsEqual.equalTo("bar~keys"));
}
/**
* @see DATAREDIS-246
*/
@Rollback(false)
@Test
public void testValuesAddedToCacheWhenTransactionIsCommited() {
transactionalService.foo();
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2014 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
*
* http://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.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.TransactionalRedisCacheManagerTestBase.BarRepository;
import org.springframework.data.redis.cache.TransactionalRedisCacheManagerTestBase.FooService;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.AfterTransaction;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Christoph Strobl
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@Transactional
@TransactionConfiguration(transactionManager = "transactionManager")
public class TransactionalRedisCacheManagerWithRollbackUnitTests {
@SuppressWarnings("rawtypes")//
protected @Autowired RedisTemplate redisTemplate;
protected @Autowired FooService transactionalService;
@Configuration
@EnableCaching
public static class Config {
@Bean
public PlatformTransactionManager transactionManager() throws SQLException {
DataSourceTransactionManager txmgr = new DataSourceTransactionManager();
txmgr.setDataSource(dataSource());
txmgr.afterPropertiesSet();
return txmgr;
}
@Bean
public DataSource dataSource() throws SQLException {
DataSource dataSourceMock = mock(DataSource.class);
when(dataSourceMock.getConnection()).thenReturn(mock(Connection.class));
return dataSourceMock;
}
@Bean
public CacheManager cacheManager() {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate());
cacheManager.setTransactionAware(true);
return cacheManager;
}
@SuppressWarnings({ "rawtypes" })
@Bean
public RedisTemplate redisTemplate() {
RedisConnection connectionMock = mock(RedisConnection.class);
RedisConnectionFactory factoryMock = mock(RedisConnectionFactory.class);
when(factoryMock.getConnection()).thenReturn(connectionMock);
RedisTemplate template = new RedisTemplate();
template.setConnectionFactory(factoryMock);
return template;
}
@Bean
public FooService fooService() {
return new FooService();
}
@Bean
public BarRepository barRepository() {
return new BarRepository();
}
}
@AfterTransaction
public void assertThatValuesNeverAddedToRedis() {
verify(redisTemplate.getConnectionFactory().getConnection(), times(0)).zAdd(any(byte[].class), eq(0D),
any(byte[].class));
}
/**
* @see DATAREDIS-246
*/
@Rollback(true)
@Test
public void tesValuesNotAddedToCacheWhenTransactionIsRolledBack() {
transactionalService.foo();
}
}