Browse files

support redis scripting commands

  • Loading branch information...
1 parent b305a52 commit e9f71f93aaa5ac821cd101772eff05c538d6a5d4 @wg committed Nov 29, 2011
View
62 src/main/java/com/lambdaworks/codec/Base16.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2011 - Will Glozer. All rights reserved.
+
+package com.lambdaworks.codec;
+
+/**
+ * High-performance base16 (AKA hex) codec.
+ *
+ * @author Will Glozer
+ */
+public class Base16 {
+ private static final char[] upper = "0123456789ABCDEF".toCharArray();
+ private static final char[] lower = "0123456789abcdef".toCharArray();
+ private static final byte[] decode = new byte[128];
+
+ static {
+ for (int i = 0; i < 10; i++) {
+ decode['0' + i] = (byte) i;
+ decode['A' + i] = (byte) (10 + i);
+ decode['a' + i] = (byte) (10 + i);
+ }
+ }
+
+ /**
+ * Encode bytes to base16 chars.
+ *
+ * @param src Bytes to encode.
+ * @param upper Use upper or lowercase chars.
+ *
+ * @return Encoded chars.
+ */
+ public static char[] encode(byte[] src, boolean upper) {
+ char[] table = upper ? Base16.upper : Base16.lower;
+ char[] dst = new char[src.length * 2];
+
+ for (int si = 0, di = 0; si < src.length; si++) {
+ byte b = src[si];
+ dst[di++] = table[(b & 0xf0) >>> 4];
+ dst[di++] = table[(b & 0x0f)];
+ }
+
+ return dst;
+ }
+
+ /**
+ * Decode base16 chars to bytes.
+ *
+ * @param src Chars to decode.
+ *
+ * @return Decoded bytes.
+ */
+ public static byte[] decode(char[] src) {
+ byte[] dst = new byte[src.length / 2];
+
+ for (int si = 0, di = 0; di < dst.length; di++) {
+ byte high = decode[src[si++] & 0x7f];
+ byte low = decode[src[si++] & 0x7f];
+ dst[di] = (byte) ((high << 4) + low);
+ }
+
+ return dst;
+ }
+}
View
53 src/main/java/com/lambdaworks/redis/RedisAsyncConnection.java
@@ -2,11 +2,14 @@
package com.lambdaworks.redis;
+import com.lambdaworks.codec.Base16;
import com.lambdaworks.redis.codec.RedisCodec;
import com.lambdaworks.redis.output.*;
import com.lambdaworks.redis.protocol.*;
import org.jboss.netty.channel.*;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.*;
@@ -155,6 +158,18 @@ public String auth(String password) {
return dispatch(ECHO, new ValueOutput<K, V>(codec), args);
}
+ public <T> Future<T> eval(V script, Class<T> type, K... keys) {
+ CommandArgs<K, V> args = new CommandArgs<K, V>(codec);
+ args.addValue(script).add(keys.length).addKeys(keys);
+ return dispatch(EVAL, newOutputForType(type), args);
+ }
+
+ public <T> Future<T> evalsha(String digest, Class<T> type, K... keys) {
+ CommandArgs<K, V> args = new CommandArgs<K, V>(codec);
+ args.add(digest).add(keys.length).addKeys(keys);
+ return dispatch(EVALSHA, newOutputForType(type), args);
+ }
+
public Future<Boolean> exists(K key) {
return dispatch(EXISTS, new BooleanOutput<K, V>(codec), key);
}
@@ -459,6 +474,22 @@ public String auth(String password) {
return dispatch(SCARD, new IntegerOutput<K, V>(codec), key);
}
+ public Future<List<Boolean>> scriptExists(String... digests) {
+ CommandArgs<K, V> args = new CommandArgs<K, V>(codec).add(EXISTS);
+ for (String sha : digests) args.add(sha);
+ return dispatch(SCRIPT, new BooleanListOutput<K, V>(codec), args);
+ }
+
+ public Future<String> scriptFlush() {
+ CommandArgs<K, V> args = new CommandArgs<K, V>(codec).add(FLUSH);
+ return dispatch(SCRIPT, new StatusOutput<K, V>(codec), args);
+ }
+
+ public Future<String> scriptLoad(V script) {
+ CommandArgs<K, V> args = new CommandArgs<K, V>(codec).add(LOAD).addValue(script);
+ return dispatch(SCRIPT, new StatusOutput<K, V>(codec), args);
+ }
+
public Future<Set<V>> sdiff(K... keys) {
CommandArgs<K, V> args = new CommandArgs<K, V>(codec).addKeys(keys);
return dispatch(SDIFF, new ValueSetOutput<K, V>(codec), args);
@@ -867,6 +898,16 @@ public synchronized void close() {
}
}
+ public String digest(V script) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA1");
+ md.update(codec.encodeValue(script));
+ return new String(Base16.encode(md.digest(), false));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RedisException("JVM does not support SHA1");
+ }
+ }
+
@Override
public synchronized void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
channel = ctx.getChannel();
@@ -960,6 +1001,18 @@ public synchronized void channelClosed(ChannelHandlerContext ctx, ChannelStateEv
return output.get();
}
+ public <T> CommandOutput<K, V, T> newOutputForType(Class<T> type) {
+ CommandOutput<K, V, T> output;
+ if (type == Long.class) {
+ output = (CommandOutput<K, V, T>) new IntegerOutput<K, V>(codec);
+ } else if (type == List.class) {
+ output = (CommandOutput<K, V, T>) new NestedMultiOutput<K, V>(codec);
+ } else {
+ output = (CommandOutput<K, V, T>) new ValueOutput<K, V>(codec);
+ }
+ return output;
+ }
+
public String string(double n) {
if (Double.isInfinite(n)) {
return (n > 0) ? "+inf" : "-inf";
View
61 src/main/java/com/lambdaworks/redis/RedisConnection.java
@@ -5,6 +5,8 @@
import com.lambdaworks.redis.protocol.Command;
import com.lambdaworks.redis.protocol.ConnectionWatchdog;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@@ -131,6 +133,42 @@ public V echo(V msg) {
return await(c.echo(msg));
}
+ /**
+ * Eval the supplied script. Callers may request a single 64-bit integer result by
+ * passing {@code Long.class}, a (possibly deeply nested) list of {@code Longs, Strings,
+ * Vs} and possibly even {@link RedisException}s by passing {@code List.class},
+ * or a single {@code V} by passing any other class.
+ *
+ * @param script Lua script to evaluate.
+ * @param type Class of script return type.
+ * @param keys Redis keys to pass to script.
+ *
+ * @param <T> Script return type.
+ *
+ * @return The result of evaluating the script.
+ */
+ public <T> T eval(V script, Class<T> type, K... keys) {
+ return await(c.eval(script, type, keys));
+ }
+
+ /**
+ * Eval a preloaded script identified by a SHA-1 digest. Callers may request a single
+ * 64-bit integer result by passing {@code Long.class}, a (possibly deeply nested)
+ * list of {@code Longs, Strings, Vs} and possibly even {@link RedisException}s by
+ * passing {@code List.class}, or a single {@code V} by passing any other class.
+ *
+ * @param digest Lowercase hex string of script's SHA-1 digest.
+ * @param type Class of script return type.
+ * @param keys Redis keys to pass to script.
+ *
+ * @param <T> Script return type.
+ *
+ * @return The result of evaluating the script.
+ */
+ public <T> T evalsha(String digest, Class<T> type, K... keys) {
+ return await(c.evalsha(digest, type, keys));
+ }
+
public Boolean exists(K key) {
return await(c.exists(key));
}
@@ -396,6 +434,18 @@ public Long scard(K key) {
return await(c.scard(key));
}
+ public List<Boolean> scriptExists(String... digests) {
+ return await(c.scriptExists(digests));
+ }
+
+ public String scriptFlush() {
+ return await(c.scriptFlush());
+ }
+
+ public String scriptLoad(V script) {
+ return await(c.scriptLoad(script));
+ }
+
public Set<V> sdiff(K... keys) {
return await(c.sdiff(keys));
}
@@ -692,6 +742,17 @@ public void close() {
c.close();
}
+ /**
+ * Generate SHA-1 digest for the supplied script.
+ *
+ * @param script Lua script.
+ *
+ * @return Script digest as a lowercase hex string.
+ */
+ public String digest(V script) {
+ return c.digest(script);
+ }
+
@SuppressWarnings("unchecked")
private <T> T await(Future<T> future, long timeout, TimeUnit unit) {
Command<K, V, T> cmd = (Command<K, V, T>) future;
View
25 src/main/java/com/lambdaworks/redis/output/BooleanListOutput.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2011 - Will Glozer. All rights reserved.
+
+package com.lambdaworks.redis.output;
+
+import com.lambdaworks.redis.codec.RedisCodec;
+import com.lambdaworks.redis.protocol.CommandOutput;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@link java.util.List} of boolean output.
+ *
+ * @author Will Glozer
+ */
+public class BooleanListOutput<K, V> extends CommandOutput<K, V, List<Boolean>> {
+ public BooleanListOutput(RedisCodec<K, V> codec) {
+ super(codec, new ArrayList<Boolean>());
+ }
+
+ @Override
+ public void set(long integer) {
+ output.add((integer == 1) ? Boolean.TRUE : Boolean.FALSE);
+ }
+}
View
6 src/main/java/com/lambdaworks/redis/protocol/CommandKeyword.java
@@ -8,9 +8,9 @@
* @author Will Glozer
*/
public enum CommandKeyword {
- AFTER, AGGREGATE, ALPHA, ASC, BEFORE, BY, COUNT, DESC, ENCODING, IDLETIME, KILL,
- LEN, LIMIT, LIST, MAX, MIN, NO, NOSAVE, ONE, REFCOUNT, RESET, RESETSTAT, STORE,
- SUM, WEIGHTS, WITHSCORES;
+ AFTER, AGGREGATE, ALPHA, ASC, BEFORE, BY, COUNT, DESC, ENCODING, FLUSH,
+ IDLETIME, KILL, LEN, LIMIT, LIST, LOAD, MAX, MIN, NO, NOSAVE, ONE, REFCOUNT,
+ RESET, RESETSTAT, STORE, SUM, WEIGHTS, WITHSCORES;
public byte[] bytes;
View
6 src/main/java/com/lambdaworks/redis/protocol/CommandType.java
@@ -60,7 +60,11 @@
ZADD, ZCARD, ZCOUNT, ZINCRBY, ZINTERSTORE, ZRANGE, ZRANGEBYSCORE,
ZRANK, ZREM, ZREMRANGEBYRANK, ZREMRANGEBYSCORE, ZREVRANGE,
- ZREVRANGEBYSCORE, ZREVRANK, ZSCORE, ZUNIONSTORE;
+ ZREVRANGEBYSCORE, ZREVRANK, ZSCORE, ZUNIONSTORE,
+
+ // Scripting
+
+ EVAL, EVALSHA, SCRIPT;
public byte[] bytes;
View
68 src/test/java/com/lambdaworks/redis/ScriptingCommandTest.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2011 - Will Glozer. All rights reserved.
+
+package com.lambdaworks.redis;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public class ScriptingCommandTest extends AbstractCommandTest {
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void testName() throws Exception {
+ assertEquals(list(1L, "one", list(2L, "two", list(3L), 4L), "five"), redis.eval("return {1, 'one', {2, 'two', {3}, 4}, 'five'}", List.class));
+ System.out.printf("foo %s\n", redis.eval("return {{err='foo'}, 1, {2, {err='hi'}}}", List.class));
+ System.out.printf("foo %s\n", redis.eval("return {blah, 1}", List.class));
+ System.out.printf("foo %s\n", redis.evalsha("asdf", List.class));
+ System.out.printf("foo %s\n", redis.eval("return 1 + 1 == 4", Long.class));
+
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void eval() throws Exception {
+ assertEquals(2L, (long) redis.eval("return 1 + 1", Long.class));
+ assertEquals("one", redis.eval("return 'one'", String.class));
+ assertEquals(list(1L, "one", list(2L)), redis.eval("return {1, 'one', {2}}", List.class));
+ assertEquals("status", redis.eval("return {ok='status'}", String.class));
+ assertEquals(list(false), redis.eval("return 1 + 1 == 4", List.class));
+ exception.expectMessage("Oops!");
+ redis.eval("return {err='Oops!'}", String.class);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void evalsha() throws Exception {
+ redis.scriptFlush();
+ String script = "return 1 + 1";
+ String digest = redis.digest(script);
+ assertEquals(2L, (long) redis.eval(script, Long.class));
+ assertEquals(2L, (long) redis.evalsha(digest, Long.class));
+ exception.expectMessage("NOSCRIPT No matching script. Please use EVAL.");
+ redis.evalsha(redis.digest("return 1 + 1 == 4"), Long.class);
+ }
+
+ @Test
+ public void script() throws Exception {
+ assertEquals("OK", redis.scriptFlush());
+
+ String script1 = "return 1 + 1";
+ String digest1 = redis.digest(script1);
+ String script2 = "return 1 + 1 == 4";
+ String digest2 = redis.digest(script2);
+
+ assertEquals(list(false, false), redis.scriptExists(digest1, digest2));
+ assertEquals(digest1, redis.scriptLoad(script1));
+ assertEquals(2L, (long) redis.evalsha(digest1, Long.class));
+ assertEquals(list(true, false), redis.scriptExists(digest1, digest2));
+
+ assertEquals("OK", redis.scriptFlush());
+ assertEquals(list(false, false), redis.scriptExists(digest1, digest2));
+ }
+}

0 comments on commit e9f71f9

Please sign in to comment.