diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 98126a3f5..fc98d75f6 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -9,6 +9,7 @@ This section briefly covers items that are new and noteworthy in the latest rele * <> * Refined `union`/`diff`/`intersect` set-operation methods accepting a single collection of keys. * Upgrade to Jedis 3. +* Add support for scripting commands using Jedis Cluster. [[new-in-2.1.0]] == New in Spring Data Redis 2.1 diff --git a/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutor.java b/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutor.java index 69799bae6..494017718 100644 --- a/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutor.java +++ b/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutor.java @@ -587,7 +587,8 @@ public class ClusterCommandExecutor implements DisposableBean { if (CollectionUtils.isEmpty((Map) nodeResult.getValue())) { return nodeResult.getValue(); } - } else if (CollectionUtils.isEmpty((Collection) nodeResult.getValue())) { + } else if (nodeResult.getValue() instanceof Collection + && CollectionUtils.isEmpty((Collection) nodeResult.getValue())) { return nodeResult.getValue(); } else { return nodeResult.getValue(); 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 c40c018c4..c90bb7d21 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 @@ -69,6 +69,7 @@ import org.springframework.util.Assert; * @author Ninad Divadkar * @author Tao Chen * @author Chen Guanqun + * @author Pavel Khokhlov * @since 1.7 */ public class JedisClusterConnection implements DefaultedRedisClusterConnection { diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterScriptingCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterScriptingCommands.java index 62d361632..09d031bfc 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterScriptingCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterScriptingCommands.java @@ -15,24 +15,28 @@ */ package org.springframework.data.redis.connection.jedis; -import java.util.List; - import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.springframework.dao.DataAccessException; +import redis.clients.jedis.BinaryJedis; +import redis.clients.jedis.JedisCluster; + +import java.util.List; + import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.ClusterCommandExecutor; import org.springframework.data.redis.connection.RedisScriptingCommands; import org.springframework.data.redis.connection.ReturnType; -import redis.clients.jedis.JedisCluster; +import org.springframework.util.Assert; /** * @author Mark Paluch + * @author Pavel Khokhlov * @since 2.0 */ @RequiredArgsConstructor class JedisClusterScriptingCommands implements RedisScriptingCommands { - private final @NonNull JedisClusterConnection clusterConnection; + private final @NonNull JedisClusterConnection connection; /* * (non-Javadoc) @@ -40,7 +44,13 @@ class JedisClusterScriptingCommands implements RedisScriptingCommands { */ @Override public void scriptFlush() { - throw new InvalidDataAccessApiUsageException("ScriptFlush is not supported in cluster environment."); + + try { + connection.getClusterCommandExecutor().executeCommandOnAllNodes( + (JedisClusterConnection.JedisClusterCommandCallback) BinaryJedis::scriptFlush); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } } /* @@ -49,7 +59,13 @@ class JedisClusterScriptingCommands implements RedisScriptingCommands { */ @Override public void scriptKill() { - throw new InvalidDataAccessApiUsageException("ScriptKill is not supported in cluster environment."); + + try { + connection.getClusterCommandExecutor().executeCommandOnAllNodes( + (JedisClusterConnection.JedisClusterCommandCallback) BinaryJedis::scriptKill); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } } /* @@ -58,7 +74,18 @@ class JedisClusterScriptingCommands implements RedisScriptingCommands { */ @Override public String scriptLoad(byte[] script) { - throw new InvalidDataAccessApiUsageException("ScriptLoad is not supported in cluster environment."); + + Assert.notNull(script, "Script must not be null!"); + + try { + ClusterCommandExecutor.MultiNodeResult multiNodeResult = connection.getClusterCommandExecutor() + .executeCommandOnAllNodes( + (JedisClusterConnection.JedisClusterCommandCallback) client -> client.scriptLoad(script)); + + return JedisConverters.toString(multiNodeResult.getFirstNonNullNotEmptyOrDefault(new byte[0])); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } } /* @@ -77,7 +104,9 @@ class JedisClusterScriptingCommands implements RedisScriptingCommands { @Override @SuppressWarnings("unchecked") public T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { - checkConnection(); + + Assert.notNull(script, "Script must not be null!"); + try { return (T) new JedisScriptReturnConverter(returnType) .convert(getCluster().eval(script, JedisConverters.toBytes(numKeys), keysAndArgs)); @@ -102,7 +131,9 @@ class JedisClusterScriptingCommands implements RedisScriptingCommands { @Override @SuppressWarnings("unchecked") public T evalSha(byte[] scriptSha, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { - checkConnection(); + + Assert.notNull(scriptSha, "Script digest must not be null!"); + try { return (T) new JedisScriptReturnConverter(returnType) .convert(getCluster().evalsha(scriptSha, numKeys, keysAndArgs)); @@ -111,32 +142,11 @@ class JedisClusterScriptingCommands implements RedisScriptingCommands { } } - public JedisClusterConnection getClusterConnection() { - return clusterConnection; - } - protected RuntimeException convertJedisAccessException(Exception ex) { - return clusterConnection.convertJedisAccessException(ex); + return connection.convertJedisAccessException(ex); } private JedisCluster getCluster() { - return clusterConnection.getCluster(); - } - - protected void checkConnection() { - if (isQueueing()) { - throw new UnsupportedOperationException(); - } - if (isPipelined()) { - throw new UnsupportedOperationException(); - } - } - - private boolean isPipelined() { - return clusterConnection.isPipelined(); - } - - private boolean isQueueing() { - return clusterConnection.isQueueing(); + return connection.getCluster(); } } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisScriptingCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisScriptingCommands.java index ccb63e9cc..6e3239d9b 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisScriptingCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisScriptingCommands.java @@ -147,9 +147,9 @@ class JedisScriptingCommands implements RedisScriptingCommands { */ @Override @SuppressWarnings("unchecked") - public T evalSha(byte[] scriptSha1, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + public T evalSha(byte[] scriptSha, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { - Assert.notNull(scriptSha1, "Script digest must not be null!"); + Assert.notNull(scriptSha, "Script digest must not be null!"); if (isQueueing() || isPipelined()) { throw new UnsupportedOperationException(); @@ -157,7 +157,7 @@ class JedisScriptingCommands implements RedisScriptingCommands { try { return (T) new JedisScriptReturnConverter(returnType) - .convert(connection.getJedis().evalsha(scriptSha1, numKeys, keysAndArgs)); + .convert(connection.getJedis().evalsha(scriptSha, numKeys, keysAndArgs)); } catch (Exception ex) { throw convertJedisAccessException(ex); } 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 3d0a0eb82..b49905ad0 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 @@ -41,6 +41,7 @@ import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; + import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Range.Bound; @@ -51,7 +52,6 @@ import org.springframework.data.geo.Point; import org.springframework.data.redis.connection.ClusterConnectionTests; import org.springframework.data.redis.connection.ClusterSlotHashUtil; import org.springframework.data.redis.connection.DataType; -import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.DefaultSortParameters; import org.springframework.data.redis.connection.DefaultTuple; import org.springframework.data.redis.connection.RedisClusterNode; @@ -63,19 +63,21 @@ import org.springframework.data.redis.connection.RedisStringCommands.BitOperatio import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.connection.RedisZSetCommands.Range; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; +import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.ValueEncoding.RedisValueEncoding; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.script.DigestUtils; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.test.util.HexStringUtils; import org.springframework.data.redis.test.util.MinimumRedisVersionRule; import org.springframework.data.redis.test.util.RedisClusterRule; import org.springframework.test.annotation.IfProfileValue; -import org.springframework.data.redis.core.script.DigestUtils; /** * @author Christoph Strobl * @author Mark Paluch + * @author Pavel Khokhlov */ public class JedisClusterConnectionTests implements ClusterConnectionTests { @@ -2368,10 +2370,8 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests { @IfProfileValue(name = "redisVersion", value = "3.2+") public void bitFieldGetShouldWorkCorrectly() { - assertThat( - clusterConnection.stringCommands().bitField(JedisConverters.toBytes(KEY_1), - create().get(INT_8).valueAt(offset(0L))), - contains(0L)); + assertThat(clusterConnection.stringCommands().bitField(JedisConverters.toBytes(KEY_1), + create().get(INT_8).valueAt(offset(0L))), contains(0L)); } @Test // DATAREDIS-562 @@ -2392,22 +2392,16 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests { create().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L)), contains(2L)); assertThat(clusterConnection.stringCommands().bitField(JedisConverters.toBytes(KEY_1), create().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L)), contains(3L)); - assertThat( - clusterConnection.stringCommands() - .bitField(JedisConverters.toBytes(KEY_1), - create().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L)) - .get(0), - is(nullValue())); + assertThat(clusterConnection.stringCommands().bitField(JedisConverters.toBytes(KEY_1), + create().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L)).get(0), is(nullValue())); } @Test // DATAREDIS-562 @IfProfileValue(name = "redisVersion", value = "3.2+") public void bitfieldShouldAllowMultipleSubcommands() { - assertThat( - clusterConnection.stringCommands().bitField(JedisConverters.toBytes(KEY_1), - create().incr(signed(5)).valueAt(offset(100L)).by(1L).get(unsigned(4)).valueAt(0L)), - contains(1L, 0L)); + assertThat(clusterConnection.stringCommands().bitField(JedisConverters.toBytes(KEY_1), + create().incr(signed(5)).valueAt(offset(100L)).by(1L).get(unsigned(4)).valueAt(0L)), contains(1L, 0L)); } @Test // DATAREDIS-562 @@ -2415,36 +2409,71 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests { public void bitfieldShouldWorkUsingNonZeroBasedOffset() { assertThat(clusterConnection.stringCommands().bitField(JedisConverters.toBytes(KEY_1), - create().set(INT_8).valueAt(offset(0L).multipliedByTypeLength()) - .to(100L).set(INT_8).valueAt(offset(1L).multipliedByTypeLength()).to(200L)), contains(0L, 0L)); + create().set(INT_8).valueAt(offset(0L).multipliedByTypeLength()).to(100L).set(INT_8) + .valueAt(offset(1L).multipliedByTypeLength()).to(200L)), + contains(0L, 0L)); assertThat( - clusterConnection.stringCommands() - .bitField(JedisConverters.toBytes(KEY_1), create().get(INT_8) - .valueAt(offset(0L).multipliedByTypeLength()) - .get(INT_8).valueAt(offset(1L).multipliedByTypeLength())), contains(100L, -56L)); + clusterConnection.stringCommands().bitField(JedisConverters.toBytes(KEY_1), create().get(INT_8) + .valueAt(offset(0L).multipliedByTypeLength()).get(INT_8).valueAt(offset(1L).multipliedByTypeLength())), + contains(100L, -56L)); } @Test // DATAREDIS-1005 @IfProfileValue(name = "redisVersion", value = "2.6+") - public void evalOK() { - byte[] keyAndArgs = JedisConverters.toBytes("FOO"); + public void evalShouldRunScript() { + byte[] keyAndArgs = JedisConverters.toBytes("FOO"); String luaScript = "return redis.call(\"INCR\", KEYS[1])"; byte[] luaScriptBin = JedisConverters.toBytes(luaScript); + Long result = clusterConnection.scriptingCommands().eval(luaScriptBin, ReturnType.VALUE, 1, keyAndArgs); + assertThat(result, is(1L)); } @Test // DATAREDIS-1005 @IfProfileValue(name = "redisVersion", value = "2.6+") - public void evalShaOK() { - byte[] keyAndArgs = JedisConverters.toBytes("FOO"); + public void scriptLoadShouldLoadScript() { String luaScript = "return redis.call(\"INCR\", KEYS[1])"; String digest = DigestUtils.sha1DigestAsHex(luaScript); - byte[] luaScriptBin = JedisConverters.toBytes(digest); - Long result = clusterConnection.scriptingCommands().evalSha(luaScriptBin, ReturnType.VALUE, 1, keyAndArgs); - assertThat(result, is(1L)); + byte[] luaScriptBin = JedisConverters.toBytes(luaScript); + + String result = clusterConnection.scriptingCommands().scriptLoad(luaScriptBin); + + assertThat(result, is(digest)); } + @Test // DATAREDIS-1005 + @IfProfileValue(name = "redisVersion", value = "2.6+") + public void scriptFlushShouldRemoveScripts() { + + byte[] keyAndArgs = JedisConverters.toBytes("FOO"); + String luaScript = "return redis.call(\"GET\", KEYS[1])"; + byte[] luaScriptBin = JedisConverters.toBytes(luaScript); + + clusterConnection.scriptingCommands().scriptLoad(luaScriptBin); + clusterConnection.scriptingCommands().scriptFlush(); + + try { + clusterConnection.scriptingCommands().evalSha(luaScriptBin, ReturnType.VALUE, 1, keyAndArgs); + fail("expected InvalidDataAccessApiUsageException"); + } catch (InvalidDataAccessApiUsageException e) { + assertThat(e.getMessage(), containsString("NOSCRIPT")); + } + } + + @Test // DATAREDIS-1005 + @IfProfileValue(name = "redisVersion", value = "2.6+") + public void evelShaShouldRunScript() { + + byte[] keyAndArgs = JedisConverters.toBytes("FOO"); + String luaScript = "return redis.call(\"INCR\", KEYS[1])"; + byte[] digest = JedisConverters.toBytes(DigestUtils.sha1DigestAsHex(luaScript)); + + clusterConnection.scriptingCommands().scriptLoad(JedisConverters.toBytes(luaScript)); + + Long result = clusterConnection.scriptingCommands().evalSha(digest, ReturnType.VALUE, 1, keyAndArgs); + assertThat(result, is(1L)); + } }