Add Redis 2.6 scripting commands to Connections

DATAREDIS-180
This commit is contained in:
Jennifer Hickey
2013-06-19 18:50:51 -07:00
parent 844782384d
commit 2c9ba9a5ed
26 changed files with 1266 additions and 39 deletions

View File

@@ -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<Boolean> scriptExists(String... scriptSha1) {
return delegate.scriptExists(scriptSha1);
}
public <T> T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) {
return delegate.eval(script, returnType, numKeys, keysAndArgs);
}
public <T> 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> 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> T evalSha(String scriptSha1, ReturnType returnType, int numKeys, String... keysAndArgs) {
return delegate.evalSha(scriptSha1, returnType, numKeys, serializeMulti(keysAndArgs));
}
}

View File

@@ -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 {
/**

View File

@@ -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<Boolean> scriptExists(String... scriptSha1);
<T> T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs);
<T> T evalSha(String scriptSha1, ReturnType returnType, int numKeys, byte[]... keysAndArgs);
}

View File

@@ -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
}

View File

@@ -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> T eval(String script, ReturnType returnType, int numKeys, String... keysAndArgs);
<T> T evalSha(String scriptSha1, ReturnType returnType, int numKeys, String... keysAndArgs);
}

View File

@@ -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<Boolean> 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> 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> 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);
}
}
}

View File

@@ -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<Object> resultList = (List<Object>) result;
List<Object> convertedResults = new ArrayList<Object>();
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;
}
}

View File

@@ -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<Boolean> scriptExists(String... scriptSha1) {
throw new UnsupportedOperationException();
}
public <T> T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) {
throw new UnsupportedOperationException();
}
public <T> T evalSha(String scriptSha1, ReturnType returnType, int numKeys, byte[]... keysAndArgs) {
throw new UnsupportedOperationException();
}
}

View File

@@ -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<Boolean> scriptExists(String... scriptSha1) {
try {
if (isPipelined()) {
pipeline(getAsyncConnection().scriptExists(scriptSha1));
return null;
}
return getConnection().scriptExists(scriptSha1);
} catch (Exception ex) {
throw convertLettuceAccessException(ex);
}
}
public <T> 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> 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
//

View File

@@ -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];
}
}

View File

@@ -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<Boolean> 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> 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> 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

View File

@@ -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<String> asStatusList(Reply[] replies) {
List<String> statuses = new ArrayList<String>();
for(Reply reply: replies) {
statuses.add(((StatusReply)reply).data());
}
return statuses;
}
static <T> List<T> 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<Boolean> asBooleanList(Reply reply) {
if(!(reply instanceof MultiBulkReply)) {
throw new IllegalArgumentException();
}
List<Boolean> results = new ArrayList<Boolean>();
for(Reply r: ((MultiBulkReply)reply).data()) {
results.add(SrpUtils.asBoolean((IntegerReply)r));
}
return results;
}
static List<Long> asIntegerList(Reply[] replies) {
List<Long> results = new ArrayList<Long>();
for(Reply reply: replies) {
results.add(((IntegerReply)reply).data());
}
return results;
}
static List<Object> asList(MultiBulkReply genericReply) {
Reply[] replies = genericReply.data();
List<Object> results = new ArrayList<Object>();
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();
}
}