/* * Copyright 2011-2017 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 edu.umd.cs.mtc.TestFramework.*; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsEqual.*; import static org.hamcrest.core.IsInstanceOf.*; import static org.hamcrest.core.IsNot.*; import static org.hamcrest.core.IsNull.*; import static org.hamcrest.core.IsSame.*; import static org.junit.Assert.*; import static org.junit.Assume.*; import static org.springframework.data.redis.matcher.RedisTestMatchers.*; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.hamcrest.core.IsInstanceOf; import org.junit.AfterClass; import org.junit.AssumptionViolatedException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import org.springframework.cache.Cache; import org.springframework.cache.Cache.ValueRetrievalException; import org.springframework.cache.Cache.ValueWrapper; import org.springframework.data.redis.ConnectionFactoryTracker; import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.StringObjectFactory; import org.springframework.data.redis.core.AbstractOperationsTestParams; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import edu.umd.cs.mtc.MultithreadedTestCase; /** * @author Costin Leau * @author Jennifer Hickey * @author Christoph Strobl * @author Mark Paluch */ @SuppressWarnings("rawtypes") @RunWith(Parameterized.class) public class RedisCacheTest extends AbstractNativeCacheTest<RedisTemplate> { private ObjectFactory<Object> keyFactory; private ObjectFactory<Object> valueFactory; private RedisTemplate template; public RedisCacheTest(RedisTemplate template, ObjectFactory<Object> keyFactory, ObjectFactory<Object> valueFactory, boolean allowCacheNullValues) { super(allowCacheNullValues); this.keyFactory = keyFactory; this.valueFactory = valueFactory; this.template = template; ConnectionFactoryTracker.add(template.getConnectionFactory()); } @Parameters public static Collection<Object[]> testParams() { Collection<Object[]> params = AbstractOperationsTestParams.testParams(); Collection<Object[]> target = new ArrayList<Object[]>(); for (Object[] source : params) { Object[] cacheNullDisabled = Arrays.copyOf(source, source.length + 1); Object[] cacheNullEnabled = Arrays.copyOf(source, source.length + 1); cacheNullDisabled[source.length] = false; cacheNullEnabled[source.length] = true; target.add(cacheNullDisabled); target.add(cacheNullEnabled); } return target; } @SuppressWarnings("unchecked") protected RedisCache createCache(RedisTemplate nativeCache, boolean allowCacheNullValues) { return new RedisCache(CACHE_NAME, CACHE_NAME.concat(":").getBytes(), nativeCache, TimeUnit.MINUTES.toSeconds(10), allowCacheNullValues); } protected RedisTemplate createNativeCache() throws Exception { return template; } @Before public void setUp() throws Exception { if (!(template.getValueSerializer() instanceof JdkSerializationRedisSerializer || template.getValueSerializer() instanceof GenericJackson2JsonRedisSerializer || template.getValueSerializer() == null) && getAllowCacheNullValues()) { throw new AssumptionViolatedException( "Null values can only be cachend with the Jdk or GenericJackson2 serialization"); } ConnectionFactoryTracker.add(template.getConnectionFactory()); super.setUp(); } @AfterClass public static void cleanUp() { ConnectionFactoryTracker.cleanUp(); } protected Object getValue() { return valueFactory.instance(); } protected Object getKey() { return keyFactory.instance(); } @Test public void testConcurrentRead() throws Exception { final Object key1 = getKey(); final Object value1 = getValue(); final Object k1 = getKey(); final Object v1 = getValue(); final Object key2 = getKey(); final Object value2 = getValue(); final Object k2 = getKey(); final Object v2 = getValue(); final AtomicBoolean failed = new AtomicBoolean(true); cache.put(key1, value1); cache.put(key2, value2); Thread th = new Thread(new Runnable() { public void run() { cache.clear(); cache.put(k1, v1); cache.put(k2, v2); failed.set(v1.equals(cache.get(k1))); } }, "concurrent-cache-access"); th.start(); th.join(); assertFalse(failed.get()); final Object key3 = getKey(); final Object key4 = getKey(); final Object value3 = getValue(); final Object value4 = getValue(); cache.put(key3, value3); cache.put(key4, value4); assertNull(cache.get(key1)); assertNull(cache.get(key2)); ValueWrapper valueWrapper = cache.get(k1); assertNotNull(valueWrapper); assertThat(valueWrapper.get(), isEqual(v1)); } @Test public void testCacheName() throws Exception { RedisCacheManager redisCM = new RedisCacheManager(template); redisCM.afterPropertiesSet(); String cacheName = "s2gx11"; Cache cache = redisCM.getCache(cacheName); assertNotNull(cache); assertTrue(redisCM.getCacheNames().contains(cacheName)); } @Test public void testGetWhileClear() throws InterruptedException { final Object key1 = getKey(); final Object value1 = getValue(); int numTries = 10; final AtomicBoolean monitorStateException = new AtomicBoolean(false); final CountDownLatch latch = new CountDownLatch(numTries); Runnable clearCache = new Runnable() { public void run() { cache.clear(); } }; Runnable putCache = new Runnable() { public void run() { try { cache.put(key1, value1); } catch (IllegalMonitorStateException e) { monitorStateException.set(true); } finally { latch.countDown(); } } }; for (int i = 0; i < numTries; i++) { new Thread(clearCache).start(); new Thread(putCache).start(); } latch.await(); assertFalse(monitorStateException.get()); } @Test // DATAREDIS-243 public void testCacheGetShouldReturnCachedInstance() { assumeThat(cache, instanceOf(RedisCache.class)); Object key = getKey(); Object value = getValue(); cache.put(key, value); assertThat(value, isEqual(((RedisCache) cache).get(key, Object.class))); } @Test // DATAREDIS-243 public void testCacheGetShouldRetunInstanceOfCorrectType() { assumeThat(cache, instanceOf(RedisCache.class)); Object key = getKey(); Object value = getValue(); cache.put(key, value); RedisCache redisCache = (RedisCache) cache; assertThat(redisCache.get(key, value.getClass()), IsInstanceOf.<Object>instanceOf(value.getClass())); } @Test(expected = ClassCastException.class) // DATAREDIS-243 public void testCacheGetShouldThrowExceptionOnInvalidType() { assumeThat(cache, instanceOf(RedisCache.class)); Object key = getKey(); Object value = getValue(); cache.put(key, value); RedisCache redisCache = (RedisCache) cache; @SuppressWarnings("unused") Cache retrievedObject = redisCache.get(key, Cache.class); } @Test // DATAREDIS-243 public void testCacheGetShouldReturnNullIfNoCachedValueFound() { assumeThat(cache, instanceOf(RedisCache.class)); Object key = getKey(); Object value = getValue(); cache.put(key, value); RedisCache redisCache = (RedisCache) cache; Object invalidKey = template.getKeySerializer() == null ? "spring-data-redis".getBytes() : "spring-data-redis"; assertThat(redisCache.get(invalidKey, value.getClass()), nullValue()); } @Test // DATAREDIS-344, DATAREDIS-416 public void putIfAbsentShouldSetValueOnlyIfNotPresent() { assumeThat(cache, instanceOf(RedisCache.class)); RedisCache redisCache = (RedisCache) cache; Object key = getKey(); template.delete(key); Object value = getValue(); assertThat(redisCache.putIfAbsent(key, value), nullValue()); ValueWrapper wrapper = redisCache.putIfAbsent(key, value); if (!(value instanceof Number)) { assertThat(wrapper.get(), not(sameInstance(value))); } assertThat(wrapper.get(), equalTo(value)); } @Test(expected = IllegalArgumentException.class) // DATAREDIS-510, DATAREDIS-606 public void cachePutWithNullShouldNotAddStuffToRedis() { assumeThat(getAllowCacheNullValues(), is(false)); Object key = getKey(); cache.put(key, null); } @Test // DATAREDIS-510, DATAREDIS-606 public void cachePutWithNullShouldErrorAndLeaveExistingKeyUntouched() { assumeThat(getAllowCacheNullValues(), is(false)); Object key = getKey(); Object value = getValue(); cache.put(key, value); assertThat(cache.get(key).get(), is(equalTo(value))); try { cache.put(key, null); } catch (IllegalArgumentException e) { // forget this one. } assertThat(cache.get(key).get(), is(equalTo(value))); } @Test // DATAREDIS-443, DATAREDIS-452 public void testCacheGetSynchronized() throws Throwable { assumeThat(cache, instanceOf(RedisCache.class)); assumeThat(valueFactory, instanceOf(StringObjectFactory.class)); runOnce(new CacheGetWithValueLoaderIsThreadSafe((RedisCache) cache)); } @Test // DATAREDIS-553 public void cachePutWithNullShouldAddStuffToRedisWhenCachingNullIsEnabled() { assumeThat(getAllowCacheNullValues(), is(true)); Object key = getKey(); Object value = getValue(); cache.put(key, null); assertThat(cache.get(key, String.class), is(nullValue())); } @Test // DATAREDIS-553 public void testCacheGetSynchronizedNullAllowingNull() { assumeThat(getAllowCacheNullValues(), is(true)); assumeThat(cache, instanceOf(RedisCache.class)); Object key = getKey(); Object value = cache.get(key, new Callable<Object>() { @Override public Object call() throws Exception { return null; } }); assertThat(value, is(nullValue())); assertThat(cache.get(key).get(), is(nullValue())); } @Test(expected = ValueRetrievalException.class) // DATAREDIS-553, DATAREDIS-606 public void testCacheGetSynchronizedNullNotAllowingNull() { assumeThat(getAllowCacheNullValues(), is(false)); assumeThat(cache, instanceOf(RedisCache.class)); assumeThat(template.getValueSerializer(), not(instanceOf(StringRedisSerializer.class))); Object key = getKey(); Object value = cache.get(key, new Callable<Object>() { @Override public Object call() throws Exception { return null; } }); } @Test // DATAREDIS-553 public void testCacheGetSynchronizedNullWithStoredNull() { assumeThat(getAllowCacheNullValues(), is(true)); assumeThat(cache, instanceOf(RedisCache.class)); Object key = getKey(); cache.put(key, null); Object cachedValue = cache.get(key, new Callable<Object>() { @Override public Object call() throws Exception { return null; } }); assertThat(cachedValue, is(nullValue())); } @SuppressWarnings("unused") private static class CacheGetWithValueLoaderIsThreadSafe extends MultithreadedTestCase { RedisCache redisCache; TestCacheLoader<String> cacheLoader; public CacheGetWithValueLoaderIsThreadSafe(RedisCache redisCache) { this.redisCache = redisCache; cacheLoader = new TestCacheLoader<String>("test") { @Override public String call() throws Exception { waitForTick(2); return super.call(); } }; } public void thread1() { assertTick(0); assertThat(redisCache.get("key", cacheLoader), equalTo("test")); } public void thread2() { waitForTick(1); assertThat(redisCache.get("key", new TestCacheLoader<String>("illegal value")), equalTo("test")); assertTick(2); } } private static class TestCacheLoader<T> implements Callable<T> { private final T value; public TestCacheLoader(T value) { this.value = value; } @Override public T call() throws Exception { return value; } } }