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 0b5935ca3..2e279e5c3 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -624,6 +624,30 @@ public class DefaultStringRedisConnection implements StringRedisConnection { delegate.restore(key, ttlInMillis, serializedValue); } + public void scriptFlush() { + delegate.scriptFlush(); + } + + public void scriptKill() { + delegate.scriptKill(); + } + + public String scriptLoad(byte[] script) { + return delegate.scriptLoad(script); + } + + public List scriptExists(String... scriptSha1) { + return delegate.scriptExists(scriptSha1); + } + + public T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + return delegate.eval(script, returnType, numKeys, keysAndArgs); + } + + public T evalSha(String scriptSha1, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + return delegate.evalSha(scriptSha1, returnType, numKeys, keysAndArgs); + } + // // String methods // @@ -1235,4 +1259,26 @@ public class DefaultStringRedisConnection implements StringRedisConnection { public Long pTtl(String key) { return pTtl(serialize(key)); } + + public String scriptLoad(String script) { + return delegate.scriptLoad(serialize(script)); + } + + /** + * NOTE: This method will not deserialize Strings returned by Lua scripts, as + * they may not be encoded with the same serializer used here. They will + * be returned as byte[]s + */ + public T eval(String script, ReturnType returnType, int numKeys, String... keysAndArgs) { + return delegate.eval(serialize(script), returnType, numKeys, serializeMulti(keysAndArgs)); + } + + /** + * NOTE: This method will not deserialize Strings returned by Lua scripts, as + * they may not be encoded with the same serializer used here. They will + * be returned as byte[]s + */ + public T evalSha(String scriptSha1, ReturnType returnType, int numKeys, String... keysAndArgs) { + return delegate.evalSha(scriptSha1, returnType, numKeys, serializeMulti(keysAndArgs)); + } } \ No newline at end of file diff --git a/src/main/java/org/springframework/data/redis/connection/RedisCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisCommands.java index fec664664..2a9b68cdc 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisCommands.java @@ -24,7 +24,7 @@ package org.springframework.data.redis.connection; */ public interface RedisCommands extends RedisKeyCommands, RedisStringCommands, RedisListCommands, RedisSetCommands, RedisZSetCommands, RedisHashCommands, RedisTxCommands, RedisPubSubCommands, RedisConnectionCommands, - RedisServerCommands { + RedisServerCommands, RedisScriptingCommands { /** diff --git a/src/main/java/org/springframework/data/redis/connection/RedisScriptingCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisScriptingCommands.java new file mode 100644 index 000000000..cd836dcf8 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/RedisScriptingCommands.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2013 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.connection; + +import java.util.List; + +/** + * Scripting commands. + * + * @author Costin Leau + */ +public interface RedisScriptingCommands { + + void scriptFlush(); + + void scriptKill(); + + String scriptLoad(byte[] script); + + List scriptExists(String... scriptSha1); + + T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs); + + T evalSha(String scriptSha1, ReturnType returnType, int numKeys, byte[]... keysAndArgs); +} \ No newline at end of file diff --git a/src/main/java/org/springframework/data/redis/connection/ReturnType.java b/src/main/java/org/springframework/data/redis/connection/ReturnType.java new file mode 100644 index 000000000..5dee288bf --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReturnType.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013 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.connection; + +/** + * Represents a data type returned from Redis, currently used to denote the + * expected return type of Redis scripting commands + * + * @author Jennifer Hickey + * + */ +public enum ReturnType { + BOOLEAN, INTEGER, MULTI, STATUS, VALUE +} 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 5af01dab6..da511fb97 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -262,4 +262,10 @@ public interface StringRedisConnection extends RedisConnection { void subscribe(MessageListener listener, String... channels); void pSubscribe(MessageListener listener, String... patterns); + + String scriptLoad(String script); + + T eval(String script, ReturnType returnType, int numKeys, String... keysAndArgs); + + T evalSha(String scriptSha1, ReturnType returnType, int numKeys, String... keysAndArgs); } \ No newline at end of file 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 2b0448173..4a8f64041 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 @@ -32,6 +32,7 @@ import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisPipelineException; import org.springframework.data.redis.connection.RedisSubscribedConnectionException; +import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.Subscription; import org.springframework.util.Assert; @@ -2476,4 +2477,96 @@ public class JedisConnection implements RedisConnection { throw convertJedisAccessException(ex); } } + + // + // Scripting commands + // + + public void scriptFlush() { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + try { + jedis.scriptFlush(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + public void scriptKill() { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + try { + jedis.scriptKill(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + public String scriptLoad(byte[] script) { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + try { + return JedisUtils.asString(jedis.scriptLoad(script)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + public List scriptExists(String... scriptSha1) { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + try { + return jedis.scriptExists(scriptSha1); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @SuppressWarnings("unchecked") + public T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + try { + return (T) JedisUtils.convertScriptReturn(returnType, + jedis.eval(script, JedisUtils.asBytes(numKeys), keysAndArgs)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @SuppressWarnings("unchecked") + public T evalSha(String scriptSha1, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + try { + return (T) JedisUtils.convertScriptReturn(returnType, + jedis.evalsha(scriptSha1, numKeys, JedisUtils.convert(keysAndArgs))); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisUtils.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisUtils.java index 573f54ac1..4ce9075bd 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisUtils.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisUtils.java @@ -34,6 +34,7 @@ import org.springframework.data.redis.RedisConnectionFailureException; import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.DefaultTuple; import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; import org.springframework.data.redis.connection.SortParameters; @@ -48,11 +49,13 @@ import redis.clients.jedis.SortingParams; import redis.clients.jedis.exceptions.JedisConnectionException; import redis.clients.jedis.exceptions.JedisDataException; import redis.clients.jedis.exceptions.JedisException; +import redis.clients.util.SafeEncoder; /** * Helper class featuring methods for Jedis connection handling, providing support for exception translation. * * @author Costin Leau + * @author Jennifer Hickey */ public abstract class JedisUtils { @@ -216,7 +219,7 @@ public abstract class JedisUtils { String[] result = new String[raw.length]; for (int i = 0; i < raw.length; i++) { - result[i] = new String(raw[i]); + result[i] = SafeEncoder.encode(raw[i]); } return result; @@ -230,4 +233,45 @@ public abstract class JedisUtils { args.add(Protocol.toByteArray(timeout)); return args.toArray(new byte[args.size()][]); } + + static byte[] asBytes(int number) { + return String.valueOf(number).getBytes(); + } + + static String asString(byte[] raw) { + return SafeEncoder.encode(raw); + } + + @SuppressWarnings("unchecked") + static Object convertScriptReturn(ReturnType returnType, Object result) { + if(result instanceof String) { + //evalsha converts byte[] to String. Convert back for consistency + return SafeEncoder.encode((String)result); + } + if(returnType == ReturnType.STATUS) { + return JedisUtils.asString((byte[])result); + } + if(returnType == ReturnType.BOOLEAN) { + // Lua false comes back as a null bulk reply + if(result == null) { + return Boolean.FALSE; + } + return ((Long)result == 1); + } + if(returnType == ReturnType.MULTI) { + List resultList = (List) result; + List convertedResults = new ArrayList(); + for(Object res: resultList) { + if(res instanceof String) { + //evalsha converts byte[] to String. Convert back for consistency + convertedResults.add(SafeEncoder.encode((String)res)); + }else { + convertedResults.add(res); + } + } + return convertedResults; + } + return result; + } + } \ No newline at end of file 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 c56dfc060..b0aa5bea6 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 @@ -39,6 +39,7 @@ import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.Subscription; import org.springframework.util.Assert; @@ -1223,7 +1224,36 @@ public class JredisConnection implements RedisConnection { } + // + // Scripting commands + // + + public void subscribe(MessageListener listener, byte[]... channels) { throw new UnsupportedOperationException(); } + + public void scriptFlush() { + throw new UnsupportedOperationException(); + } + + public void scriptKill() { + throw new UnsupportedOperationException(); + } + + public String scriptLoad(byte[] script) { + throw new UnsupportedOperationException(); + } + + public List scriptExists(String... scriptSha1) { + throw new UnsupportedOperationException(); + } + + public T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + throw new UnsupportedOperationException(); + } + + public T evalSha(String scriptSha1, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + throw new UnsupportedOperationException(); + } } \ No newline at end of file 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 6600554c1..f49476c89 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 @@ -39,6 +39,7 @@ import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisPipelineException; import org.springframework.data.redis.connection.RedisSubscribedConnectionException; +import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.Subscription; import org.springframework.util.Assert; @@ -1963,6 +1964,92 @@ public class LettuceConnection implements RedisConnection { } + // + // Scripting commands + // + + public void scriptFlush() { + try { + if (isPipelined()) { + pipeline(getAsyncConnection().scriptFlush()); + return; + } + getConnection().scriptFlush(); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + + public void scriptKill() { + if(isQueueing()) { + throw new UnsupportedOperationException("Script kill not permitted in a transaction"); + } + try { + if (isPipelined()) { + pipeline(getAsyncConnection().scriptKill()); + return; + } + getConnection().scriptKill(); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + + public String scriptLoad(byte[] script) { + try { + if (isPipelined()) { + pipeline(getAsyncConnection().scriptLoad(script)); + return null; + } + return getConnection().scriptLoad(script); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + + public List scriptExists(String... scriptSha1) { + try { + if (isPipelined()) { + pipeline(getAsyncConnection().scriptExists(scriptSha1)); + return null; + } + return getConnection().scriptExists(scriptSha1); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + + public T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + try { + byte[][] keys = LettuceUtils.extractScriptKeys(numKeys, keysAndArgs); + byte[][] args = LettuceUtils.extractScriptArgs(numKeys, keysAndArgs); + + if (isPipelined()) { + pipeline(getAsyncConnection().eval(script, LettuceUtils.toScriptOutputType(returnType), keys, args)); + return null; + } + return getConnection().eval(script, LettuceUtils.toScriptOutputType(returnType), keys, args); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + + public T evalSha(String scriptSha1, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + try { + byte[][] keys = LettuceUtils.extractScriptKeys(numKeys, keysAndArgs); + byte[][] args = LettuceUtils.extractScriptArgs(numKeys, keysAndArgs); + + if (isPipelined()) { + pipeline(getAsyncConnection().evalsha(scriptSha1, LettuceUtils.toScriptOutputType(returnType), + keys, args)); + return null; + } + return getConnection().evalsha(scriptSha1, LettuceUtils.toScriptOutputType(returnType), keys, args); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + // // Pub/Sub functionality // diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceUtils.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceUtils.java index 2075cdfa5..d9416d7f4 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceUtils.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceUtils.java @@ -18,6 +18,7 @@ package org.springframework.data.redis.connection.lettuce; import java.io.StringReader; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Properties; @@ -26,6 +27,7 @@ import java.util.Set; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.DefaultTuple; +import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; @@ -37,13 +39,15 @@ import com.lambdaworks.redis.KeyValue; import com.lambdaworks.redis.RedisCommandInterruptedException; import com.lambdaworks.redis.RedisException; import com.lambdaworks.redis.ScoredValue; +import com.lambdaworks.redis.ScriptOutputType; import com.lambdaworks.redis.SortArgs; import com.lambdaworks.redis.ZStoreArgs; import com.lambdaworks.redis.codec.RedisCodec; import com.lambdaworks.redis.protocol.Charsets; /** - * Helper class featuring methods for Lettuce connection handling, providing support for exception translation. + * Helper class featuring methods for Lettuce connection handling, providing + * support for exception translation. * * @author Costin Leau */ @@ -166,4 +170,37 @@ abstract class LettuceUtils { list.add(blpop.value); return list; } + + static ScriptOutputType toScriptOutputType(ReturnType returnType) { + switch (returnType) { + case BOOLEAN: + return ScriptOutputType.BOOLEAN; + case MULTI: + return ScriptOutputType.MULTI; + case VALUE: + return ScriptOutputType.VALUE; + case INTEGER: + return ScriptOutputType.INTEGER; + case STATUS: + return ScriptOutputType.STATUS; + default: + throw new IllegalArgumentException("Return type " + returnType + + " is not a supported script output type"); + } + } + + static byte[][] extractScriptKeys(int numKeys, byte[]... keysAndArgs) { + if(numKeys > 0) { + return Arrays.copyOfRange(keysAndArgs, 0,numKeys); + } + return new byte[0][0]; + } + + static byte[][] extractScriptArgs(int numKeys, byte[]... keysAndArgs) { + if(keysAndArgs.length > numKeys) { + return Arrays.copyOfRange(keysAndArgs, numKeys, keysAndArgs.length); + } + return new byte[0][0]; + } + } \ No newline at end of file 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 7e6268868..3b95aae7c 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 @@ -33,6 +33,7 @@ import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisPipelineException; import org.springframework.data.redis.connection.RedisSubscribedConnectionException; +import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.Subscription; import org.springframework.util.Assert; @@ -1921,6 +1922,90 @@ public class SrpConnection implements RedisConnection { } } + // + // Scripting commands + // + + + public void scriptFlush() { + try { + if (isPipelined()) { + pipeline(pipeline.script_flush()); + return; + } + client.script_flush(); + } catch (Exception ex) { + throw convertSrpAccessException(ex); + } + } + + public void scriptKill() { + if(isQueueing()) { + throw new UnsupportedOperationException("Script kill not permitted in a transaction"); + } + try { + if (isPipelined()) { + pipeline(pipeline.script_kill()); + return; + } + client.script_kill(); + } catch (Exception ex) { + throw convertSrpAccessException(ex); + } + } + + public String scriptLoad(byte[] script) { + try { + if (isPipelined()) { + pipeline(pipeline.script_load(script)); + return null; + } + return SrpUtils.asShasum(client.script_load(script)); + } catch (Exception ex) { + throw convertSrpAccessException(ex); + } + } + + public List scriptExists(String... scriptSha1) { + try { + if (isPipelined()) { + pipeline(pipeline.script_exists((Object[])scriptSha1)); + return null; + } + return SrpUtils.asBooleanList(client.script_exists_((Object[])scriptSha1)); + } catch (Exception ex) { + throw convertSrpAccessException(ex); + } + } + + @SuppressWarnings("unchecked") + public T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + try { + if (isPipelined()) { + pipeline(pipeline.eval(script, numKeys, (Object[])keysAndArgs)); + return null; + } + return (T) SrpUtils.convertScriptReturn(returnType, client.eval(script, numKeys, + (Object[])keysAndArgs)); + } catch (Exception ex) { + throw convertSrpAccessException(ex); + } + } + + @SuppressWarnings("unchecked") + public T evalSha(String scriptSha1, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + try { + if (isPipelined()) { + pipeline(pipeline.evalsha(scriptSha1, numKeys, (Object[])keysAndArgs)); + return null; + } + return (T) SrpUtils.convertScriptReturn(returnType, + client.evalsha(scriptSha1, numKeys, (Object[])keysAndArgs)); + } catch (Exception ex) { + throw convertSrpAccessException(ex); + } + } + // // Pub/Sub functionality diff --git a/src/main/java/org/springframework/data/redis/connection/srp/SrpUtils.java b/src/main/java/org/springframework/data/redis/connection/srp/SrpUtils.java index c47a074be..7d310b375 100644 --- a/src/main/java/org/springframework/data/redis/connection/srp/SrpUtils.java +++ b/src/main/java/org/springframework/data/redis/connection/srp/SrpUtils.java @@ -29,9 +29,11 @@ import java.util.Set; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.DefaultTuple; +import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; +import org.springframework.data.redis.connection.jedis.JedisUtils; import org.springframework.data.redis.connection.SortParameters; import org.springframework.util.Assert; @@ -40,6 +42,7 @@ import redis.reply.BulkReply; import redis.reply.IntegerReply; import redis.reply.MultiBulkReply; import redis.reply.Reply; +import redis.reply.StatusReply; import com.google.common.base.Charsets; @@ -104,6 +107,14 @@ abstract class SrpUtils { return list; } + static List asStatusList(Reply[] replies) { + List statuses = new ArrayList(); + for(Reply reply: replies) { + statuses.add(((StatusReply)reply).data()); + } + return statuses; + } + static List toList(T[] byteArrays) { return Arrays.asList(byteArrays); } @@ -273,4 +284,51 @@ abstract class SrpUtils { Assert.notNull(op, "The bit operation is required"); return op.name().toUpperCase().getBytes(Charsets.UTF_8); } + + static String asShasum(Reply reply) { + Object data = reply.data(); + return (data instanceof String ? (String) data : new String((byte[]) data, Charsets.UTF_8)); + } + + static List asBooleanList(Reply reply) { + if(!(reply instanceof MultiBulkReply)) { + throw new IllegalArgumentException(); + } + List results = new ArrayList(); + for(Reply r: ((MultiBulkReply)reply).data()) { + results.add(SrpUtils.asBoolean((IntegerReply)r)); + } + return results; + } + + static List asIntegerList(Reply[] replies) { + List results = new ArrayList(); + for(Reply reply: replies) { + results.add(((IntegerReply)reply).data()); + } + return results; + } + + static List asList(MultiBulkReply genericReply) { + Reply[] replies = genericReply.data(); + List results = new ArrayList(); + for(Reply reply: replies) { + results.add(reply.data()); + } + return results; + } + + static Object convertScriptReturn(ReturnType returnType, Reply reply) { + if(reply instanceof MultiBulkReply) { + return SrpUtils.asList((MultiBulkReply)reply); + } + if(returnType == ReturnType.BOOLEAN) { + // Lua false comes back as a null bulk reply + if(reply.data() == null) { + return Boolean.FALSE; + } + return ((Long)reply.data() == 1); + } + return reply.data(); + } } \ No newline at end of file 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 c20bc4dd7..79b1dbf17 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import static org.springframework.data.redis.SpinBarrier.waitFor; import java.util.ArrayList; @@ -33,10 +34,12 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.UUID; import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.After; import org.junit.Before; @@ -47,6 +50,7 @@ import org.springframework.data.redis.Address; import org.springframework.data.redis.Person; import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.RedisTestProfileValueSource; +import org.springframework.data.redis.RedisVersionUtils; import org.springframework.data.redis.TestCondition; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; @@ -147,6 +151,152 @@ public abstract class AbstractConnectionIntegrationTests { assertFalse(connection.pExpireAt("nonexistent", System.currentTimeMillis() + 200)); } + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptLoadEvalSha() { + String sha1 = connection.scriptLoad("return KEYS[1]"); + actual.add(connection.evalSha(sha1, ReturnType.VALUE, 2, "key1", "key2")); + verifyResults(Arrays.asList(new Object[] {"key1"}), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalShaArrayStrings() { + String sha1 = connection.scriptLoad("return {KEYS[1],ARGV[1]}"); + actual.add(connection.evalSha(sha1, ReturnType.MULTI, 1, "key1", "arg1")); + verifyResults(Arrays.asList(new Object[] {Arrays.asList(new Object[] {"key1", "arg1"} )}), actual); + } + + @Test(expected=RedisSystemException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalShaNotFound() { + connection.evalSha("somefakesha", ReturnType.VALUE, 2, "key1", "key2"); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnString() { + actual.add(connection.eval("return KEYS[1]", ReturnType.VALUE, 1, "foo")); + verifyResults(Arrays.asList(new Object[] {"foo"}), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnNumber() { + actual.add(connection.eval("return 10", ReturnType.INTEGER, 0)); + verifyResults(Arrays.asList(new Object[] {10l}), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnSingleOK() { + actual.add(connection.eval("return redis.call('set','abc','ghk')", ReturnType.STATUS, 0)); + assertEquals(Arrays.asList(new Object[] { "OK" }), convertResults(false)); + } + + @Test(expected=RedisSystemException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnSingleError() { + connection.eval("return redis.call('expire','foo')", ReturnType.BOOLEAN, 0); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnFalse() { + actual.add(connection.eval("return false", ReturnType.BOOLEAN, 0)); + verifyResults(Arrays.asList(new Object[] { false }), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnTrue() { + actual.add(connection.eval("return true", ReturnType.BOOLEAN, 0)); + verifyResults(Arrays.asList(new Object[] { true }), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayStrings() { + actual.add(connection.eval("return {KEYS[1],ARGV[1]}", ReturnType.MULTI, 1, "foo", "bar")); + verifyResults(Arrays.asList(new Object[] {Arrays.asList(new Object[] {"foo", "bar"} )}), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayNumbers() { + actual.add(connection.eval("return {1,2}", ReturnType.MULTI, 1, "foo", "bar")); + verifyResults(Arrays.asList(new Object[] {Arrays.asList(new Object[] {1l, 2l} )}), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayOKs() { + actual.add(connection.eval("return { redis.call('set','abc','ghk'), redis.call('set','abc','lfdf')}", + ReturnType.MULTI, 0)); + assertEquals(Arrays.asList(new Object[] {Arrays.asList(new Object[] {"OK", "OK"} )}), convertResults(false)); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayFalses() { + actual.add(connection.eval("return { false, false}", ReturnType.MULTI, 0)); + verifyResults(Arrays.asList(new Object[] {Arrays.asList(new Object[] {null, null} )}), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayTrues() { + actual.add(connection.eval("return { true, true}", ReturnType.MULTI,0)); + verifyResults(Arrays.asList(new Object[] {Arrays.asList(new Object[] {1l, 1l} )}), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptExists() { + String sha1 = connection.scriptLoad("return 'foo'"); + actual.add(connection.scriptExists(sha1, "98777234")); + verifyResults(Arrays.asList(new Object[] {Arrays.asList(new Object[] {true, false})}), actual); + } + + @Test + @IfProfileValue(name = "runLongTests", value = "true") + public void testScriptKill() throws Exception{ + getResults(); + assumeTrue(RedisVersionUtils.atLeast("2.6", byteConnection)); + initConnection(); + final AtomicBoolean scriptDead = new AtomicBoolean(false); + Thread th = new Thread(new Runnable() { + public void run() { + DefaultStringRedisConnection conn2 = new DefaultStringRedisConnection( + connectionFactory.getConnection()); + try { + conn2.eval("local time=1 while time < 10000000000 do time=time+1 end", ReturnType.BOOLEAN, 0); + }catch(DataAccessException e) { + scriptDead.set(true); + } + conn2.close(); + } + }); + th.start(); + Thread.sleep(1000); + connection.scriptKill(); + getResults(); + assertTrue(waitFor(new TestCondition() { + public boolean passes() { + return scriptDead.get(); + } + }, 3000l)); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptFlush() { + String sha1 = connection.scriptLoad("return KEYS[1]"); + connection.scriptFlush(); + actual.add(connection.scriptExists(sha1)); + verifyResults(Arrays.asList(new Object[] {Arrays.asList(new Object[] { false })}), actual); + } + @Test @IfProfileValue(name = "runLongTests", value = "true") public void testPersist() throws Exception { @@ -1394,7 +1544,51 @@ public abstract class AbstractConnectionIntegrationTests { } protected void verifyResults(List expected, List actual) { - assertEquals(expected, actual); + assertEquals(expected, convertResults(true)); + } + + protected List convertResults() { + return convertResults(true); + } + + protected List convertResults(boolean filterStatus) { + List actual = getResults(); + List serializedResults = new ArrayList(); + for (Object result : actual) { + Object convertedResult = convertResult(result); + if (filterStatus) { + if(!"OK".equals(convertedResult) && !"QUEUED".equals(convertedResult)) { + serializedResults.add(convertedResult); + } + }else { + serializedResults.add(convertedResult); + } + } + return serializedResults; + } + + protected List getResults() { + return actual; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected Object convertResult(Object result) { + if (result instanceof List && !(((List) result).isEmpty()) + && ((List) result).get(0) instanceof byte[]) { + return (SerializationUtils.deserialize((List) result, stringSerializer)); + } else if (result instanceof byte[]) { + return (stringSerializer.deserialize((byte[]) result)); + } else if (result instanceof Map + && ((Map) result).keySet().iterator().next() instanceof byte[]) { + return (SerializationUtils.deserialize((Map) result, stringSerializer)); + } else if (result instanceof Set && !(((Set) result).isEmpty()) + && ((Set) result).iterator().next() instanceof byte[]) { + return (SerializationUtils.deserialize((Set) result, stringSerializer)); + } + return result; + } + + protected void initConnection() { } protected class KeyExpired implements TestCondition { diff --git a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionPipelineIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionPipelineIntegrationTests.java index f5c734937..aa6f98e74 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionPipelineIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionPipelineIntegrationTests.java @@ -31,7 +31,6 @@ import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.UUID; import org.junit.Before; @@ -42,7 +41,6 @@ import org.springframework.data.redis.connection.RedisStringCommands.BitOperatio import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; import org.springframework.data.redis.connection.StringRedisConnection.StringTuple; -import org.springframework.data.redis.serializer.SerializationUtils; import org.springframework.test.annotation.IfProfileValue; /** @@ -960,6 +958,79 @@ abstract public class AbstractConnectionPipelineIntegrationTests extends Arrays.asList(new String[] { "foo", "bar" }) }), actual); } + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptLoadEvalSha() { + // close the pipeline to get the return value of script load + getResults(); + String sha1 = connection.scriptLoad("return KEYS[1]"); + initConnection(); + actual.add(connection.evalSha(sha1, ReturnType.VALUE, 2, "key1", "key2")); + verifyResults(Arrays.asList(new Object[] {"key1"}), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalShaArrayStrings() { + // close the pipeline to get the return value of script load + getResults(); + String sha1 = connection.scriptLoad("return {KEYS[1],ARGV[1]}"); + initConnection(); + actual.add(connection.evalSha(sha1, ReturnType.MULTI, 1, "key1", "arg1")); + verifyResults(Arrays.asList(new Object[] { Arrays.asList(new Object[] {"key1", "arg1"})}), actual); + } + + @Test(expected=RedisPipelineException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalShaNotFound() { + connection.evalSha("somefakesha", ReturnType.VALUE, 2, "key1", "key2"); + getResults(); + } + + @Test(expected=RedisPipelineException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnSingleError() { + connection.eval("return redis.call('expire','foo')", ReturnType.BOOLEAN, 0); + getResults(); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnFalse() { + // pipelined results don't get converted to Booleans + actual.add(connection.eval("return false", ReturnType.BOOLEAN, 0)); + verifyResults(Arrays.asList(new Object[] { null }), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnTrue() { + // pipelined results don't get converted to Booleans + actual.add(connection.eval("return true", ReturnType.BOOLEAN, 0)); + verifyResults(Arrays.asList(new Object[] { 1l }), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptExists() { + getResults(); + String sha1 = connection.scriptLoad("return 'foo'"); + initConnection(); + actual.add(connection.scriptExists(sha1, "98777234")); + verifyResults(Arrays.asList(new Object[] {Arrays.asList(new Object[] {true, false})}), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptFlush() { + getResults(); + String sha1 = connection.scriptLoad("return KEYS[1]"); + connection.scriptFlush(); + initConnection(); + actual.add(connection.scriptExists(sha1)); + verifyResults(Arrays.asList(new Object[] {Arrays.asList(new Object[] { 0l })}), actual); + } + protected void initConnection() { connection.openPipeline(); } @@ -976,33 +1047,4 @@ abstract public class AbstractConnectionPipelineIntegrationTests extends protected List getResults() { return connection.closePipeline(); } - - protected List convertResults() { - List pipelinedResults = getResults(); - List serializedResults = new ArrayList(); - for (Object result : pipelinedResults) { - Object convertedResult = convertResult(result); - if (!"OK".equals(convertedResult) && !"QUEUED".equals(convertedResult)) { - serializedResults.add(convertedResult); - } - } - return serializedResults; - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - protected Object convertResult(Object result) { - if (result instanceof List && !(((List) result).isEmpty()) - && ((List) result).get(0) instanceof byte[]) { - return (SerializationUtils.deserialize((List) result, stringSerializer)); - } else if (result instanceof byte[]) { - return (stringSerializer.deserialize((byte[]) result)); - } else if (result instanceof Map - && ((Map) result).keySet().iterator().next() instanceof byte[]) { - return (SerializationUtils.deserialize((Map) result, stringSerializer)); - } else if (result instanceof Set && !(((Set) result).isEmpty()) - && ((Set) result).iterator().next() instanceof byte[]) { - return (SerializationUtils.deserialize((Set) result, stringSerializer)); - } - return result; - } } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests.java index de9b95bc9..4e2303816 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests.java @@ -21,7 +21,10 @@ import static org.junit.Assert.assertEquals; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.AbstractConnectionIntegrationTests; +import org.springframework.data.redis.connection.ReturnType; +import org.springframework.test.annotation.IfProfileValue; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -180,4 +183,16 @@ public class JedisConnectionIntegrationTests extends AbstractConnectionIntegrati connection.hIncrBy(key, hkey, -2 * largeNumber); assertEquals(-largeNumber, Long.valueOf(connection.hGet(key, hkey)).longValue()); } + + @Test(expected=InvalidDataAccessApiUsageException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnSingleError() { + connection.eval("return redis.call('expire','foo')", ReturnType.BOOLEAN, 0); + } + + @Test(expected=InvalidDataAccessApiUsageException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalShaNotFound() { + connection.evalSha("somefakesha", ReturnType.VALUE, 2, "key1", "key2"); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionPipelineIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionPipelineIntegrationTests.java index 34bdb3ccf..524d0e8c4 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionPipelineIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionPipelineIntegrationTests.java @@ -31,6 +31,7 @@ import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.AbstractConnectionPipelineIntegrationTests; import org.springframework.data.redis.connection.DefaultStringTuple; import org.springframework.data.redis.connection.StringRedisConnection.StringTuple; +import org.springframework.test.annotation.IfProfileValue; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -242,6 +243,108 @@ public class JedisConnectionPipelineIntegrationTests extends super.testIncrByDouble(); } + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptLoadEvalSha() { + super.testScriptLoadEvalSha(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalShaArrayStrings() { + super.testEvalShaArrayStrings(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalShaNotFound() { + super.testEvalShaNotFound(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnString() { + super.testEvalReturnString(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnNumber() { + super.testEvalReturnNumber(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnSingleOK() { + super.testEvalReturnSingleOK(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnSingleError() { + super.testEvalReturnSingleError(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnFalse() { + super.testEvalReturnFalse(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnTrue() { + super.testEvalReturnTrue(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayStrings() { + super.testEvalReturnArrayStrings(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayNumbers() { + super.testEvalReturnArrayNumbers(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayOKs() { + super.testEvalReturnArrayOKs(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayFalses() { + super.testEvalReturnArrayFalses(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayTrues() { + super.testEvalReturnArrayTrues(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptExists() { + super.testScriptExists(); + } + + @IfProfileValue(name = "redisVersion", value = "2.6") + @Test(expected=UnsupportedOperationException.class) + public void testScriptKill() throws Exception{ + connection.scriptKill(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptFlush() { + super.testScriptFlush(); + } + // Overrides, usually due to return values being Long vs Boolean or Set vs // List 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 577963a7e..10ef1942d 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 @@ -21,6 +21,7 @@ import static org.junit.Assert.fail; import java.util.Arrays; +import org.apache.commons.pool.impl.GenericObjectPool.Config; import org.jredis.JRedis; import org.jredis.protocol.BulkResponse; import org.junit.After; @@ -34,9 +35,9 @@ import org.springframework.data.redis.connection.AbstractConnectionIntegrationTe import org.springframework.data.redis.connection.DefaultSortParameters; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.SortParameters.Order; +import org.springframework.test.annotation.IfProfileValue; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.apache.commons.pool.impl.GenericObjectPool.Config; /** * Integration test of {@link JredisConnection} @@ -393,6 +394,107 @@ public class JRedisConnectionIntegrationTests extends AbstractConnectionIntegrat super.testIncrByDouble(); } + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptLoadEvalSha() { + super.testScriptLoadEvalSha(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalShaArrayStrings() { + super.testEvalShaArrayStrings(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalShaNotFound() { + super.testEvalShaNotFound(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnString() { + super.testEvalReturnString(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnNumber() { + super.testEvalReturnNumber(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnSingleOK() { + super.testEvalReturnSingleOK(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnSingleError() { + super.testEvalReturnSingleError(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnFalse() { + super.testEvalReturnFalse(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnTrue() { + super.testEvalReturnTrue(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayStrings() { + super.testEvalReturnArrayStrings(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayNumbers() { + super.testEvalReturnArrayNumbers(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayOKs() { + super.testEvalReturnArrayOKs(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayFalses() { + super.testEvalReturnArrayFalses(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayTrues() { + super.testEvalReturnArrayTrues(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptExists() { + super.testScriptExists(); + } + + @Test(expected=UnsupportedOperationException.class) + public void testScriptKill() throws Exception{ + connection.scriptKill(); + } + + @Test(expected=UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptFlush() { + super.testScriptFlush(); + } + // Jredis returns null for rPush @Test public void testSort() { 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 a507b5c26..96e96fd85 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 @@ -16,17 +16,26 @@ package org.springframework.data.redis.connection.lettuce; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; +import static org.springframework.data.redis.SpinBarrier.waitFor; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.dao.DataAccessException; import org.springframework.data.redis.RedisSystemException; +import org.springframework.data.redis.RedisVersionUtils; +import org.springframework.data.redis.SettingsUtils; +import org.springframework.data.redis.TestCondition; import org.springframework.data.redis.connection.AbstractConnectionIntegrationTests; import org.springframework.data.redis.connection.DefaultStringRedisConnection; +import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; import org.springframework.test.annotation.IfProfileValue; import org.springframework.test.context.ContextConfiguration; @@ -134,4 +143,35 @@ public class LettuceConnectionIntegrationTests extends AbstractConnectionIntegra connection.set("key2", "efgh"); actual.add(connection.bitOp(BitOperation.NOT, "key3", "key1", "key2")); } + + @Test + @IfProfileValue(name = "runLongTests", value = "true") + public void testScriptKill() throws Exception{ + getResults(); + assumeTrue(RedisVersionUtils.atLeast("2.6", byteConnection)); + final AtomicBoolean scriptDead = new AtomicBoolean(false); + Thread th = new Thread(new Runnable() { + public void run() { + final LettuceConnectionFactory factory2 = new LettuceConnectionFactory(SettingsUtils.getHost(), + SettingsUtils.getPort()); + factory2.afterPropertiesSet(); + DefaultStringRedisConnection conn2 = new DefaultStringRedisConnection( + factory2.getConnection()); + try { + conn2.eval("local time=1 while time < 10000000000 do time=time+1 end", ReturnType.BOOLEAN, 0); + }catch(DataAccessException e) { + scriptDead.set(true); + } + conn2.close(); + } + }); + th.start(); + Thread.sleep(1000); + connection.scriptKill(); + assertTrue(waitFor(new TestCondition() { + public boolean passes() { + return scriptDead.get(); + } + }, 3000l)); + } } \ No newline at end of file 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 8e414d925..ad9bc0cb5 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 @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; import static org.springframework.data.redis.SpinBarrier.waitFor; import java.util.ArrayList; @@ -29,14 +30,19 @@ import java.util.List; import java.util.Map; import java.util.Properties; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.RedisVersionUtils; +import org.springframework.data.redis.SettingsUtils; import org.springframework.data.redis.TestCondition; import org.springframework.data.redis.connection.AbstractConnectionPipelineIntegrationTests; import org.springframework.data.redis.connection.DefaultStringRedisConnection; import org.springframework.data.redis.connection.DefaultStringTuple; +import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; import org.springframework.data.redis.connection.StringRedisConnection.StringTuple; import org.springframework.test.annotation.IfProfileValue; @@ -352,6 +358,67 @@ public class LettuceConnectionPipelineIntegrationTests extends } } + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnFalse() { + // Lettuce actually returns booleans, it's not an SDR conversion + actual.add(connection.eval("return false", ReturnType.BOOLEAN, 0)); + verifyResults(Arrays.asList(new Object[] { false }), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnTrue() { + // Lettuce actually returns booleans, it's not an SDR conversion + actual.add(connection.eval("return true", ReturnType.BOOLEAN, 0)); + verifyResults(Arrays.asList(new Object[] { true }), actual); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptFlush() { + getResults(); + String sha1 = connection.scriptLoad("return KEYS[1]"); + connection.scriptFlush(); + initConnection(); + actual.add(connection.scriptExists(sha1)); + // Lettuce actually returns booleans, it's not an SDR conversion + verifyResults(Arrays.asList(new Object[] {Arrays.asList(new Object[] { false })}), actual); + } + + @Test + @IfProfileValue(name = "runLongTests", value = "true") + public void testScriptKill() throws Exception{ + getResults(); + assumeTrue(RedisVersionUtils.atLeast("2.6", byteConnection)); + initConnection(); + final AtomicBoolean scriptDead = new AtomicBoolean(false); + Thread th = new Thread(new Runnable() { + public void run() { + final LettuceConnectionFactory factory2 = new LettuceConnectionFactory(SettingsUtils.getHost(), + SettingsUtils.getPort()); + factory2.afterPropertiesSet(); + DefaultStringRedisConnection conn2 = new DefaultStringRedisConnection( + factory2.getConnection()); + try { + conn2.eval("local time=1 while time < 10000000000 do time=time+1 end", ReturnType.BOOLEAN, 0); + }catch(DataAccessException e) { + scriptDead.set(true); + } + conn2.close(); + } + }); + th.start(); + Thread.sleep(1000); + getResults(); + connection.scriptKill(); + assertTrue(waitFor(new TestCondition() { + public boolean passes() { + return scriptDead.get(); + } + }, 3000l)); + } + @SuppressWarnings({ "rawtypes", "unchecked" }) protected Object convertResult(Object result) { Object convertedResult = super.convertResult(result); 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 eb35abc3e..4f4c5197e 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 @@ -6,11 +6,13 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.util.Arrays; import java.util.List; import java.util.Properties; import org.junit.Test; import org.springframework.data.redis.connection.RedisPipelineException; +import org.springframework.data.redis.connection.ReturnType; import org.springframework.test.annotation.IfProfileValue; /** @@ -71,6 +73,37 @@ public class LettuceConnectionPipelineTxIntegrationTests extends } } + @Test(expected=RedisPipelineException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalShaNotFound() { + connection.evalSha("somefakesha", ReturnType.VALUE, 2, "key1", "key2"); + getResults(); + } + + @Test(expected=RedisPipelineException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnSingleError() { + connection.eval("return redis.call('expire','foo')", ReturnType.BOOLEAN, 0); + getResults(); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnSingleOK() { + actual.add(connection.eval("return redis.call('set','abc','ghk')", ReturnType.STATUS, 0)); + // We pipeline the result of multi, so add an extra OK here + assertEquals(Arrays.asList(new Object[] { "OK", "OK" }), convertResults(false)); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayOKs() { + actual.add(connection.eval("return { redis.call('set','abc','ghk'), redis.call('set','abc','lfdf')}", + ReturnType.MULTI, 0)); + // We pipeline the result of multi, so add an extra OK here + assertEquals(Arrays.asList(new Object[] {"OK", Arrays.asList(new Object[] {"OK", "OK"} )}), convertResults(false)); + } + protected void initConnection() { connection.openPipeline(); connection.multi(); 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 0c75559da..9eb1d2a93 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 @@ -23,6 +23,7 @@ import java.util.List; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.data.redis.connection.ReturnType; import org.springframework.test.annotation.IfProfileValue; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -118,6 +119,30 @@ public class LettuceConnectionTransactionIntegrationTests extends assertTrue(restoreResults.get(0) instanceof RedisException); } + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalShaNotFound() { + connection.evalSha("somefakesha", ReturnType.VALUE, 2, "key1", "key2"); + List results = getResults(); + assertTrue(results.get(0) instanceof RedisException); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnSingleError() { + connection.eval("return redis.call('expire','foo')", ReturnType.BOOLEAN, 0); + List results = getResults(); + assertTrue(results.get(0) instanceof RedisException); + } + + @Test(expected = UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptKill() { + // Impossible to call script kill in a tx because you can't issue the + // exec command while Redis is running a script + connection.scriptKill(); + } + protected void initConnection() { connection.multi(); } diff --git a/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionIntegrationTests.java index 1279035a1..94438f144 100644 --- a/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionIntegrationTests.java @@ -18,10 +18,14 @@ package org.springframework.data.redis.connection.srp; import static org.junit.Assert.assertEquals; +import java.util.Arrays; + import org.junit.After; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.data.redis.connection.AbstractConnectionIntegrationTests; +import org.springframework.data.redis.connection.ReturnType; import org.springframework.test.annotation.IfProfileValue; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -74,4 +78,18 @@ public class SrpConnectionIntegrationTests extends AbstractConnectionIntegration BulkReply reply = (BulkReply) connection.execute("GET", "foo"); assertEquals("bar", stringSerializer.deserialize(reply.data())); } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnArrayOKs() { + // SRP returns the Strings from individual StatusReplys in a MultiBulkReply, while other clients return as byte[] + actual.add(connection.eval("return { redis.call('set','abc','ghk'), redis.call('set','abc','lfdf')}", + ReturnType.MULTI, 0)); + verifyResults(Arrays.asList(new Object[] {Arrays.asList(new Object[] {"OK", "OK"} )}), actual); + } + + @Ignore("https://github.com/spullara/redis-protocol/issues/25") + public void testScriptExists() { + //script_exists only returns one result and it's false + } } diff --git a/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionPipelineIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionPipelineIntegrationTests.java index cef67abab..dc26c0788 100644 --- a/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionPipelineIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionPipelineIntegrationTests.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Properties; import java.util.Set; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.data.redis.connection.AbstractConnectionPipelineIntegrationTests; @@ -42,7 +43,9 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import redis.reply.BulkReply; +import redis.reply.IntegerReply; import redis.reply.Reply; +import redis.reply.StatusReply; /** * Integration test of {@link SrpConnection} pipeline functionality @@ -190,6 +193,11 @@ public class SrpConnectionPipelineIntegrationTests extends verifyResults(Arrays.asList(new Object[] { 1l, "6.4", "6.4" }), actual); } + @Ignore("https://github.com/spullara/redis-protocol/issues/25") + public void testScriptExists() { + // script_exists only returns one result and it's false + } + protected Object convertResult(Object result) { Object convertedResult = super.convertResult(result); if (convertedResult instanceof Reply[]) { @@ -203,7 +211,11 @@ public class SrpConnectionPipelineIntegrationTests extends stringTuples.add(new DefaultStringTuple(tuple, new String(tuple.getValue()))); } return stringTuples; - } else { + } else if(((Reply[]) convertedResult).length > 0 && ((Reply[])convertedResult)[0] instanceof IntegerReply) { + return SrpUtils.asIntegerList((Reply[])convertedResult); + } else if(((Reply[]) convertedResult).length > 0 && ((Reply[])convertedResult)[0] instanceof StatusReply) { + return SrpUtils.asStatusList((Reply[])convertedResult); + } else { return SerializationUtils.deserialize( SrpUtils.toBytesList((Reply[]) convertedResult), stringSerializer); } diff --git a/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionPipelineTxIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionPipelineTxIntegrationTests.java index f24c7fe10..1020c5d8b 100644 --- a/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionPipelineTxIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionPipelineTxIntegrationTests.java @@ -17,8 +17,8 @@ package org.springframework.data.redis.connection.srp; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import static org.junit.Assume.assumeTrue; import static org.junit.Assert.fail; + import java.util.Arrays; import java.util.List; diff --git a/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionTransactionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionTransactionIntegrationTests.java index 28027ae48..7fc095342 100644 --- a/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/srp/SrpConnectionTransactionIntegrationTests.java @@ -95,6 +95,22 @@ public class SrpConnectionTransactionIntegrationTests extends SrpConnectionPipel verifyResults(Arrays.asList(new Object[] { "sup", 13l, "suckrcalifrag" }), actual); } + @Ignore("https://github.com/spullara/redis-protocol/issues/24") + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalReturnSingleError() { + // SRP issue exec does not report individual ErrorReplys in a MultiBulkReply + // as Exceptions + } + + @Ignore("https://github.com/spullara/redis-protocol/issues/24") + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testEvalShaNotFound() { + // SRP issue exec does not report individual ErrorReplys in a MultiBulkReply + // as Exceptions + } + @Ignore("https://github.com/spullara/redis-protocol/issues/24") @Test @IfProfileValue(name = "redisVersion", value = "2.6") @@ -119,6 +135,14 @@ public class SrpConnectionTransactionIntegrationTests extends SrpConnectionPipel // as Exceptions } + @Test(expected = UnsupportedOperationException.class) + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testScriptKill() { + // Impossible to call script kill in a tx because you can't issue the + // exec command while Redis is running a script + connection.scriptKill(); + } + protected void initConnection() { connection.multi(); } diff --git a/src/test/resources/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests-context.xml b/src/test/resources/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests-context.xml index ba982af64..e5a93aef9 100644 --- a/src/test/resources/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests-context.xml +++ b/src/test/resources/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests-context.xml @@ -10,7 +10,7 @@ reconnect them on next attempted use --> + p:usePool="false" p:timeout="60000">