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/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 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();