diff --git a/src/main/java/io/vertx/redis/client/LuaScript.java b/src/main/java/io/vertx/redis/client/LuaScript.java new file mode 100644 index 00000000..47e1324a --- /dev/null +++ b/src/main/java/io/vertx/redis/client/LuaScript.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.redis.client; + +import io.vertx.redis.client.util.Hash; + +/** + * LuaScript + * + *

Lua script with pre-computed sha hash. + */ +public class LuaScript { + + // this lua script + private final String lua; + + // pre-computed sha + private final String sha; + + public LuaScript(String lua) { + this.lua = lua; + this.sha = Hash.sha1(lua); + } + + /** + * @return the lua + */ + public String getLua() { + return lua; + } + + /** + * @return the sha + */ + public String getSha() { + return sha; + } +} diff --git a/src/main/java/io/vertx/redis/client/RedisAPI.java b/src/main/java/io/vertx/redis/client/RedisAPI.java index 78c4cce8..4fef59ce 100644 --- a/src/main/java/io/vertx/redis/client/RedisAPI.java +++ b/src/main/java/io/vertx/redis/client/RedisAPI.java @@ -21,6 +21,8 @@ import io.vertx.core.Future; import io.vertx.redis.client.impl.RedisAPIImpl; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import static io.vertx.codegen.annotations.GenIgnore.PERMITTED_TYPE; @@ -3165,4 +3167,97 @@ static RedisAPI api(RedisConnection connection) { */ @GenIgnore Future<@Nullable Response> send(Command cmd, String... args); + + /** + * Run redis command eval lua with arguments but will try evalsha first, if + * evalsha failed with `NOSCRIPT` error, will retry with the script, so the + * next try evalsha will success as script has already been cached. + * + * Example: + * {@code + * final static LuaScript luaScript = new LuaScript("return ARGV[1]", 0); + * ... some codes ... + * evalLua(luaScript, Arrays.asList("hello")); + * } + * + * @param luaScript lua script with pre-computed sha hash + * @return Future response. + */ + @GenIgnore + default Future evalLua(LuaScript luaScript, List args) { + // pass empty args if no args, no null check + List redisArgs = new ArrayList<>(args.size() + 1); + redisArgs.add(luaScript.getSha()); + redisArgs.addAll(args); + + return evalsha(redisArgs) + .compose( + // directly use result when succeeded + result -> Future.succeededFuture(result), + t -> { + // load script and try ONCE again when failed cause script missing + if (t.getMessage().startsWith("NOSCRIPT")) { + // directly eval script and then script will be cached in redis, so + // evalhash will success next time + redisArgs.set(0, luaScript.getLua()); + return eval(redisArgs); + } + + // fail when failed cause not script missing + return Future.failedFuture(t); + }); + } + + /** + * evalLua with no pre-computed sha. + * + * NOTE: prefer to define a static variable with type of LuaScript and use the + * `evalLua(LuaScript, List)`, with this method, sha will always be computed. + * + * @param lua string of lua script + * @param keys list of keys, size of keys will be numkeys argument + * @param args list of args + * @return Future response. + */ + @GenIgnore + default Future evalLua(String lua, List keys, List args) { + LuaScript luaScript = new LuaScript(lua); + + List allArgs = new ArrayList<>(); + + int numKeys; + if (keys != null) { + numKeys = keys.size(); + } else { + numKeys = 0; + } + allArgs.add(String.valueOf(numKeys)); + + if (numKeys != 0) { + allArgs.addAll(keys); + } + + if (args != null && args.size() != 0) { + allArgs.addAll(args); + } + + return evalLua(luaScript, allArgs); + } + + /** + * evalLua with no pre-computed sha. + * + * NOTE: prefer to define a static variable with type of LuaScript and use the + * `evalLua(LuaScript, List)`, with this method, sha will always be computed. + * + * @param numkeys, keys, args. If args is empty, a numkeys = 0 will add by default + * @return Future response. + */ + @GenIgnore + default Future evalLua(String lua, String... args) { + if (args.length == 0) { + return evalLua(new LuaScript(lua), Arrays.asList("0")); + } + return evalLua(new LuaScript(lua), Arrays.asList(args)); + } } diff --git a/src/main/java/io/vertx/redis/client/util/Hash.java b/src/main/java/io/vertx/redis/client/util/Hash.java new file mode 100644 index 00000000..3fb6f728 --- /dev/null +++ b/src/main/java/io/vertx/redis/client/util/Hash.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.redis.client.util; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class Hash { + + private static String bytesToHex(byte[] hash) { + StringBuilder hexString = new StringBuilder(2 * hash.length); + for (int i = 0; i < hash.length; i++) { + String hex = Integer.toHexString(0xff & hash[i]); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + public static String hash(String input, String name) { + try { + MessageDigest digest = MessageDigest.getInstance(name); + byte[] encodedhash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + return bytesToHex(encodedhash); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static String sha1(String input) { + return hash(input, "SHA-1"); + } +} diff --git a/src/test/java/io/vertx/redis/client/test/RedisTest.java b/src/test/java/io/vertx/redis/client/test/RedisTest.java index ba75a579..2b2d3e73 100644 --- a/src/test/java/io/vertx/redis/client/test/RedisTest.java +++ b/src/test/java/io/vertx/redis/client/test/RedisTest.java @@ -334,4 +334,52 @@ public void testBatch2(TestContext should) { }) .onSuccess(responses -> should.fail("Commands are wrong")); } + + @Test + public void testEvalLua(TestContext should) { + final String lua = "return ARGV[2]"; + final Async test = should.async(); + + Redis + .createClient(rule.vertx(), + new RedisOptions().setConnectionString("redis://" + redis.getHost() + ":" + redis.getFirstMappedPort())) + .connect().onComplete(create -> { + should.assertTrue(create.succeeded()); + + RedisAPI redis = RedisAPI.api(create.result()); + + redis.evalLua(lua, "0", "hello", "world").onComplete(should.asyncAssertSuccess(r -> { + should.assertNotNull(r); + should.assertEquals("world", r.toString()); + test.complete(); + })); + }); + } + + @Test + public void testEvalLua2(TestContext should) { + final String script = "return ARGV[3]"; + final List keys = Arrays.asList("hello"); + final List args = Arrays.asList("1", "22", "333"); + final Async test = should.async(); + + Redis + .createClient(rule.vertx(), + new RedisOptions().setConnectionString("redis://" + redis.getHost() + ":" + redis.getFirstMappedPort())) + .connect().onComplete(create -> { + should.assertTrue(create.succeeded()); + + RedisAPI redis = RedisAPI.api(create.result()); + + redis.script(Arrays.asList("flush")) + .compose(_resp -> redis.evalLua(script, keys, args)) + .compose(_resp -> redis.script(Arrays.asList("flush"))) + .compose(_resp -> redis.evalLua(script, keys, args)) + .onComplete(should.asyncAssertSuccess(r -> { + should.assertNotNull(r); + should.assertEquals("333", r.toString()); + test.complete(); + })); + }); + } }