From 43e741849875d202a630d8bdf1b46f5d8d7c8431 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 13 Mar 2014 12:30:52 +0100 Subject: [PATCH 1/2] DATAREDIS-271 - Add support for 'pSetEx'. Prepare issue branch. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f09fb8eb53..6a57455f5f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ jredisVersion=06052013 jedisVersion=2.4.1 springVersion=3.2.8.RELEASE log4jVersion=1.2.17 -version=1.3.0.BUILD-SNAPSHOT +version=1.3.0.DATAREDIS-271-SNAPSHOT srpVersion=0.7 jacksonVersion=1.8.8 fasterXmlJacksonVersion=2.2.0 From 498a19c1e41e9ed7c47831fe5780b24328c8a55a Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 13 Mar 2014 15:21:45 +0100 Subject: [PATCH 2/2] DATAREDIS-271 - Add support for 'pSetEx'. Support for 'pSetEx' has been added for 'jedis' & 'srp'. For 'lettuce' the command is emulated using 'eval'. There is no support for 'pSetEx' when using 'jredis'. 'DefaultValueOperations.set' used with 'TimeUnit.MILLISECONDS' will fallback to 'setEx' in case 'pSetEx' is not supported by the driver in use. Original pull request: #46. --- .../appendix/appendix-command-reference.xml | 2 +- .../DefaultStringRedisConnection.java | 18 ++++++++ .../redis/connection/RedisStringCommands.java | 11 +++++ .../connection/StringRedisConnection.java | 14 +++++- .../connection/jedis/JedisConnection.java | 30 +++++++++++++ .../connection/jredis/JredisConnection.java | 9 ++++ .../connection/lettuce/LettuceConnection.java | 44 +++++++++++++++++++ .../redis/connection/srp/SrpConnection.java | 23 ++++++++++ .../redis/core/DefaultValueOperations.java | 29 ++++++++++-- .../data/redis/core/ValueOperations.java | 12 ++++- .../AbstractConnectionIntegrationTests.java | 15 +++++++ .../DefaultStringRedisConnectionTests.java | 13 +++++- .../JRedisConnectionIntegrationTests.java | 8 ++++ .../LettuceConnectionIntegrationTests.java | 1 + ...uceConnectionPipelineIntegrationTests.java | 1 + ...eConnectionPipelineTxIntegrationTests.java | 1 + ...ConnectionTransactionIntegrationTests.java | 1 + .../core/DefaultValueOperationsTests.java | 23 ++++++++-- 18 files changed, 244 insertions(+), 11 deletions(-) diff --git a/docs/src/reference/docbook/appendix/appendix-command-reference.xml b/docs/src/reference/docbook/appendix/appendix-command-reference.xml index bef5d3879e..a8c5aeb668 100644 --- a/docs/src/reference/docbook/appendix/appendix-command-reference.xml +++ b/docs/src/reference/docbook/appendix/appendix-command-reference.xml @@ -94,7 +94,7 @@ PEXIPREX PEXPIREATX PINGX - PSETEX- + PSETEXX PSUBSCRIBEX PTTLX PUBLISHX diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java index fdb338ce3d..9d2805c662 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -735,6 +735,15 @@ public void setEx(byte[] key, long seconds, byte[] value) { delegate.setEx(key, seconds, value); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#pSetEx(byte[], long, byte[]) + */ + @Override + public void pSetEx(byte[] key, long milliseconds, byte[] value) { + delegate.pSetEx(key, milliseconds, value); + } + public Boolean setNX(byte[] key, byte[] value) { Boolean result = delegate.setNX(key, value); if (isFutureConversion()) { @@ -1695,6 +1704,15 @@ public void setEx(String key, long seconds, String value) { delegate.setEx(serialize(key), seconds, serialize(value)); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#pSetEx(java.lang.String, long, java.lang.String) + */ + @Override + public void pSetEx(String key, long seconds, String value) { + pSetEx(serialize(key), seconds, serialize(value)); + } + public Boolean setNX(String key, String value) { Boolean result = delegate.setNX(serialize(key), serialize(value)); if (isFutureConversion()) { diff --git a/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java index afc951cde6..f362be7511 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java @@ -87,6 +87,17 @@ public enum BitOperation { */ void setEx(byte[] key, long seconds, byte[] value); + /** + * Set the {@code value} and expiration in {@code milliseconds} for {@code key}. + * + * @see http://redis.io/commands/psetex + * @param key + * @param milliseconds + * @param value + * @since 1.3 + */ + void pSetEx(byte[] key, long milliseconds, byte[] value); + /** * Set multiple keys to multiple values using key-value pairs provided in {@code tuple}. * diff --git a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index ed0a62deb2..20657682e1 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2013 the original author or authors. + * Copyright 2011-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. @@ -29,6 +29,7 @@ * Uses a {@link RedisSerializer} underneath to perform the conversion. * * @author Costin Leau + * @author Christoph Strobl * @see RedisCallback * @see RedisSerializer * @see StringRedisTemplate @@ -93,6 +94,17 @@ public interface StringTuple extends Tuple { void setEx(String key, long seconds, String value); + /** + * Set the {@code value} and expiration in {@code milliseconds} for {@code key}. + * + * @see http://redis.io/commands/psetex + * @param key + * @param seconds + * @param value + * @since 1.3 + */ + void pSetEx(String key, long milliseconds, String value); + void mSetString(Map tuple); Boolean mSetNXString(Map tuple); diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java index e6ad85f06c..67460b7541 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java @@ -331,6 +331,10 @@ private List convertPipelineResults() { return results; } + private void doPipelined(Response response) { + pipeline(new JedisStatusResult(response)); + } + private void pipeline(FutureResult> result) { if (isQueueing()) { transaction(result); @@ -339,6 +343,10 @@ private void pipeline(FutureResult> result) { } } + private void doQueued(Response response) { + transaction(new JedisStatusResult(response)); + } + private void transaction(FutureResult> result) { txResults.add(result); } @@ -1141,6 +1149,28 @@ public void setEx(byte[] key, long time, byte[] value) { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#pSetEx(byte[], long, byte[]) + */ + @Override + public void pSetEx(byte[] key, long milliseconds, byte[] value) { + + try { + if (isPipelined()) { + doPipelined(pipeline.psetex(key, (int) milliseconds, value)); + return; + } + if (isQueueing()) { + doQueued(transaction.psetex(key, (int) milliseconds, value)); + return; + } + jedis.psetex(key, (int) milliseconds, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + public Boolean setNX(byte[] key, byte[] value) { try { if (isPipelined()) { diff --git a/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnection.java b/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnection.java index 85c4e9a42c..6ecab2ee40 100644 --- a/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnection.java @@ -495,6 +495,15 @@ public void setEx(byte[] key, long seconds, byte[] value) { throw new UnsupportedOperationException(); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#pSetEx(byte[], long, byte[]) + */ + @Override + public void pSetEx(byte[] key, long milliseconds, byte[] value) { + throw new UnsupportedOperationException(); + } + public Boolean setNX(byte[] key, byte[] value) { try { return jredis.setnx(key, value); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java index 98044a813c..8cdbadc1fe 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java @@ -56,6 +56,7 @@ import com.lambdaworks.redis.RedisAsyncConnection; import com.lambdaworks.redis.RedisClient; import com.lambdaworks.redis.RedisException; +import com.lambdaworks.redis.ScriptOutputType; import com.lambdaworks.redis.SortArgs; import com.lambdaworks.redis.ZStoreArgs; import com.lambdaworks.redis.codec.RedisCodec; @@ -1197,6 +1198,49 @@ public void setEx(byte[] key, long time, byte[] value) { } } + /** + * {@code pSetEx} is not directly supported and therefore emulated via {@literal lua script}. + * + * @since 1.3 + * @see org.springframework.data.redis.connection.RedisStringCommands#pSetEx(byte[], long, byte[]) + */ + @Override + public void pSetEx(byte[] key, long milliseconds, byte[] value) { + + byte[] script = createRedisScriptForPSetEx(key, milliseconds, value); + byte[][] emptyArgs = new byte[0][0]; + + try { + if (isPipelined()) { + pipeline(new LettuceStatusResult(getAsyncConnection().eval(script, ScriptOutputType.STATUS, emptyArgs, + emptyArgs))); + return; + } + if (isQueueing()) { + transaction(new LettuceTxStatusResult(getConnection().eval(script, ScriptOutputType.STATUS, emptyArgs, + emptyArgs))); + return; + } + this.eval(script, ReturnType.STATUS, 0); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + + private byte[] createRedisScriptForPSetEx(byte[] key, long milliseconds, byte[] value) { + + StringBuilder sb = new StringBuilder("return redis.call('PSETEX'"); + sb.append(",'"); + sb.append(new String(key)); + sb.append("',"); + sb.append(milliseconds); + sb.append(",'"); + sb.append(new String(value)); + sb.append("')"); + + return sb.toString().getBytes(); + } + public Boolean setNX(byte[] key, byte[] value) { try { if (isPipelined()) { diff --git a/src/main/java/org/springframework/data/redis/connection/srp/SrpConnection.java b/src/main/java/org/springframework/data/redis/connection/srp/SrpConnection.java index 2aecab82d9..ba7c5a6c36 100644 --- a/src/main/java/org/springframework/data/redis/connection/srp/SrpConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/srp/SrpConnection.java @@ -900,6 +900,24 @@ public void setEx(byte[] key, long time, byte[] value) { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#pSetEx(byte[], long, byte[]) + */ + @Override + public void pSetEx(byte[] key, long milliseconds, byte[] value) { + + try { + if (isPipelined()) { + doPipelined(pipeline.psetex(key, milliseconds, value)); + return; + } + client.psetex(key, milliseconds, value); + } catch (Exception ex) { + throw convertSrpAccessException(ex); + } + } + public Boolean setNX(byte[] key, byte[] value) { try { if (isPipelined()) { @@ -2113,6 +2131,11 @@ private void checkSubscription() { } } + @SuppressWarnings("rawtypes") + private void doPipelined(ListenableFuture listenableFuture) { + pipeline(new SrpStatusResult(listenableFuture)); + } + // processing method that adds a listener to the future in order to track down the results and close the pipeline private void pipeline(FutureResult future) { if (isQueueing()) { diff --git a/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java index 1e148b426d..bdfaf8d2c3 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2013 the original author or authors. + * Copyright 2011-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. @@ -30,6 +30,7 @@ * * @author Costin Leau * @author Jennifer Hickey + * @author Christoph Strobl */ class DefaultValueOperations extends AbstractOperations implements ValueOperations { @@ -173,17 +174,37 @@ protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { }, true); } - public void set(K key, V value, long timeout, TimeUnit unit) { + public void set(K key, V value, final long timeout, final TimeUnit unit) { final byte[] rawKey = rawKey(key); final byte[] rawValue = rawValue(value); - final long rawTimeout = TimeoutUtils.toSeconds(timeout, unit); execute(new RedisCallback() { public Object doInRedis(RedisConnection connection) throws DataAccessException { - connection.setEx(rawKey, rawTimeout, rawValue); + + potentiallyUsePsetEx(connection); return null; } + + public void potentiallyUsePsetEx(RedisConnection connection) { + + if (!TimeUnit.MILLISECONDS.equals(unit) || !failsafeInvokePsetEx(connection)) { + connection.setEx(rawKey, TimeoutUtils.toSeconds(timeout, unit), rawValue); + } + } + + private boolean failsafeInvokePsetEx(RedisConnection connection) { + + boolean failed = false; + try { + connection.pSetEx(rawKey, timeout, rawValue); + } catch (UnsupportedOperationException e) { + // in case the connection does not support pSetEx return false to allow fallback to other operation. + failed = true; + } + return !failed; + } + }, true); } diff --git a/src/main/java/org/springframework/data/redis/core/ValueOperations.java b/src/main/java/org/springframework/data/redis/core/ValueOperations.java index 2cbe1a5dca..41570ecef0 100644 --- a/src/main/java/org/springframework/data/redis/core/ValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ValueOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2013 the original author or authors. + * Copyright 2011-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. @@ -24,11 +24,21 @@ * Redis operations for simple (or in Redis terminology 'string') values. * * @author Costin Leau + * @author Christoph Strobl */ public interface ValueOperations { void set(K key, V value); + /** + * Set {@code key} to hold the string {@code value} until {@code timeout}. + * + * @param key + * @param value + * @param timeout + * @param units + * @see http://redis.io/commands/set + */ void set(K key, V value, long timeout, TimeUnit unit); Boolean setIfAbsent(K key, V value); diff --git a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java index 4a0943f3b2..6fd8135e53 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -66,6 +66,7 @@ * @author Costin Leau * @author Jennifer Hickey * @author Christoph Strobl + * @author Thomas Darimont */ @ProfileValueSourceConfiguration(RedisTestProfileValueSource.class) public abstract class AbstractConnectionIntegrationTests { @@ -359,6 +360,20 @@ public void testSetEx() throws Exception { assertTrue(waitFor(new KeyExpired("expy"), 2500l)); } + /** + * @see DATAREDIS-271 + */ + @Test + @IfProfileValue(name = "runLongTests", value = "true") + public void testPsetEx() throws Exception { + + connection.pSetEx("expy", 500L, "yep"); + actual.add(connection.get("expy")); + + verifyResults(Arrays.asList(new Object[] { "yep" })); + assertTrue(waitFor(new KeyExpired("expy"), 2500L)); + } + @Test @IfProfileValue(name = "runLongTests", value = "true") public void testBRPopTimeout() throws Exception { diff --git a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java index ea1a3d3e59..816a0594c6 100644 --- a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-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. @@ -45,6 +45,7 @@ * Unit test of {@link DefaultStringRedisConnection} * * @author Jennifer Hickey + * @auhtor Christoph Strobl */ public class DefaultStringRedisConnectionTests { @@ -907,6 +908,16 @@ public void testSetNX() { verifyResults(Arrays.asList(new Object[] { true })); } + /** + * @see DATAREDIS-271 + */ + @Test + public void testPSetExShouldDelegateCallToNativeConnection() { + + connection.pSetEx(fooBytes, 10L, barBytes); + verify(nativeConnection, times(1)).pSetEx(eq(fooBytes), eq(10L), eq(barBytes)); + } + @Test public void testSInterBytes() { doReturn(bytesSet).when(nativeConnection).sInter(fooBytes, barBytes); diff --git a/src/test/java/org/springframework/data/redis/connection/jredis/JRedisConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jredis/JRedisConnectionIntegrationTests.java index e7326aa33f..88806fe5dc 100644 --- a/src/test/java/org/springframework/data/redis/connection/jredis/JRedisConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jredis/JRedisConnectionIntegrationTests.java @@ -801,4 +801,12 @@ public void testExecuteShouldConvertArrayReplyCorrectly() { IsCollectionContaining.hasItems("awesome".getBytes(), "cool".getBytes(), "supercalifragilisticexpialidocious".getBytes())); } + + /** + * @see DATAREDIS-271 + */ + @Test(expected = UnsupportedOperationException.class) + public void testPsetEx() throws Exception { + super.testPsetEx(); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java index eeaf4d218e..5ef58a80cf 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java @@ -287,6 +287,7 @@ public void testMove() { @SuppressWarnings("unchecked") @Test public void testExecuteShouldConvertArrayReplyCorrectly() { + connection.set("spring", "awesome"); connection.set("data", "cool"); connection.set("redis", "supercalifragilisticexpialidocious"); diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineIntegrationTests.java index 88e03211f5..58a6943e3c 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineIntegrationTests.java @@ -112,4 +112,5 @@ public void testMove() { factory2.destroy(); } } + } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineTxIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineTxIntegrationTests.java index 294a3b5221..59aa57d2fb 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineTxIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineTxIntegrationTests.java @@ -82,4 +82,5 @@ protected List getResults() { // Return exec results and this test should behave exactly like its superclass return txResults; } + } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionTransactionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionTransactionIntegrationTests.java index 1c48f12aa5..56b460f69c 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionTransactionIntegrationTests.java @@ -75,4 +75,5 @@ public void testMove() { public void testSelect() { super.testSelect(); } + } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsTests.java b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsTests.java index ce25f621a5..d0dc5a5308 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-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. @@ -44,6 +44,7 @@ * Integration test of {@link DefaultValueOperations} * * @author Jennifer Hickey + * @author Christoph Strobl */ @RunWith(Parameterized.class) public class DefaultValueOperationsTests { @@ -173,11 +174,10 @@ public void testGetAndSet() { @Test public void testSetWithExpiration() { - // 1 ms timeout gets upgraded to 1 sec timeout at the moment assumeTrue(RedisTestProfileValueSource.matches("runLongTests", "true")); final K key1 = keyFactory.instance(); V value1 = valueFactory.instance(); - valueOps.set(key1, value1, 1, TimeUnit.MILLISECONDS); + valueOps.set(key1, value1, 1, TimeUnit.SECONDS); waitFor(new TestCondition() { public boolean passes() { return (!redisTemplate.hasKey(key1)); @@ -185,6 +185,23 @@ public boolean passes() { }, 1000); } + /** + * @see DATAREDIS-271 + */ + @Test + public void testSetWithExpirationWithTimeUnitMilliseconds() { + + assumeTrue(RedisTestProfileValueSource.matches("runLongTests", "true")); + final K key1 = keyFactory.instance(); + V value1 = valueFactory.instance(); + valueOps.set(key1, value1, 1, TimeUnit.MILLISECONDS); + waitFor(new TestCondition() { + public boolean passes() { + return (!redisTemplate.hasKey(key1)); + } + }, 500); + } + @Test public void testAppend() { K key1 = keyFactory.instance();