From b0c751ae91eadfdcce46e41f706d786e8da9766c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=86=B2?= Date: Fri, 21 Apr 2023 14:26:33 +0800 Subject: [PATCH 1/5] Add evalLua method with trying of evalsha in RedisAPI --- .../java/io/vertx/redis/client/RedisAPI.java | 67 +++++++++++++++++++ .../java/io/vertx/redis/client/util/Hash.java | 49 ++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/main/java/io/vertx/redis/client/util/Hash.java diff --git a/src/main/java/io/vertx/redis/client/RedisAPI.java b/src/main/java/io/vertx/redis/client/RedisAPI.java index 78c4cce8..f1c9a95a 100644 --- a/src/main/java/io/vertx/redis/client/RedisAPI.java +++ b/src/main/java/io/vertx/redis/client/RedisAPI.java @@ -20,7 +20,9 @@ import io.vertx.codegen.annotations.VertxGen; import io.vertx.core.Future; import io.vertx.redis.client.impl.RedisAPIImpl; +import io.vertx.redis.client.util.Hash; +import java.util.ArrayList; import java.util.List; import static io.vertx.codegen.annotations.GenIgnore.PERMITTED_TYPE; @@ -3165,4 +3167,69 @@ static RedisAPI api(RedisConnection connection) { */ @GenIgnore Future<@Nullable Response> send(Command cmd, String... args); + + /** + * Run redis command with same arguments as eval 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. + * + * @return Future response. + */ + default Future evalLua(List args) { + List shaArgs = new ArrayList<>(args); + shaArgs.set(0, Hash.sha1(shaArgs.get(0))); + + return evalsha(shaArgs) + .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 + return eval(args); + } + + // fail when failed cause not script missing + return Future.failedFuture(t); + }); + } + + /** + * evalLua with a specified luaScript argument. + */ + default Future evalLua(String luaScript, List args) { + List newArgs = new ArrayList<>(args.size() + 1); + + newArgs.add(luaScript); + newArgs.addAll(args); + + return evalLua(newArgs); + } + + /** + * evalLua with a specified arguments and with checking on them. + */ + default Future evalLua(String luaScript, int numkeys, List keys, List args) { + List allArgs = new ArrayList<>(); + + allArgs.add(luaScript); + allArgs.add(String.valueOf(numkeys)); + + if ((keys == null && numkeys != 0) || (keys != null && keys.size() != numkeys)) { + throw new IllegalArgumentException( + "numkeys " + numkeys + " and keys " + keys + " are not match"); + } + + if (keys != null && keys.size() != 0) { + allArgs.addAll(keys); + } + + if (args != null && args.size() != 0) { + allArgs.addAll(args); + } + + return evalLua(allArgs); + } } 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"); + } +} From 3af9c78e1261894ee9f1b48ecb2575e801077ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=86=B2?= Date: Fri, 21 Apr 2023 14:40:49 +0800 Subject: [PATCH 2/5] Add tests for evalLua --- .../io/vertx/redis/client/test/RedisTest.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) 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..f993de84 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,53 @@ 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(Arrays.asList(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 int numKeys = 1; + 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, numKeys, keys, args)) + .compose(_resp -> redis.script(Arrays.asList("flush"))) + .compose(_resp -> redis.evalLua(script, numKeys, keys, args)) + .onComplete(should.asyncAssertSuccess(r -> { + should.assertNotNull(r); + should.assertEquals("333", r.toString()); + test.complete(); + })); + }); + } } From 8aabb5f5add75898adedfd3837aa568785be4b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=86=B2?= Date: Fri, 28 Apr 2023 14:00:14 +0800 Subject: [PATCH 3/5] Add class LuaScript for creating pre-computed sha for evalLua --- .../java/io/vertx/redis/client/LuaScript.java | 62 ++++++++++ .../java/io/vertx/redis/client/RedisAPI.java | 110 +++++++++--------- .../io/vertx/redis/client/test/RedisTest.java | 2 +- 3 files changed, 120 insertions(+), 54 deletions(-) create mode 100644 src/main/java/io/vertx/redis/client/LuaScript.java 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..86c036c6 --- /dev/null +++ b/src/main/java/io/vertx/redis/client/LuaScript.java @@ -0,0 +1,62 @@ +/* + * 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; + + // pre-converted string of num of keys + private final String numKeys; + + public LuaScript(String lua, int numKeys) { + this.lua = lua; + this.sha = Hash.sha1(lua); + this.numKeys = String.valueOf(numKeys); + } + + /** + * @return the lua + */ + public String getLua() { + return lua; + } + + /** + * @return the sha + */ + public String getSha() { + return sha; + } + + /** + * @return the numKeys + */ + public String getNumKeys() { + return numKeys; + } +} diff --git a/src/main/java/io/vertx/redis/client/RedisAPI.java b/src/main/java/io/vertx/redis/client/RedisAPI.java index f1c9a95a..8752572b 100644 --- a/src/main/java/io/vertx/redis/client/RedisAPI.java +++ b/src/main/java/io/vertx/redis/client/RedisAPI.java @@ -20,9 +20,9 @@ import io.vertx.codegen.annotations.VertxGen; import io.vertx.core.Future; import io.vertx.redis.client.impl.RedisAPIImpl; -import io.vertx.redis.client.util.Hash; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import static io.vertx.codegen.annotations.GenIgnore.PERMITTED_TYPE; @@ -3169,67 +3169,71 @@ static RedisAPI api(RedisConnection connection) { Future<@Nullable Response> send(Command cmd, String... args); /** - * Run redis command with same arguments as eval 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. + * 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. */ - default Future evalLua(List args) { - List shaArgs = new ArrayList<>(args); - shaArgs.set(0, Hash.sha1(shaArgs.get(0))); - - return evalsha(shaArgs) - .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 - return eval(args); - } - - // fail when failed cause not script missing - return Future.failedFuture(t); - }); - } - - /** - * evalLua with a specified luaScript argument. + @GenIgnore + default Future evalLua(LuaScript luaScript, List args) { + // pass empty args if no args to avoid null check on args + List redisArgs = new ArrayList<>(args.size() + 2); + redisArgs.add(luaScript.getSha()); + redisArgs.add(luaScript.getNumKeys()); + 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. */ - default Future evalLua(String luaScript, List args) { - List newArgs = new ArrayList<>(args.size() + 1); + @GenIgnore + default Future evalLua(String lua, int numkeys, List keys, List args) { + LuaScript luaScript = new LuaScript(lua, numkeys); - newArgs.add(luaScript); - newArgs.addAll(args); + List allArgs = new ArrayList<>(); + allArgs.addAll(keys); + allArgs.addAll(args); - return evalLua(newArgs); + return evalLua(luaScript, allArgs); } /** - * evalLua with a specified arguments and with checking on them. + * 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. */ - default Future evalLua(String luaScript, int numkeys, List keys, List args) { - List allArgs = new ArrayList<>(); - - allArgs.add(luaScript); - allArgs.add(String.valueOf(numkeys)); - - if ((keys == null && numkeys != 0) || (keys != null && keys.size() != numkeys)) { - throw new IllegalArgumentException( - "numkeys " + numkeys + " and keys " + keys + " are not match"); - } - - if (keys != null && keys.size() != 0) { - allArgs.addAll(keys); - } - - if (args != null && args.size() != 0) { - allArgs.addAll(args); - } - - return evalLua(allArgs); + @GenIgnore + default Future evalLua(String lua, int numkeys, String... args) { + return evalLua(new LuaScript(lua, numkeys), Arrays.asList(args)); } } 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 f993de84..2b8efd6f 100644 --- a/src/test/java/io/vertx/redis/client/test/RedisTest.java +++ b/src/test/java/io/vertx/redis/client/test/RedisTest.java @@ -348,7 +348,7 @@ public void testEvalLua(TestContext should) { RedisAPI redis = RedisAPI.api(create.result()); - redis.evalLua(Arrays.asList(lua, "0", "hello", "world")).onComplete(should.asyncAssertSuccess(r -> { + redis.evalLua(lua, 0, "hello", "world").onComplete(should.asyncAssertSuccess(r -> { should.assertNotNull(r); should.assertEquals("world", r.toString()); test.complete(); From 70612ac7a73f9ed145a292456b250bc6d4c88163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=86=B2?= Date: Fri, 28 Apr 2023 16:29:51 +0800 Subject: [PATCH 4/5] Protect null for keys and args of evalLua --- src/main/java/io/vertx/redis/client/RedisAPI.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/vertx/redis/client/RedisAPI.java b/src/main/java/io/vertx/redis/client/RedisAPI.java index 8752572b..db444289 100644 --- a/src/main/java/io/vertx/redis/client/RedisAPI.java +++ b/src/main/java/io/vertx/redis/client/RedisAPI.java @@ -3220,8 +3220,12 @@ default Future evalLua(String lua, int numkeys, List keys, Lis LuaScript luaScript = new LuaScript(lua, numkeys); List allArgs = new ArrayList<>(); - allArgs.addAll(keys); - allArgs.addAll(args); + if (keys != null && keys.size() != 0) { + allArgs.addAll(keys); + } + if (args != null && args.size() != 0) { + allArgs.addAll(args); + } return evalLua(luaScript, allArgs); } From 424d90000fc7e8a916f07195b2a4a029415c9169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=86=B2?= Date: Fri, 28 Apr 2023 17:20:22 +0800 Subject: [PATCH 5/5] Move numkeys from RedisScript to arguments of evalLua --- .../java/io/vertx/redis/client/LuaScript.java | 15 ++------ .../java/io/vertx/redis/client/RedisAPI.java | 36 ++++++++++++++----- .../io/vertx/redis/client/test/RedisTest.java | 7 ++-- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/main/java/io/vertx/redis/client/LuaScript.java b/src/main/java/io/vertx/redis/client/LuaScript.java index 86c036c6..47e1324a 100644 --- a/src/main/java/io/vertx/redis/client/LuaScript.java +++ b/src/main/java/io/vertx/redis/client/LuaScript.java @@ -20,7 +20,7 @@ /** * LuaScript * - * Lua script with pre-computed sha hash. + *

Lua script with pre-computed sha hash. */ public class LuaScript { @@ -30,13 +30,9 @@ public class LuaScript { // pre-computed sha private final String sha; - // pre-converted string of num of keys - private final String numKeys; - - public LuaScript(String lua, int numKeys) { + public LuaScript(String lua) { this.lua = lua; this.sha = Hash.sha1(lua); - this.numKeys = String.valueOf(numKeys); } /** @@ -52,11 +48,4 @@ public String getLua() { public String getSha() { return sha; } - - /** - * @return the numKeys - */ - public String getNumKeys() { - return numKeys; - } } diff --git a/src/main/java/io/vertx/redis/client/RedisAPI.java b/src/main/java/io/vertx/redis/client/RedisAPI.java index db444289..4fef59ce 100644 --- a/src/main/java/io/vertx/redis/client/RedisAPI.java +++ b/src/main/java/io/vertx/redis/client/RedisAPI.java @@ -3185,10 +3185,9 @@ static RedisAPI api(RedisConnection connection) { */ @GenIgnore default Future evalLua(LuaScript luaScript, List args) { - // pass empty args if no args to avoid null check on args - List redisArgs = new ArrayList<>(args.size() + 2); + // pass empty args if no args, no null check + List redisArgs = new ArrayList<>(args.size() + 1); redisArgs.add(luaScript.getSha()); - redisArgs.add(luaScript.getNumKeys()); redisArgs.addAll(args); return evalsha(redisArgs) @@ -3214,15 +3213,30 @@ default Future evalLua(LuaScript luaScript, List args) { * * 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, int numkeys, List keys, List args) { - LuaScript luaScript = new LuaScript(lua, numkeys); + default Future evalLua(String lua, List keys, List args) { + LuaScript luaScript = new LuaScript(lua); List allArgs = new ArrayList<>(); - if (keys != null && keys.size() != 0) { + + 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); } @@ -3235,9 +3249,15 @@ default Future evalLua(String lua, int numkeys, List keys, Lis * * 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, int numkeys, String... args) { - return evalLua(new LuaScript(lua, numkeys), Arrays.asList(args)); + 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/test/java/io/vertx/redis/client/test/RedisTest.java b/src/test/java/io/vertx/redis/client/test/RedisTest.java index 2b8efd6f..2b2d3e73 100644 --- a/src/test/java/io/vertx/redis/client/test/RedisTest.java +++ b/src/test/java/io/vertx/redis/client/test/RedisTest.java @@ -348,7 +348,7 @@ public void testEvalLua(TestContext should) { RedisAPI redis = RedisAPI.api(create.result()); - redis.evalLua(lua, 0, "hello", "world").onComplete(should.asyncAssertSuccess(r -> { + redis.evalLua(lua, "0", "hello", "world").onComplete(should.asyncAssertSuccess(r -> { should.assertNotNull(r); should.assertEquals("world", r.toString()); test.complete(); @@ -359,7 +359,6 @@ public void testEvalLua(TestContext should) { @Test public void testEvalLua2(TestContext should) { final String script = "return ARGV[3]"; - final int numKeys = 1; final List keys = Arrays.asList("hello"); final List args = Arrays.asList("1", "22", "333"); final Async test = should.async(); @@ -373,9 +372,9 @@ public void testEvalLua2(TestContext should) { RedisAPI redis = RedisAPI.api(create.result()); redis.script(Arrays.asList("flush")) - .compose(_resp -> redis.evalLua(script, numKeys, keys, args)) + .compose(_resp -> redis.evalLua(script, keys, args)) .compose(_resp -> redis.script(Arrays.asList("flush"))) - .compose(_resp -> redis.evalLua(script, numKeys, keys, args)) + .compose(_resp -> redis.evalLua(script, keys, args)) .onComplete(should.asyncAssertSuccess(r -> { should.assertNotNull(r); should.assertEquals("333", r.toString());