From 8a640a29b6ac405e4e1ef28c8fc39ecec4a24537 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 5 Oct 2017 14:27:09 +0200 Subject: [PATCH] DATAREDIS-694 - Add support for TOUCH. We now support TOUCH for both Lettuce and Jedis in imperative and reactive (Lettuce only) KeyCommands. Original pull request: #284. --- .../DefaultStringRedisConnection.java | 19 +++++++++++ .../connection/DefaultedRedisConnection.java | 7 ++++ .../redis/connection/ReactiveKeyCommands.java | 23 +++++++++++++ .../redis/connection/RedisKeyCommands.java | 11 ++++++ .../connection/StringRedisConnection.java | 11 ++++++ .../jedis/JedisClusterConnection.java | 34 +++++++++++++++++-- .../jedis/JedisClusterKeyCommands.java | 15 ++++++++ .../connection/jedis/JedisKeyCommands.java | 13 +++++++ .../lettuce/LettuceKeyCommands.java | 24 +++++++++++++ .../lettuce/LettuceReactiveKeyCommands.java | 17 ++++++++++ .../AbstractConnectionIntegrationTests.java | 19 +++++++++++ .../jedis/JedisClusterConnectionTests.java | 28 +++++++++++++-- .../LettuceReactiveKeyCommandsTests.java | 19 +++++++++++ 13 files changed, 236 insertions(+), 4 deletions(-) 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 930a4318f..3790f3202 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -1214,6 +1214,15 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return convertAndReturn(delegate.type(key), identityConverter); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#touch(byte[][]) + */ + @Override + public Long touch(byte[]... keys) { + return convertAndReturn(delegate.touch(keys), identityConverter); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisTxCommands#unwatch() @@ -2505,6 +2514,16 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return type(serialize(key)); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#touch(java.lang.String[]) + */ + @Nullable + @Override + public Long touch(String... keys) { + return touch(serializeMulti(keys)); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.StringRedisConnection#zAdd(java.lang.String, double, java.lang.String) diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java index 3fa2358ee..4e150bbe0 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -75,6 +75,13 @@ public interface DefaultedRedisConnection extends RedisConnection { return keyCommands().type(pattern); } + /** @deprecated in favor of {@link RedisConnection#keyCommands()}. */ + @Override + @Deprecated + default Long touch(byte[]... keys) { + return keyCommands().touch(keys); + } + /** @deprecated in favor of {@link RedisConnection#keyCommands()}. */ @Override @Deprecated diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java index 306c15de8..24852a275 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java @@ -21,6 +21,7 @@ import reactor.core.publisher.Mono; import java.nio.ByteBuffer; import java.time.Duration; import java.time.Instant; +import java.util.Collection; import java.util.List; import org.reactivestreams.Publisher; @@ -87,6 +88,28 @@ public interface ReactiveKeyCommands { */ Flux> type(Publisher keys); + /** + * Alter the last access time of given {@code key(s)}. + * + * @param keys must not be {@literal null}. + * @return {@link Mono} emitting the number of keys touched. + * @see Redis Documentation: TOUCH + * @since 2.1 + */ + default Mono touch(Collection keys) { + return touch(Mono.just(keys)).next().map(NumericResponse::getOutput); + } + + /** + * Alter the last access time of given {@code key(s)}. + * + * @param keys must not be {@literal null}. + * @return + * @see Redis Documentation: TOUCH + * @since 2.1 + */ + Flux, Long>> touch(Publisher> keys); + /** * Find all keys matching the given {@literal pattern}. * diff --git a/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java index aad3ceb4d..557e1edc8 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java @@ -80,6 +80,17 @@ public interface RedisKeyCommands { @Nullable DataType type(byte[] key); + /** + * Alter the last access time of given {@code key(s)}. + * + * @param keys must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: TOUCH + * @since 2.1 + */ + @Nullable + Long touch(byte[]... keys); + /** * Find all keys matching the given {@code pattern}. * 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 d0002ea22..eb88c5b8a 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -123,6 +123,17 @@ public interface StringRedisConnection extends RedisConnection { */ DataType type(String key); + /** + * Alter the last access time of given {@code key(s)}. + * + * @param keys must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: TOUCH + * @since 2.1 + */ + @Nullable + Long touch(String... keys); + /** * Find all keys matching the given {@code pattern}. * diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java index 40ac08398..7cf4013e0 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java @@ -176,6 +176,37 @@ public class JedisClusterConnection implements DefaultedRedisClusterConnection { return commandArgs; } + /** + * Execute the given command for the {@code key} provided potentially appending args.
+ * This method, other than {@link #execute(String, byte[]...)}, dispatches the command to the {@code key} serving + * master node. + * + *
+	 * 
+	 * // SET foo bar EX 10 NX
+	 * execute("SET", "foo".getBytes(), asBinaryList("bar", "EX", 10, "NX")
+	 * 
+	 * 
+ * + * @param command must not be {@literal null}. + * @param keys must not be {@literal null}. + * @param args must not be {@literal null}. + * @return command result as delivered by the underlying Redis driver. Can be {@literal null}. + * @since 2.1 + */ + @Nullable + public List execute(String command, Collection keys, Collection args) { + + Assert.notNull(command, "Command must not be null!"); + Assert.notNull(keys, "Key must not be null!"); + Assert.notNull(args, "Args must not be null!"); + + return clusterCommandExecutor.executeMultiKeyCommand((JedisMultiKeyClusterCommandCallback) (client, key) -> { + return JedisClientUtils.execute(command, new byte[][] { key }, args.toArray(new byte[args.size()][]), + () -> client); + }, keys).resultsAsList(); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisConnection#geoCommands() @@ -829,8 +860,7 @@ public class JedisClusterConnection implements DefaultedRedisClusterConnection { PropertyAccessor accessor = new DirectFieldAccessFallbackBeanWrapper(cluster); this.connectionHandler = accessor.isReadableProperty("connectionHandler") - ? (JedisClusterConnectionHandler) accessor.getPropertyValue("connectionHandler") - : null; + ? (JedisClusterConnectionHandler) accessor.getPropertyValue("connectionHandler") : null; } else { this.connectionHandler = null; } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java index 47c36790a..d6db0ea6b 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java @@ -20,6 +20,7 @@ import redis.clients.jedis.BinaryJedis; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -97,6 +98,20 @@ class JedisClusterKeyCommands implements RedisKeyCommands { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#touch(byte[][]) + */ + @Nullable + @Override + public Long touch(byte[]... keys) { + + Assert.notNull(keys, "Keys must not be null!"); + + return connection. execute("TOUCH", Arrays.asList(keys), Collections.emptyList()).stream() + .mapToLong(val -> val).sum(); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisKeyCommands#keys(byte[]) diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisKeyCommands.java index 98b190dd3..d03bf16d1 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisKeyCommands.java @@ -146,6 +146,19 @@ class JedisKeyCommands implements RedisKeyCommands { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#touch(byte[][]) + */ + @Nullable + @Override + public Long touch(byte[]... keys) { + + Assert.notNull(keys, "Keys must not be null!"); + + return Long.class.cast(connection.execute("TOUCH", keys)); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisKeyCommands#keys(byte[]) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java index f725dcd1f..2b840add5 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java @@ -150,6 +150,30 @@ class LettuceKeyCommands implements RedisKeyCommands { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#touch(byte[][]) + */ + @Override + public Long touch(byte[]... keys) { + + Assert.notNull(keys, "Keys must not be null!"); + + try { + if (isPipelined()) { + pipeline(connection.newLettuceResult(getAsyncConnection().touch(keys))); + return null; + } + if (isQueueing()) { + transaction(connection.newLettuceTxResult(getConnection().touch(keys))); + return null; + } + return getConnection().touch(keys); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisKeyCommands#keys(byte[]) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java index c38239b5e..3696d5c87 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java @@ -20,6 +20,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.nio.ByteBuffer; +import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @@ -86,6 +87,22 @@ class LettuceReactiveKeyCommands implements ReactiveKeyCommands { })); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#touch(org.reactivestreams.Publisher) + */ + @Override + public Flux, Long>> touch(Publisher> keysCollection) { + + return connection.execute(cmd -> Flux.from(keysCollection).concatMap((keys) -> { + + Assert.notEmpty(keys, "Keys must not be null!"); + + return cmd.touch(keys.toArray(new ByteBuffer[keys.size()])) + .map((value) -> new NumericResponse<>(keys, value)); + })); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#keys(org.reactivestreams.Publisher) 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 4c283de44..44746b6aa 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -2771,6 +2771,25 @@ public abstract class AbstractConnectionIntegrationTests { actual.add(connection.hStrLen("hash-no-exist", "key-2")); + verifyResults( + Arrays.asList(new Object[] { 0L })); + } + + @Test // DATAREDIS-694 + public void touchReturnsNrOfKeysTouched() { + + connection.set("touch.this", "Can't touch this! - oh-oh oh oh oh-oh-oh"); + + actual.add(connection.touch("touch.this", "touch.that")); + + verifyResults(Arrays.asList(new Object[] { 1L })); + } + + @Test // DATAREDIS-694 + public void touchReturnsZeroIfNoKeysTouched() { + + actual.add(connection.touch("touch.this", "touch.that")); + verifyResults(Arrays.asList(new Object[] { 0L })); } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java index 39ab89e72..717872860 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java @@ -28,7 +28,16 @@ import redis.clients.jedis.JedisPool; import java.io.IOException; import java.nio.charset.Charset; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -375,7 +384,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests { @Test(expected = IllegalArgumentException.class) // DATAREDIS-689 public void executeWithNoKeyAndArgsThrowsException() { - clusterConnection.execute("KEYS", null, Collections.singletonList("*".getBytes())); + clusterConnection.execute("KEYS", (byte[]) null, Collections.singletonList("*".getBytes())); } @Test // DATAREDIS-529 @@ -2195,4 +2204,19 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests { assertThat(nativeConnection.zrange(SAME_SLOT_KEY_3_BYTES, 0, -1), hasItems(VALUE_1_BYTES, VALUE_2_BYTES, VALUE_3_BYTES)); } + + @Test // DATAREDIS-694 + public void touchReturnsNrOfKeysTouched() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_1); + + assertThat(clusterConnection.keyCommands().touch(KEY_1_BYTES, KEY_2_BYTES, KEY_3_BYTES), is(2L)); + } + + @Test // DATAREDIS-694 + public void touchReturnsZeroIfNoKeysTouched() { + assertThat(clusterConnection.keyCommands().touch(KEY_1_BYTES), is(0L)); + } + } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsTests.java index 831b44ce6..f2bd47131 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsTests.java @@ -287,4 +287,23 @@ public class LettuceReactiveKeyCommandsTests extends LettuceReactiveCommandsTest .verify(); assertThat(nativeCommands.exists(KEY_1), is(0L)); } + + @Test // DATAREDIS-694 + public void touchReturnsNrOfKeysTouched() { + + nativeCommands.set(KEY_1, VALUE_1); + nativeCommands.set(KEY_2, VALUE_2); + + StepVerifier.create(connection.keyCommands().touch(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER, KEY_3_BBUFFER))) + .expectNext(2L) // + .verifyComplete(); + } + + @Test // DATAREDIS-694 + public void touchReturnsZeroIfNoKeysTouched() { + + StepVerifier.create(connection.keyCommands().touch(Arrays.asList(KEY_1_BBUFFER))) // + .expectNext(0L) // + .verifyComplete(); + } }