diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cc0a14c09..db6852682e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,6 +122,7 @@ * Java, Node, Python: Change BITCOUNT end param to optional (Valkey-8) ([#2248](https://github.com/valkey-io/valkey-glide/pull/2248)) * Java, Node, Python: Add NOSCORES option to ZSCAN & NOVALUES option to HSCAN (Valkey-8) ([#2174](https://github.com/valkey-io/valkey-glide/pull/2174)) * Node: Add SCAN command ([#2257](https://github.com/valkey-io/valkey-glide/pull/2257)) +* Java: Add Script commands ([#2261](https://github.com/valkey-io/valkey-glide/pull/2261)) #### Breaking Changes * Java: Update INFO command ([#2274](https://github.com/valkey-io/valkey-glide/pull/2274)) diff --git a/java/client/src/main/java/glide/api/GlideClient.java b/java/client/src/main/java/glide/api/GlideClient.java index ddd7686a66..88d7f4aac7 100644 --- a/java/client/src/main/java/glide/api/GlideClient.java +++ b/java/client/src/main/java/glide/api/GlideClient.java @@ -28,6 +28,9 @@ import static command_request.CommandRequestOuterClass.RequestType.Ping; import static command_request.CommandRequestOuterClass.RequestType.RandomKey; import static command_request.CommandRequestOuterClass.RequestType.Scan; +import static command_request.CommandRequestOuterClass.RequestType.ScriptExists; +import static command_request.CommandRequestOuterClass.RequestType.ScriptFlush; +import static command_request.CommandRequestOuterClass.RequestType.ScriptKill; import static command_request.CommandRequestOuterClass.RequestType.Select; import static command_request.CommandRequestOuterClass.RequestType.Time; import static command_request.CommandRequestOuterClass.RequestType.UnWatch; @@ -499,4 +502,32 @@ public CompletableFuture scan( GlideString[] arguments = new ArgsBuilder().add(cursor).add(options.toArgs()).toArray(); return commandManager.submitNewCommand(Scan, arguments, this::handleArrayResponseBinary); } + + @Override + public CompletableFuture scriptExists(@NonNull String[] sha1s) { + return commandManager.submitNewCommand( + ScriptExists, sha1s, response -> castArray(handleArrayResponse(response), Boolean.class)); + } + + @Override + public CompletableFuture scriptExists(@NonNull GlideString[] sha1s) { + return commandManager.submitNewCommand( + ScriptExists, sha1s, response -> castArray(handleArrayResponse(response), Boolean.class)); + } + + @Override + public CompletableFuture scriptFlush() { + return commandManager.submitNewCommand(ScriptFlush, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture scriptFlush(@NonNull FlushMode flushMode) { + return commandManager.submitNewCommand( + ScriptFlush, new String[] {flushMode.toString()}, this::handleStringResponse); + } + + @Override + public CompletableFuture scriptKill() { + return commandManager.submitNewCommand(ScriptKill, new String[0], this::handleStringResponse); + } } diff --git a/java/client/src/main/java/glide/api/GlideClusterClient.java b/java/client/src/main/java/glide/api/GlideClusterClient.java index 84bac03c5a..056fe49b15 100644 --- a/java/client/src/main/java/glide/api/GlideClusterClient.java +++ b/java/client/src/main/java/glide/api/GlideClusterClient.java @@ -30,6 +30,9 @@ import static command_request.CommandRequestOuterClass.RequestType.PubSubSNumSub; import static command_request.CommandRequestOuterClass.RequestType.RandomKey; import static command_request.CommandRequestOuterClass.RequestType.SPublish; +import static command_request.CommandRequestOuterClass.RequestType.ScriptExists; +import static command_request.CommandRequestOuterClass.RequestType.ScriptFlush; +import static command_request.CommandRequestOuterClass.RequestType.ScriptKill; import static command_request.CommandRequestOuterClass.RequestType.Time; import static command_request.CommandRequestOuterClass.RequestType.UnWatch; import static glide.api.commands.ServerManagementCommands.VERSION_VALKEY_API; @@ -52,8 +55,11 @@ import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; import glide.api.models.GlideString; +import glide.api.models.Script; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions.Section; +import glide.api.models.commands.ScriptArgOptions; +import glide.api.models.commands.ScriptArgOptionsGlideString; import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.commands.scan.ClusterScanCursor; import glide.api.models.commands.scan.ScanOptions; @@ -65,9 +71,11 @@ import glide.utils.ArgsBuilder; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.NonNull; import response.ResponseOuterClass.Response; @@ -946,6 +954,106 @@ public CompletableFuture functionKill(@NonNull Route route) { FunctionKill, new String[0], route, this::handleStringResponse); } + @Override + public CompletableFuture invokeScript(@NonNull Script script, @NonNull Route route) { + if (script.getBinaryOutput()) { + return commandManager.submitScript( + script, List.of(), route, this::handleBinaryObjectOrNullResponse); + } else { + return commandManager.submitScript( + script, List.of(), route, this::handleObjectOrNullResponse); + } + } + + @Override + public CompletableFuture invokeScript( + @NonNull Script script, @NonNull ScriptArgOptions options, @NonNull Route route) { + return commandManager.submitScript( + script, + options.getArgs().stream().map(GlideString::gs).collect(Collectors.toList()), + route, + script.getBinaryOutput() + ? this::handleBinaryObjectOrNullResponse + : this::handleObjectOrNullResponse); + } + + @Override + public CompletableFuture invokeScript( + @NonNull Script script, @NonNull ScriptArgOptionsGlideString options, @NonNull Route route) { + return commandManager.submitScript( + script, + options.getArgs(), + route, + script.getBinaryOutput() + ? this::handleBinaryObjectOrNullResponse + : this::handleObjectOrNullResponse); + } + + @Override + public CompletableFuture scriptExists(@NonNull String[] sha1s) { + return commandManager.submitNewCommand( + ScriptExists, sha1s, response -> castArray(handleArrayResponse(response), Boolean.class)); + } + + @Override + public CompletableFuture scriptExists(@NonNull GlideString[] sha1s) { + return commandManager.submitNewCommand( + ScriptExists, sha1s, response -> castArray(handleArrayResponse(response), Boolean.class)); + } + + @Override + public CompletableFuture scriptExists(@NonNull String[] sha1s, @NonNull Route route) { + return commandManager.submitNewCommand( + ScriptExists, + sha1s, + route, + response -> castArray(handleArrayResponse(response), Boolean.class)); + } + + @Override + public CompletableFuture scriptExists( + @NonNull GlideString[] sha1s, @NonNull Route route) { + return commandManager.submitNewCommand( + ScriptExists, + sha1s, + route, + response -> castArray(handleArrayResponse(response), Boolean.class)); + } + + @Override + public CompletableFuture scriptFlush() { + return commandManager.submitNewCommand(ScriptFlush, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture scriptFlush(@NonNull FlushMode flushMode) { + return commandManager.submitNewCommand( + ScriptFlush, new String[] {flushMode.toString()}, this::handleStringResponse); + } + + @Override + public CompletableFuture scriptFlush(@NonNull Route route) { + return commandManager.submitNewCommand( + ScriptFlush, new String[0], route, this::handleStringResponse); + } + + @Override + public CompletableFuture scriptFlush(@NonNull FlushMode flushMode, @NonNull Route route) { + return commandManager.submitNewCommand( + ScriptFlush, new String[] {flushMode.toString()}, route, this::handleStringResponse); + } + + @Override + public CompletableFuture scriptKill() { + return commandManager.submitNewCommand(ScriptKill, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture scriptKill(@NonNull Route route) { + return commandManager.submitNewCommand( + ScriptKill, new String[0], route, this::handleStringResponse); + } + @Override public CompletableFuture>>> functionStats() { return commandManager.submitNewCommand( diff --git a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java index 6d60c2044c..a55c1ef1a8 100644 --- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java @@ -2,11 +2,8 @@ package glide.api.commands; import glide.api.models.GlideString; -import glide.api.models.Script; import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.RestoreOptions; -import glide.api.models.commands.ScriptOptions; -import glide.api.models.commands.ScriptOptionsGlideString; import glide.api.models.commands.SortOptions; import glide.api.models.commands.SortOptionsBinary; import glide.api.models.configuration.ReadFrom; @@ -622,80 +619,6 @@ CompletableFuture pexpireAt( */ CompletableFuture pexpiretime(GlideString key); - // TODO move invokeScript to ScriptingAndFunctionsBaseCommands - // TODO add note to invokeScript about routing on cluster client - /** - * Invokes a Lua script.
- * This method simplifies the process of invoking scripts on the server by using an object that - * represents a Lua script. The script loading and execution will all be handled internally. If - * the script has not already been loaded, it will be loaded automatically using the - * SCRIPT LOAD command. After that, it will be invoked using the EVALSHA - * command. - * - * @see SCRIPT LOAD and EVALSHA for details. - * @param script The Lua script to execute. - * @return a value that depends on the script that was executed. - * @example - *
{@code
-     * try(Script luaScript = new Script("return 'Hello'", false)) {
-     *     String result = (String) client.invokeScript(luaScript).get();
-     *     assert result.equals("Hello");
-     * }
-     * }
- */ - CompletableFuture invokeScript(Script script); - - /** - * Invokes a Lua script with its keys and arguments.
- * This method simplifies the process of invoking scripts on the server by using an object that - * represents a Lua script. The script loading, argument preparation, and execution will all be - * handled internally. If the script has not already been loaded, it will be loaded automatically - * using the SCRIPT LOAD command. After that, it will be invoked using the - * EVALSHA command. - * - * @see SCRIPT LOAD and EVALSHA for details. - * @param script The Lua script to execute. - * @param options The script option that contains keys and arguments for the script. - * @return a value that depends on the script that was executed. - * @example - *
{@code
-     * try(Script luaScript = new Script("return { KEYS[1], ARGV[1] }", false)) {
-     *     ScriptOptions scriptOptions = ScriptOptions.builder().key("foo").arg("bar").build();
-     *     Object[] result = (Object[]) client.invokeScript(luaScript, scriptOptions).get();
-     *     assert result[0].equals("foo");
-     *     assert result[1].equals("bar");
-     * }
-     * }
- */ - CompletableFuture invokeScript(Script script, ScriptOptions options); - - /** - * Invokes a Lua script with its keys and arguments.
- * This method simplifies the process of invoking scripts on the server by using an object that - * represents a Lua script. The script loading, argument preparation, and execution will all be - * handled internally. If the script has not already been loaded, it will be loaded automatically - * using the SCRIPT LOAD command. After that, it will be invoked using the - * EVALSHA command. - * - * @see SCRIPT LOAD and EVALSHA for details. - * @param script The Lua script to execute. - * @param options The script option that contains keys and arguments for the script. - * @return a value that depends on the script that was executed. - * @example - *
{@code
-     * try(Script luaScript = new Script(gs("return { KEYS[1], ARGV[1] }", true))) {
-     *     ScriptOptionsGlideString scriptOptions = ScriptOptionsGlideString.builder().key(gs("foo")).arg(gs("bar")).build();
-     *     Object[] result = (Object[]) client.invokeScript(luaScript, scriptOptions).get();
-     *     assert result[0].equals(gs("foo"));
-     *     assert result[1].equals(gs("bar"));
-     * }
-     * }
- */ - CompletableFuture invokeScript(Script script, ScriptOptionsGlideString options); - /** * Returns the remaining time to live of key that has a timeout, in milliseconds. * diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsBaseCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsBaseCommands.java index 798dbedf30..e703db8c59 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsBaseCommands.java @@ -2,6 +2,9 @@ package glide.api.commands; import glide.api.models.GlideString; +import glide.api.models.Script; +import glide.api.models.commands.ScriptOptions; +import glide.api.models.commands.ScriptOptionsGlideString; import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; @@ -160,4 +163,88 @@ CompletableFuture fcallReadOnly( * } */ CompletableFuture scriptShow(GlideString sha1); + + /** + * Invokes a Lua script.
+ * This method simplifies the process of invoking scripts on the server by using an object that + * represents a Lua script. The script loading and execution will all be handled internally. If + * the script has not already been loaded, it will be loaded automatically using the + * SCRIPT LOAD command. After that, it will be invoked using the EVALSHA + * command. + * + * @see SCRIPT LOAD and EVALSHA for details. + * @param script The Lua script to execute. + * @return A value that depends on the script that was executed. + * @example + *
{@code
+     * try(Script luaScript = new Script("return 'Hello'", false)) {
+     *     String result = (String) client.invokeScript(luaScript).get();
+     *     assert result.equals("Hello");
+     * }
+     * }
+ */ + CompletableFuture invokeScript(Script script); + + /** + * Invokes a Lua script with its keys and arguments.
+ * This method simplifies the process of invoking scripts on the server by using an object that + * represents a Lua script. The script loading, argument preparation, and execution will all be + * handled internally. If the script has not already been loaded, it will be loaded automatically + * using the SCRIPT LOAD command. After that, it will be invoked using the + * EVALSHA command. + * + * @apiNote When in cluster mode + *
    + *
  • all keys in options must map to the same hash slot. + *
  • if no keys are given, command will be routed to a random primary node. + *
+ * + * @see SCRIPT LOAD and EVALSHA for details. + * @param script The Lua script to execute. + * @param options The script option that contains keys and arguments for the script. + * @return A value that depends on the script that was executed. + * @example + *
{@code
+     * try(Script luaScript = new Script("return { KEYS[1], ARGV[1] }", false)) {
+     *     ScriptOptions scriptOptions = ScriptOptions.builder().key("foo").arg("bar").build();
+     *     Object[] result = (Object[]) client.invokeScript(luaScript, scriptOptions).get();
+     *     assert result[0].equals("foo");
+     *     assert result[1].equals("bar");
+     * }
+     * }
+ */ + CompletableFuture invokeScript(Script script, ScriptOptions options); + + /** + * Invokes a Lua script with its keys and arguments.
+ * This method simplifies the process of invoking scripts on the server by using an object that + * represents a Lua script. The script loading, argument preparation, and execution will all be + * handled internally. If the script has not already been loaded, it will be loaded automatically + * using the SCRIPT LOAD command. After that, it will be invoked using the + * EVALSHA command. + * + * @apiNote When in cluster mode + *
    + *
  • all keys in options must map to the same hash slot. + *
  • if no keys are given, command will be routed to a random primary node. + *
+ * + * @see SCRIPT LOAD and EVALSHA for details. + * @param script The Lua script to execute. + * @param options The script option that contains keys and arguments for the script. + * @return A value that depends on the script that was executed. + * @example + *
{@code
+     * try(Script luaScript = new Script(gs("return { KEYS[1], ARGV[1] }", true))) {
+     *     ScriptOptionsGlideString scriptOptions = ScriptOptionsGlideString.builder().key(gs("foo")).arg(gs("bar")).build();
+     *     Object[] result = (Object[]) client.invokeScript(luaScript, scriptOptions).get();
+     *     assert result[0].equals(gs("foo"));
+     *     assert result[1].equals(gs("bar"));
+     * }
+     * }
+ */ + CompletableFuture invokeScript(Script script, ScriptOptionsGlideString options); } diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java index 0c4a9a891f..254f1208f7 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java @@ -3,7 +3,10 @@ import glide.api.models.ClusterValue; import glide.api.models.GlideString; +import glide.api.models.Script; import glide.api.models.commands.FlushMode; +import glide.api.models.commands.ScriptArgOptions; +import glide.api.models.commands.ScriptArgOptionsGlideString; import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.configuration.ReadFrom; import glide.api.models.configuration.RequestRoutingConfiguration.Route; @@ -1029,4 +1032,248 @@ CompletableFuture> fcallReadOnly( */ CompletableFuture>>> functionStatsBinary( Route route); + + /** + * Invokes a Lua script.
+ * This method simplifies the process of invoking scripts on the server by using an object that + * represents a Lua script. The script loading and execution will all be handled internally. If + * the script has not already been loaded, it will be loaded automatically using the + * SCRIPT LOAD command. After that, it will be invoked using the EVALSHA + * command. + * + * @see SCRIPT LOAD and EVALSHA for details. + * @param script The Lua script to execute. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A value that depends on the script that was executed. + * @example + *
{@code
+     * try(Script luaScript = new Script("return 'Hello'", false)) {
+     *     String result = (String) client.invokeScript(luaScript).get();
+     *     assert result.equals("Hello");
+     * }
+     * }
+ */ + CompletableFuture invokeScript(Script script, Route route); + + /** + * Invokes a Lua script with its keys and arguments.
+ * This method simplifies the process of invoking scripts on the server by using an object that + * represents a Lua script. The script loading, argument preparation, and execution will all be + * handled internally. If the script has not already been loaded, it will be loaded automatically + * using the SCRIPT LOAD command. After that, it will be invoked using the + * EVALSHA command. + * + * @see SCRIPT LOAD and EVALSHA for details. + * @param script The Lua script to execute. + * @param options The script option that contains the non-key arguments for the script. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A value that depends on the script that was executed. + * @example + *
{@code
+     * try(Script luaScript = new Script("return { ARGV[1] }", false)) {
+     *     ScriptArgOptions scriptArgOptions = ScriptArgOptions.builder().arg("abc").build();
+     *     Object[] result = (Object[]) client.invokeScript(luaScript, scriptOptions, ALL_PRIMARIES).get();
+     *     assert result[0].equals("abc");
+     * }
+     * }
+ */ + CompletableFuture invokeScript(Script script, ScriptArgOptions options, Route route); + + /** + * Invokes a Lua script with its keys and arguments.
+ * This method simplifies the process of invoking scripts on the server by using an object that + * represents a Lua script. The script loading, argument preparation, and execution will all be + * handled internally. If the script has not already been loaded, it will be loaded automatically + * using the SCRIPT LOAD command. After that, it will be invoked using the + * EVALSHA command. + * + * @see SCRIPT LOAD and EVALSHA for details. + * @param script The Lua script to execute. + * @param options The script option that contains the non-key arguments for the script. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A value that depends on the script that was executed. + * @example + *
{@code
+     * try(Script luaScript = new Script(gs("return { ARGV[1] }", true))) {
+     *     ScriptArgOptionsGlideString options = ScriptArgOptions.builder().arg(gs("abc")).build();
+     *     Object[] result = (Object[]) client.invokeScript(luaScript, options, ALL_PRIMARIES).get();
+     *     assert result[0].equals(gs("abc"));
+     * }
+     * }
+ */ + CompletableFuture invokeScript( + Script script, ScriptArgOptionsGlideString options, Route route); + + /** + * Checks existence of scripts in the script cache by their SHA1 digest.
+ * The command will be routed to all primary nodes. + * + * @see SCRIPT EXISTS for details. + * @param sha1s The Lua script to execute. + * @return An array of boolean values indicating the existence of each script. + * @example + *
{@code
+     * try(Script luaScript = new Script("return { KEYS[1], ARGV[1] }", true)) {
+     *     client.invokeScript(luaScript).get();
+     *     Boolean[] result = client.scriptExists(new String[]{luaScript.getHash()});
+     *     assert result[0].equals(true);
+     * }
+     * }
+ */ + CompletableFuture scriptExists(String[] sha1s); + + /** + * Checks existence of scripts in the script cache by their SHA1 digest.
+ * The command will be routed to all primary nodes. + * + * @see SCRIPT EXISTS for details. + * @param sha1s The Lua script to execute. + * @return An array of boolean values indicating the existence of each script. + * @example + *
{@code
+     * try(Script luaScript = new Script(gs("return { KEYS[1], ARGV[1] }", true))) {
+     *     client.invokeScript(luaScript).get();
+     *     Boolean[] result = client.scriptExists(new String[]{luaScript.getHash()});
+     *     assert result[0].equals(true);
+     * }
+     * }
+ */ + CompletableFuture scriptExists(GlideString[] sha1s); + + /** + * Checks existence of scripts in the script cache by their SHA1 digest. + * + * @see SCRIPT EXISTS for details. + * @param sha1s The Lua script to execute. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return An array of boolean values indicating the existence of each script. + * @example + *
{@code
+     * try(Script luaScript = new Script("return { KEYS[1], ARGV[1] }", true)) {
+     *     client.invokeScript(luaScript).get();
+     *     Boolean[] result = client.scriptExists(new String[]{luaScript.getHash()});
+     *     assert result[0].equals(true);
+     * }
+     * }
+ */ + CompletableFuture scriptExists(String[] sha1s, Route route); + + /** + * Checks existence of scripts in the script cache by their SHA1 digest. + * + * @see SCRIPT EXISTS for details. + * @param sha1s The Lua script to execute. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return An array of boolean values indicating the existence of each script. + * @example + *
{@code
+     * try(Script luaScript = new Script(gs("return { KEYS[1], ARGV[1] }", true))) {
+     *     client.invokeScript(luaScript).get();
+     *     Boolean[] result = client.scriptExists(new String[]{luaScript.getHash()});
+     *     assert result[0].equals(true);
+     * }
+     * }
+ */ + CompletableFuture scriptExists(GlideString[] sha1s, Route route); + + /** + * Flushes the Lua scripts cache.
+ * The command will be routed to all nodes. + * + * @see SCRIPT FLUSH for details. + * @return A simple "OK" response. + * @example + *
{@code
+     * String result = client.scriptFlush();
+     * assert "OK".equals(result);
+     * }
+ */ + CompletableFuture scriptFlush(); + + /** + * Flushes the Lua scripts cache.
+ * The command will be routed to all nodes. + * + * @see SCRIPT FLUSH for details. + * @param flushMode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return A simple "OK" response. + * @example + *
{@code
+     * String result = client.scriptFlush(ASYNC);
+     * assert "OK".equals(result);
+     * }
+ */ + CompletableFuture scriptFlush(FlushMode flushMode); + + /** + * Flushes the Lua scripts cache. + * + * @see SCRIPT flush for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A simple "OK" response. + * @example + *
{@code
+     * String result = client.scriptFlush(ALL_PRIMARIES);
+     * assert "OK".equals(result);
+     * }
+ */ + CompletableFuture scriptFlush(Route route); + + /** + * Flushes the Lua scripts cache. + * + * @see SCRIPT flush for details. + * @param flushMode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A simple "OK" response. + * @example + *
{@code
+     * String result = client.scriptFlush(ASYNC, ALL_PRIMARIES);
+     * assert "OK".equals(result);
+     * }
+ */ + CompletableFuture scriptFlush(FlushMode flushMode, Route route); + + /** + * Kill the currently executing Lua script, assuming no write operation was yet performed by the + * script.
+ * The command will be routed to all nodes. + * + * @see SCRIPT KILL for details. + * @return A simple "OK" response. + * @example + *
{@code
+     * String result = client.scriptKill();
+     * assert "OK".equals(result);
+     * }
+ */ + CompletableFuture scriptKill(); + + /** + * Kills the currently executing Lua script, assuming no write operation was yet performed by the + * script. + * + * @see SCRIPT KILL for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A simple "OK" response. + * @example + *
{@code
+     * String result = client.scriptKill(RANDOM);
+     * assert "OK".equals(result);
+     * }
+ */ + CompletableFuture scriptKill(Route route); } diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java index 7cb5bb3f36..6dfbeb8671 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java @@ -413,4 +413,80 @@ CompletableFuture[]> functionListBinary( * } */ CompletableFuture>>> functionStatsBinary(); + + /** + * Checks existence of scripts in the script cache by their SHA1 digest. + * + * @see SCRIPT EXISTS for details. + * @param sha1s The Lua script to execute. + * @return An array of boolean values indicating the existence of each script. + * @example + *
{@code
+     * try(Script luaScript = new Script("return { KEYS[1], ARGV[1] }", true)) {
+     *     client.invokeScript(luaScript).get();
+     *     Boolean[] result = client.scriptExists(new String[]{luaScript.getHash()});
+     *     assert result[0].equals(true);
+     * }
+     * }
+ */ + CompletableFuture scriptExists(String[] sha1s); + + /** + * Checks existence of scripts in the script cache by their SHA1 digest. + * + * @see SCRIPT EXISTS for details. + * @param sha1s The Lua script to execute. + * @return An array of boolean values indicating the existence of each script. + * @example + *
{@code
+     * try(Script luaScript = new Script(gs("return { KEYS[1], ARGV[1] }", true))) {
+     *     client.invokeScript(luaScript).get();
+     *     Boolean[] result = client.scriptExists(new String[]{luaScript.getHash()});
+     *     assert result[0].equals(true);
+     * }
+     * }
+ */ + CompletableFuture scriptExists(GlideString[] sha1s); + + /** + * Flushes the Lua scripts cache. + * + * @see SCRIPT FLUSH for details. + * @return A simple "OK" response. + * @example + *
{@code
+     * String result = client.scriptFlush();
+     * assert "OK".equals(result);
+     * }
+ */ + CompletableFuture scriptFlush(); + + /** + * Flushes the Lua scripts cache. + * + * @see SCRIPT FLUSH for details. + * @param flushMode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return A simple "OK" response. + * @example + *
{@code
+     * String result = client.scriptFlush(ASYNC);
+     * assert "OK".equals(result);
+     * }
+ */ + CompletableFuture scriptFlush(FlushMode flushMode); + + /** + * Kill the currently executing Lua script, assuming no write operation was yet performed by the + * script. + * + * @see SCRIPT KILL for details. + * @return A simple "OK" response. + * @example + *
{@code
+     * String result = client.scriptKill();
+     * assert "OK".equals(result);
+     * }
+ */ + CompletableFuture scriptKill(); } diff --git a/java/client/src/main/java/glide/api/models/commands/FlushMode.java b/java/client/src/main/java/glide/api/models/commands/FlushMode.java index ff7ea4c812..8e2437bf5f 100644 --- a/java/client/src/main/java/glide/api/models/commands/FlushMode.java +++ b/java/client/src/main/java/glide/api/models/commands/FlushMode.java @@ -20,6 +20,8 @@ *
  • FUNCTION FLUSH command implemented by {@link * GlideClient#functionFlush(FlushMode)}, {@link GlideClusterClient#functionFlush(FlushMode)}, * and {@link GlideClusterClient#functionFlush(FlushMode, Route)}. + *
  • SCRIPT FLUSH command implemented by {@link GlideClient#scriptFlush(FlushMode)} + * and {@link GlideClusterClient#scriptFlush(FlushMode, Route)} * * * @see flushall, valkey.io + */ +@SuperBuilder +public class ScriptArgOptions { + + /** The arguments for the script. */ + @Singular @Getter private final List args; +} diff --git a/java/client/src/main/java/glide/api/models/commands/ScriptArgOptionsGlideString.java b/java/client/src/main/java/glide/api/models/commands/ScriptArgOptionsGlideString.java new file mode 100644 index 0000000000..3aea503124 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/ScriptArgOptionsGlideString.java @@ -0,0 +1,23 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.ScriptingAndFunctionsClusterCommands; +import glide.api.models.GlideString; +import glide.api.models.Script; +import java.util.List; +import lombok.Getter; +import lombok.Singular; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments for {@link ScriptingAndFunctionsClusterCommands#invokeScript(Script, + * ScriptArgOptionsGlideString)} command. + * + * @see valkey.io + */ +@SuperBuilder +public class ScriptArgOptionsGlideString { + + /** The arguments for the script. */ + @Singular @Getter private final List args; +} diff --git a/java/client/src/main/java/glide/api/models/commands/ScriptOptions.java b/java/client/src/main/java/glide/api/models/commands/ScriptOptions.java index dbdba5a461..3ea43210b0 100644 --- a/java/client/src/main/java/glide/api/models/commands/ScriptOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/ScriptOptions.java @@ -1,24 +1,22 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.commands; -import glide.api.commands.GenericBaseCommands; +import glide.api.commands.ScriptingAndFunctionsBaseCommands; import glide.api.models.Script; import java.util.List; -import lombok.Builder; import lombok.Getter; import lombok.Singular; +import lombok.experimental.SuperBuilder; /** - * Optional arguments for {@link GenericBaseCommands#invokeScript(Script, ScriptOptions)} command. + * Optional arguments for {@link ScriptingAndFunctionsBaseCommands#invokeScript(Script, + * ScriptOptions)} command. * * @see valkey.io */ -@Builder -public final class ScriptOptions { +@SuperBuilder +public final class ScriptOptions extends ScriptArgOptions { /** The keys that are used in the script. */ @Singular @Getter private final List keys; - - /** The arguments for the script. */ - @Singular @Getter private final List args; } diff --git a/java/client/src/main/java/glide/api/models/commands/ScriptOptionsGlideString.java b/java/client/src/main/java/glide/api/models/commands/ScriptOptionsGlideString.java index 50535904f4..61847f3872 100644 --- a/java/client/src/main/java/glide/api/models/commands/ScriptOptionsGlideString.java +++ b/java/client/src/main/java/glide/api/models/commands/ScriptOptionsGlideString.java @@ -1,26 +1,23 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.commands; -import glide.api.commands.GenericBaseCommands; +import glide.api.commands.ScriptingAndFunctionsBaseCommands; import glide.api.models.GlideString; import glide.api.models.Script; import java.util.List; -import lombok.Builder; import lombok.Getter; import lombok.Singular; +import lombok.experimental.SuperBuilder; /** - * Optional arguments for {@link GenericBaseCommands#invokeScript(Script, ScriptOptionsGlideString)} - * command. + * Optional arguments for {@link ScriptingAndFunctionsBaseCommands#invokeScript(Script, + * ScriptOptionsGlideString)} command. * * @see valkey.io */ -@Builder -public class ScriptOptionsGlideString { +@SuperBuilder +public class ScriptOptionsGlideString extends ScriptArgOptionsGlideString { /** The keys that are used in the script. */ @Singular @Getter private final List keys; - - /** The arguments for the script. */ - @Singular @Getter private final List args; } diff --git a/java/client/src/main/java/glide/managers/CommandManager.java b/java/client/src/main/java/glide/managers/CommandManager.java index c934883bd1..639e258d81 100644 --- a/java/client/src/main/java/glide/managers/CommandManager.java +++ b/java/client/src/main/java/glide/managers/CommandManager.java @@ -166,6 +166,24 @@ public CompletableFuture submitScript( return submitCommandToChannel(command, responseHandler); } + /** + * Build a Script (by hash) request with route to send to Valkey. + * + * @param script Lua script hash object + * @param args The arguments for the script + * @param responseHandler The handler for the response object + * @return A result promise of type T + */ + public CompletableFuture submitScript( + Script script, + List args, + Route route, + GlideExceptionCheckedFunction responseHandler) { + + CommandRequest.Builder command = prepareScript(script, args, route); + return submitCommandToChannel(command, responseHandler); + } + /** * Build a Cluster Transaction and send. * @@ -322,6 +340,21 @@ protected CommandRequest.Builder prepareScript( .build()); } + /** + * Build a protobuf Script Invoke request with route. + * + * @param script Valkey Script + * @param args args for the Script + * @param route route specified for the Script Invoke request + * @return An uncompleted request. {@link CallbackDispatcher} is responsible to complete it by + * adding a callback id. + */ + protected CommandRequest.Builder prepareScript( + Script script, List args, Route route) { + CommandRequest.Builder builder = prepareScript(script, List.of(), args); + return prepareCommandRequestRoute(builder, route); + } + /** * Build a protobuf transaction request object with routing options. * diff --git a/java/client/src/test/java/glide/api/GlideClientTest.java b/java/client/src/test/java/glide/api/GlideClientTest.java index f9aa61daeb..92decc27d5 100644 --- a/java/client/src/test/java/glide/api/GlideClientTest.java +++ b/java/client/src/test/java/glide/api/GlideClientTest.java @@ -136,6 +136,9 @@ import static command_request.CommandRequestOuterClass.RequestType.SUnion; import static command_request.CommandRequestOuterClass.RequestType.SUnionStore; import static command_request.CommandRequestOuterClass.RequestType.Scan; +import static command_request.CommandRequestOuterClass.RequestType.ScriptExists; +import static command_request.CommandRequestOuterClass.RequestType.ScriptFlush; +import static command_request.CommandRequestOuterClass.RequestType.ScriptKill; import static command_request.CommandRequestOuterClass.RequestType.ScriptShow; import static command_request.CommandRequestOuterClass.RequestType.Select; import static command_request.CommandRequestOuterClass.RequestType.SetBit; @@ -15672,4 +15675,113 @@ public void xinfoStreamFull_glidestring_with_count_returns_success() { assertEquals(testResponse, response); assertEquals(summary, payload); } + + @SneakyThrows + @Test + public void scriptExists_returns_success() { + // setup + String hash = UUID.randomUUID().toString(); + String[] sha1s = {hash}; + Boolean[] value = {true}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ScriptExists), eq(sha1s), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scriptExists(sha1s); + Boolean[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scriptExists_binary_returns_success() { + // setup + GlideString hash = gs(UUID.randomUUID().toString()); + GlideString[] sha1s = {hash}; + Boolean[] value = {true}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ScriptExists), eq(sha1s), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scriptExists(sha1s); + Boolean[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scriptFlush_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ScriptFlush), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scriptFlush(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void scriptFlush_with_mode_returns_success() { + // setup + String[] args = new String[] {ASYNC.toString()}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ScriptFlush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scriptFlush(ASYNC); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void scriptKill_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ScriptKill), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scriptKill(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } } diff --git a/java/client/src/test/java/glide/api/GlideClusterClientTest.java b/java/client/src/test/java/glide/api/GlideClusterClientTest.java index fc20d6ecf7..4ab42385b3 100644 --- a/java/client/src/test/java/glide/api/GlideClusterClientTest.java +++ b/java/client/src/test/java/glide/api/GlideClusterClientTest.java @@ -29,6 +29,9 @@ import static command_request.CommandRequestOuterClass.RequestType.PubSubSNumSub; import static command_request.CommandRequestOuterClass.RequestType.RandomKey; import static command_request.CommandRequestOuterClass.RequestType.SPublish; +import static command_request.CommandRequestOuterClass.RequestType.ScriptExists; +import static command_request.CommandRequestOuterClass.RequestType.ScriptFlush; +import static command_request.CommandRequestOuterClass.RequestType.ScriptKill; import static command_request.CommandRequestOuterClass.RequestType.Sort; import static command_request.CommandRequestOuterClass.RequestType.SortReadOnly; import static command_request.CommandRequestOuterClass.RequestType.Time; @@ -60,8 +63,13 @@ import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; import glide.api.models.GlideString; +import glide.api.models.Script; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions.Section; +import glide.api.models.commands.ScriptArgOptions; +import glide.api.models.commands.ScriptArgOptionsGlideString; +import glide.api.models.commands.ScriptOptions; +import glide.api.models.commands.ScriptOptionsGlideString; import glide.api.models.commands.SortBaseOptions.Limit; import glide.api.models.commands.SortOptions; import glide.api.models.commands.SortOptionsBinary; @@ -75,8 +83,10 @@ import glide.managers.GlideExceptionCheckedFunction; import java.util.EnumSet; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeEach; @@ -3427,4 +3437,337 @@ public void scan_binary_existing_cursor_options() { ((CommandManager.ClusterScanCursorDetail) payload[0]).getCursorHandle()); assertArrayEquals(new Object[] {gs("foo")}, (Object[]) payload[1]); } + + @SneakyThrows + @Test + public void invokeScript_returns_success() { + // setup + Script script = mock(Script.class); + String hash = UUID.randomUUID().toString(); + when(script.getHash()).thenReturn(hash); + String payload = "hello"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(payload); + + // match on protobuf request + when(commandManager.submitScript(eq(script), eq(List.of()), eq(List.of()), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.invokeScript(script); + + // verify + assertEquals(testResponse, response); + assertEquals(payload, response.get()); + } + + @SneakyThrows + @Test + public void invokeScript_with_ScriptOptions_returns_success() { + // setup + Script script = mock(Script.class); + String hash = UUID.randomUUID().toString(); + when(script.getHash()).thenReturn(hash); + String payload = "hello"; + + ScriptOptions options = + ScriptOptions.builder().key("key1").key("key2").arg("arg1").arg("arg2").build(); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(payload); + + // match on protobuf request + when(commandManager.submitScript( + eq(script), + eq(List.of(gs("key1"), gs("key2"))), + eq(List.of(gs("arg1"), gs("arg2"))), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.invokeScript(script, options); + + // verify + assertEquals(testResponse, response); + assertEquals(payload, response.get()); + } + + @SneakyThrows + @Test + public void invokeScript_with_ScriptOptionsGlideString_returns_success() { + // setup + Script script = mock(Script.class); + String hash = UUID.randomUUID().toString(); + when(script.getHash()).thenReturn(hash); + GlideString payload = gs("hello"); + + ScriptOptionsGlideString options = + ScriptOptionsGlideString.builder() + .key(gs("key1")) + .key(gs("key2")) + .arg(gs("arg1")) + .arg(gs("arg2")) + .build(); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(payload); + + // match on protobuf request + when(commandManager.submitScript( + eq(script), + eq(List.of(gs("key1"), gs("key2"))), + eq(List.of(gs("arg1"), gs("arg2"))), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.invokeScript(script, options); + + // verify + assertEquals(testResponse, response); + assertEquals(payload, response.get()); + } + + @SneakyThrows + @Test + public void invokeScript_with_route_returns_success() { + // setup + Script script = mock(Script.class); + String hash = UUID.randomUUID().toString(); + when(script.getHash()).thenReturn(hash); + String payload = "hello"; + SingleNodeRoute route = RANDOM; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(payload); + + // match on protobuf request + when(commandManager.submitScript(eq(script), eq(List.of()), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.invokeScript(script, route); + + // verify + assertEquals(testResponse, response); + assertEquals(payload, response.get()); + } + + @SneakyThrows + @Test + public void invokeScript_with_route_args_returns_success() { + // setup + Script script = mock(Script.class); + String hash = UUID.randomUUID().toString(); + when(script.getHash()).thenReturn(hash); + String payload = "hello"; + SingleNodeRoute route = RANDOM; + + ScriptArgOptions options = ScriptArgOptions.builder().arg("arg1").arg("arg2").build(); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(payload); + + // match on protobuf request + when(commandManager.submitScript( + eq(script), eq(List.of(gs("arg1"), gs("arg2"))), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.invokeScript(script, options, route); + + // verify + assertEquals(testResponse, response); + assertEquals(payload, response.get()); + } + + @SneakyThrows + @Test + public void invokeScriptBinary_with_route_args_returns_success() { + // setup + Script script = mock(Script.class); + String hash = UUID.randomUUID().toString(); + when(script.getHash()).thenReturn(hash); + GlideString payload = gs("hello"); + SingleNodeRoute route = RANDOM; + + ScriptArgOptionsGlideString options = + ScriptArgOptionsGlideString.builder().arg(gs("arg1")).arg(gs("arg2")).build(); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(payload); + + // match on protobuf request + when(commandManager.submitScript( + eq(script), eq(List.of(gs("arg1"), gs("arg2"))), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.invokeScript(script, options, route); + + // verify + assertEquals(testResponse, response); + assertEquals(payload, response.get()); + } + + @SneakyThrows + @Test + public void scriptExists_returns_success() { + // setup + String hash = UUID.randomUUID().toString(); + String[] sha1s = {hash}; + Boolean[] value = {true}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ScriptExists), eq(sha1s), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scriptExists(sha1s); + Boolean[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scriptExists_with_route_returns_success() { + // setup + String hash = UUID.randomUUID().toString(); + String[] sha1s = {hash}; + Boolean[] value = {true}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ScriptExists), eq(sha1s), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scriptExists(sha1s, RANDOM); + Boolean[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scriptExists_binary_returns_success() { + // setup + GlideString hash = gs(UUID.randomUUID().toString()); + GlideString[] sha1s = {hash}; + Boolean[] value = {true}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ScriptExists), eq(sha1s), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scriptExists(sha1s); + Boolean[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scriptExists_binary_with_route_returns_success() { + // setup + GlideString hash = gs(UUID.randomUUID().toString()); + GlideString[] sha1s = {hash}; + Boolean[] value = {true}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ScriptExists), eq(sha1s), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scriptExists(sha1s, RANDOM); + Boolean[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scriptFlush_with_route_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ScriptFlush), eq(new String[0]), eq(ALL_PRIMARIES), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scriptFlush(ALL_PRIMARIES); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void scriptFlush_with_mode_and_route_returns_success() { + // setup + String[] args = new String[] {ASYNC.toString()}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ScriptFlush), eq(args), eq(ALL_PRIMARIES), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scriptFlush(ASYNC, ALL_PRIMARIES); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void scriptKill_with_route_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ScriptKill), eq(new String[0]), eq(ALL_PRIMARIES), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scriptKill(ALL_PRIMARIES); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } } diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index e228254653..54e5259e8d 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -50,8 +50,6 @@ import glide.api.models.commands.RangeOptions.RangeByScore; import glide.api.models.commands.RangeOptions.ScoreBoundary; import glide.api.models.commands.RestoreOptions; -import glide.api.models.commands.ScriptOptions; -import glide.api.models.commands.ScriptOptionsGlideString; import glide.api.models.commands.SetOptions; import glide.api.models.commands.SortBaseOptions; import glide.api.models.commands.SortOptions; @@ -3133,122 +3131,6 @@ public void persist_on_existing_and_non_existing_key(BaseClient client) { assertEquals(-1L, client.ttl(key).get()); } - @SneakyThrows - @ParameterizedTest(autoCloseArguments = false) - @MethodSource("getClients") - public void invokeScript_test(BaseClient client) { - String key1 = UUID.randomUUID().toString(); - String key2 = UUID.randomUUID().toString(); - - try (Script script = new Script("return 'Hello'", false)) { - Object response = client.invokeScript(script).get(); - assertEquals("Hello", response); - } - - try (Script script = new Script("return redis.call('SET', KEYS[1], ARGV[1])", false)) { - Object setResponse1 = - client - .invokeScript(script, ScriptOptions.builder().key(key1).arg("value1").build()) - .get(); - assertEquals(OK, setResponse1); - - Object setResponse2 = - client - .invokeScript(script, ScriptOptions.builder().key(key2).arg("value2").build()) - .get(); - assertEquals(OK, setResponse2); - } - - try (Script script = new Script("return redis.call('GET', KEYS[1])", false)) { - Object getResponse1 = - client.invokeScript(script, ScriptOptions.builder().key(key1).build()).get(); - assertEquals("value1", getResponse1); - - // Use GlideString in option but we still expect nonbinary output - Object getResponse2 = - client - .invokeScript(script, ScriptOptionsGlideString.builder().key(gs(key2)).build()) - .get(); - assertEquals("value2", getResponse2); - } - } - - @SneakyThrows - @ParameterizedTest(autoCloseArguments = false) - @MethodSource("getClients") - public void script_large_keys_and_or_args(BaseClient client) { - String str1 = "0".repeat(1 << 12); // 4k - String str2 = "0".repeat(1 << 12); // 4k - - try (Script script = new Script("return KEYS[1]", false)) { - // 1 very big key - Object response = - client.invokeScript(script, ScriptOptions.builder().key(str1 + str2).build()).get(); - assertEquals(str1 + str2, response); - } - - try (Script script = new Script("return KEYS[1]", false)) { - // 2 big keys - Object response = - client.invokeScript(script, ScriptOptions.builder().key(str1).key(str2).build()).get(); - assertEquals(str1, response); - } - - try (Script script = new Script("return ARGV[1]", false)) { - // 1 very big arg - Object response = - client.invokeScript(script, ScriptOptions.builder().arg(str1 + str2).build()).get(); - assertEquals(str1 + str2, response); - } - - try (Script script = new Script("return ARGV[1]", false)) { - // 1 big arg + 1 big key - Object response = - client.invokeScript(script, ScriptOptions.builder().arg(str1).key(str2).build()).get(); - assertEquals(str2, response); - } - } - - @SneakyThrows - @ParameterizedTest(autoCloseArguments = false) - @MethodSource("getClients") - public void invokeScript_gs_test(BaseClient client) { - GlideString key1 = gs(UUID.randomUUID().toString()); - GlideString key2 = gs(UUID.randomUUID().toString()); - - try (Script script = new Script(gs("return 'Hello'"), true)) { - Object response = client.invokeScript(script).get(); - assertEquals(gs("Hello"), response); - } - - try (Script script = new Script(gs("return redis.call('SET', KEYS[1], ARGV[1])"), true)) { - Object setResponse1 = - client - .invokeScript( - script, ScriptOptionsGlideString.builder().key(key1).arg(gs("value1")).build()) - .get(); - assertEquals(OK, setResponse1); - - Object setResponse2 = - client - .invokeScript( - script, ScriptOptionsGlideString.builder().key(key2).arg(gs("value2")).build()) - .get(); - assertEquals(OK, setResponse2); - } - - try (Script script = new Script(gs("return redis.call('GET', KEYS[1])"), true)) { - Object getResponse1 = - client.invokeScript(script, ScriptOptionsGlideString.builder().key(key1).build()).get(); - assertEquals(gs("value1"), getResponse1); - - // Use String in option but we still expect binary output (GlideString) - Object getResponse2 = - client.invokeScript(script, ScriptOptions.builder().key(key2.toString()).build()).get(); - assertEquals(gs("value2"), getResponse2); - } - } - @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java index 053f60c9bd..e7fc3c02cf 100644 --- a/java/integTest/src/test/java/glide/TestUtilities.java +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -360,6 +360,28 @@ public static String createLuaLibWithLongRunningFunction( .replace("$libName", libName); } + /** + * Create a lua script which runs an endless loop up to timeout sec.
    + * Execution takes at least 5 sec regardless of the timeout configured. + */ + public static String createLongRunningLuaScript(int timeout, boolean readOnly) { + String script = + readOnly + ? " local started = tonumber(redis.pcall('time')[1])\n" + + " while (true) do\n" + + " local now = tonumber(redis.pcall('time')[1])\n" + + " if now > started + $timeout then\n" + + " return 'Timed out $timeout sec'\n" + + " end\n" + + " end\n" + : "redis.call('SET', KEYS[1], 'value')\n" + + " local start = redis.call('time')[1]\n" + + " while redis.call('time')[1] - start < $timeout do\n" + + " redis.call('SET', KEYS[1], 'value')\n" + + " end\n"; + return script.replace("$timeout", Integer.toString(timeout)); + } + public static void waitForNotBusy(BaseClient client) { // If function wasn't killed, and it didn't time out - it blocks the server and cause rest // test to fail. diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 4c94c92261..bd893a7693 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -8,6 +8,7 @@ import static glide.TestUtilities.checkFunctionStatsBinaryResponse; import static glide.TestUtilities.checkFunctionStatsResponse; import static glide.TestUtilities.commonClusterClientConfig; +import static glide.TestUtilities.createLongRunningLuaScript; import static glide.TestUtilities.createLuaLibWithLongRunningFunction; import static glide.TestUtilities.generateLuaLibCode; import static glide.TestUtilities.generateLuaLibCodeBinary; @@ -54,9 +55,13 @@ import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; import glide.api.models.GlideString; +import glide.api.models.Script; +import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions.Section; import glide.api.models.commands.ListDirection; import glide.api.models.commands.RangeOptions.RangeByIndex; +import glide.api.models.commands.ScriptOptions; +import glide.api.models.commands.ScriptOptionsGlideString; import glide.api.models.commands.SortBaseOptions; import glide.api.models.commands.SortOptions; import glide.api.models.commands.SortOptionsBinary; @@ -69,6 +74,7 @@ import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.scan.ClusterScanCursor; import glide.api.models.commands.scan.ScanOptions; +import glide.api.models.configuration.RequestRoutingConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.ByAddressRoute; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; @@ -3010,4 +3016,333 @@ public void test_cluster_scan_all_stream() { cursor.releaseCursorHandle(); assertEquals(streamData.keySet(), results); } + + @SneakyThrows + @Test + public void invokeScript_test() { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + try (Script script = new Script("return 'Hello'", false)) { + Object response = clusterClient.invokeScript(script).get(); + assertEquals("Hello", response); + } + + try (Script script = new Script("return redis.call('SET', KEYS[1], ARGV[1])", false)) { + Object setResponse1 = + clusterClient + .invokeScript(script, ScriptOptions.builder().key(key1).arg("value1").build()) + .get(); + assertEquals(OK, setResponse1); + + Object setResponse2 = + clusterClient + .invokeScript(script, ScriptOptions.builder().key(key2).arg("value2").build()) + .get(); + assertEquals(OK, setResponse2); + } + + try (Script script = new Script("return redis.call('GET', KEYS[1])", false)) { + Object getResponse1 = + clusterClient.invokeScript(script, ScriptOptions.builder().key(key1).build()).get(); + assertEquals("value1", getResponse1); + + // Use GlideString in option but we still expect nonbinary output + Object getResponse2 = + clusterClient + .invokeScript(script, ScriptOptionsGlideString.builder().key(gs(key2)).build()) + .get(); + assertEquals("value2", getResponse2); + } + } + + @SneakyThrows + @Test + public void script_large_keys_and_or_args() { + String str1 = "0".repeat(1 << 12); // 4k + String str2 = "0".repeat(1 << 12); // 4k + + try (Script script = new Script("return KEYS[1]", false)) { + // 1 very big key + Object response = + clusterClient + .invokeScript(script, ScriptOptions.builder().key(str1 + str2).build()) + .get(); + assertEquals(str1 + str2, response); + } + + try (Script script = new Script("return KEYS[1]", false)) { + // 2 big keys + Object response = + clusterClient + .invokeScript(script, ScriptOptions.builder().key(str1).key(str2).build()) + .get(); + assertEquals(str1, response); + } + + try (Script script = new Script("return ARGV[1]", false)) { + // 1 very big arg + Object response = + clusterClient + .invokeScript(script, ScriptOptions.builder().arg(str1 + str2).build()) + .get(); + assertEquals(str1 + str2, response); + } + + try (Script script = new Script("return ARGV[1]", false)) { + // 1 big arg + 1 big key + Object response = + clusterClient + .invokeScript(script, ScriptOptions.builder().arg(str1).key(str2).build()) + .get(); + assertEquals(str2, response); + } + } + + @SneakyThrows + @Test + public void invokeScript_gs_test() { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + + try (Script script = new Script(gs("return 'Hello'"), true)) { + Object response = clusterClient.invokeScript(script).get(); + assertEquals(gs("Hello"), response); + } + + try (Script script = new Script(gs("return redis.call('SET', KEYS[1], ARGV[1])"), true)) { + Object setResponse1 = + clusterClient + .invokeScript( + script, ScriptOptionsGlideString.builder().key(key1).arg(gs("value1")).build()) + .get(); + assertEquals(OK, setResponse1); + + Object setResponse2 = + clusterClient + .invokeScript( + script, ScriptOptionsGlideString.builder().key(key2).arg(gs("value2")).build()) + .get(); + assertEquals(OK, setResponse2); + } + + try (Script script = new Script(gs("return redis.call('GET', KEYS[1])"), true)) { + Object getResponse1 = + clusterClient + .invokeScript(script, ScriptOptionsGlideString.builder().key(key1).build()) + .get(); + assertEquals(gs("value1"), getResponse1); + + // Use String in option but we still expect binary output (GlideString) + Object getResponse2 = + clusterClient + .invokeScript(script, ScriptOptions.builder().key(key2.toString()).build()) + .get(); + assertEquals(gs("value2"), getResponse2); + } + } + + @Test + @SneakyThrows + public void scriptExists() { + Script script1 = new Script("return 'Hello'", true); + Script script2 = new Script("return 'World'", true); + Script script3 = new Script("return 'Hello World'", true); + String key = UUID.randomUUID().toString(); + SingleNodeRoute route = new SlotKeyRoute(key, PRIMARY); + Boolean[] expected = new Boolean[] {true, false, true, false}; + + // Load script1 to all nodes, do not load script2 and load script3 with route + clusterClient.invokeScript(script1).get(); + clusterClient.invokeScript(script3, route).get(); + + // Get the SHA1 digests of the scripts + String sha1_1 = script1.getHash(); + String sha1_2 = script2.getHash(); + String sha1_3 = script3.getHash(); + String nonExistentSha1 = "0".repeat(40); // A SHA1 that doesn't exist + + // Check existence of scripts + Boolean[] result = + clusterClient.scriptExists(new String[] {sha1_1, sha1_2, sha1_3, nonExistentSha1}).get(); + Boolean[] result2 = + clusterClient + .scriptExists(new String[] {sha1_1, sha1_2, sha1_3, nonExistentSha1}, route) + .get(); + assertArrayEquals(expected, result); + assertArrayEquals(expected, result2); + } + + @Test + @SneakyThrows + public void scriptExistsBinary() { + Script script1 = new Script(gs("return 'Hello'"), true); + Script script2 = new Script(gs("return 'World'"), true); + Script script3 = new Script(gs("return 'Hello World'"), true); + String key = UUID.randomUUID().toString(); + SingleNodeRoute route = new SlotKeyRoute(key, PRIMARY); + Boolean[] expected = new Boolean[] {true, false, true, false}; + + // Load script1 to all nodes, do not load script2 and load script3 with route + clusterClient.invokeScript(script1).get(); + clusterClient.invokeScript(script3, route).get(); + + // Get the SHA1 digests of the scripts + GlideString sha1_1 = gs(script1.getHash()); + GlideString sha1_2 = gs(script2.getHash()); + GlideString sha1_3 = gs(script3.getHash()); + GlideString nonExistentSha1 = gs("0".repeat(40)); // A SHA1 that doesn't exist + + // Check existence of scripts + Boolean[] result = + clusterClient + .scriptExists(new GlideString[] {sha1_1, sha1_2, sha1_3, nonExistentSha1}) + .get(); + Boolean[] result2 = + clusterClient + .scriptExists(new GlideString[] {sha1_1, sha1_2, sha1_3, nonExistentSha1}, route) + .get(); + assertArrayEquals(expected, result); + assertArrayEquals(expected, result2); + } + + @Test + @SneakyThrows + public void scriptFlush() { + Script script = new Script("return 'Hello'", true); + + // Load script + clusterClient.invokeScript(script, ALL_PRIMARIES).get(); + + // Check existence of scripts + Boolean[] result = + clusterClient.scriptExists(new String[] {script.getHash()}, ALL_PRIMARIES).get(); + assertArrayEquals(new Boolean[] {true}, result); + + // flush the script cache + assertEquals(OK, clusterClient.scriptFlush(ALL_PRIMARIES).get()); + + // check that the script no longer exists + result = clusterClient.scriptExists(new String[] {script.getHash()}, ALL_PRIMARIES).get(); + assertArrayEquals(new Boolean[] {false}, result); + + // Test with ASYNC mode + clusterClient.invokeScript(script, ALL_PRIMARIES).get(); + assertEquals(OK, clusterClient.scriptFlush(FlushMode.ASYNC, ALL_PRIMARIES).get()); + result = clusterClient.scriptExists(new String[] {script.getHash()}, ALL_PRIMARIES).get(); + assertArrayEquals(new Boolean[] {false}, result); + } + + @Test + @SneakyThrows + public void scriptKill_with_route() { + // create and load a long-running script and a primary node route + Script script = new Script(createLongRunningLuaScript(5, true), true); + RequestRoutingConfiguration.Route route = + new RequestRoutingConfiguration.SlotKeyRoute(UUID.randomUUID().toString(), PRIMARY); + + // Verify that script_kill raises an error when no script is running + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> clusterClient.scriptKill(route).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException + .getMessage() + .toLowerCase() + .contains("no scripts in execution right now")); + + CompletableFuture promise = new CompletableFuture<>(); + promise.complete(null); + + try (var testClient = + GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(10000).build()) + .get()) { + try { + testClient.invokeScript(script, route); + + Thread.sleep(1000); + + // Run script kill until it returns OK + boolean scriptKilled = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + assertEquals(OK, clusterClient.scriptKill(route).get()); + scriptKilled = true; + break; + } catch (RequestException ignored) { + } + Thread.sleep(500); + timeout -= 500; + } + + assertTrue(scriptKilled); + } finally { + waitForNotBusy(clusterClient); + } + } + + // Verify that script_kill raises an error when no script is running + executionException = + assertThrows(ExecutionException.class, () -> clusterClient.scriptKill(route).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException + .getMessage() + .toLowerCase() + .contains("no scripts in execution right now")); + } + + @SneakyThrows + @Test + public void scriptKill_unkillable() { + String key = UUID.randomUUID().toString(); + RequestRoutingConfiguration.Route route = + new RequestRoutingConfiguration.SlotKeyRoute(key, PRIMARY); + String code = createLongRunningLuaScript(5, false); + Script script = new Script(code, false); + + CompletableFuture promise = new CompletableFuture<>(); + promise.complete(null); + + try (var testClient = + GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(10000).build()) + .get()) { + try { + // run the script without await + promise = testClient.invokeScript(script, ScriptOptions.builder().key(key).build()); + + Thread.sleep(1000); + + boolean foundUnkillable = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + // valkey kills a script with 5 sec delay + // but this will always throw an error in the test + clusterClient.scriptKill(route).get(); + } catch (ExecutionException execException) { + // looking for an error with "unkillable" in the message + // at that point we can break the loop + if (execException.getCause() instanceof RequestException + && execException.getMessage().toLowerCase().contains("unkillable")) { + foundUnkillable = true; + break; + } + } + Thread.sleep(500); + timeout -= 500; + } + assertTrue(foundUnkillable); + } finally { + // If script wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. + // wait for the script to complete (we cannot kill it) + try { + promise.get(); + } catch (Exception ignored) { + } + } + } + } } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index a5e6391fa1..34abf7861e 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -8,6 +8,7 @@ import static glide.TestUtilities.checkFunctionStatsBinaryResponse; import static glide.TestUtilities.checkFunctionStatsResponse; import static glide.TestUtilities.commonClientConfig; +import static glide.TestUtilities.createLongRunningLuaScript; import static glide.TestUtilities.createLuaLibWithLongRunningFunction; import static glide.TestUtilities.generateLuaLibCode; import static glide.TestUtilities.generateLuaLibCodeBinary; @@ -34,6 +35,7 @@ import static glide.cluster.CommandTests.EVERYTHING_INFO_SECTIONS; import static glide.utils.ArrayTransformUtils.concatenateArrays; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -45,7 +47,11 @@ import glide.api.GlideClient; import glide.api.models.GlideString; +import glide.api.models.Script; +import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions.Section; +import glide.api.models.commands.ScriptOptions; +import glide.api.models.commands.ScriptOptionsGlideString; import glide.api.models.commands.scan.ScanOptions; import glide.api.models.exceptions.RequestException; import java.time.Instant; @@ -1440,4 +1446,303 @@ public void scan_binary_with_options() { assertEquals(0, ((Object[]) hashResult[resultCollectionIndex]).length); } while (!hashCursor.equals(gs("0"))); // 0 is returned for the cursor of the last iteration. } + + @SneakyThrows + @Test + public void invokeScript_test() { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + try (Script script = new Script("return 'Hello'", false)) { + Object response = regularClient.invokeScript(script).get(); + assertEquals("Hello", response); + } + + try (Script script = new Script("return redis.call('SET', KEYS[1], ARGV[1])", false)) { + Object setResponse1 = + regularClient + .invokeScript(script, ScriptOptions.builder().key(key1).arg("value1").build()) + .get(); + assertEquals(OK, setResponse1); + + Object setResponse2 = + regularClient + .invokeScript(script, ScriptOptions.builder().key(key2).arg("value2").build()) + .get(); + assertEquals(OK, setResponse2); + } + + try (Script script = new Script("return redis.call('GET', KEYS[1])", false)) { + Object getResponse1 = + regularClient.invokeScript(script, ScriptOptions.builder().key(key1).build()).get(); + assertEquals("value1", getResponse1); + + // Use GlideString in option but we still expect nonbinary output + Object getResponse2 = + regularClient + .invokeScript(script, ScriptOptionsGlideString.builder().key(gs(key2)).build()) + .get(); + assertEquals("value2", getResponse2); + } + } + + @SneakyThrows + @Test + public void script_large_keys_and_or_args() { + String str1 = "0".repeat(1 << 12); // 4k + String str2 = "0".repeat(1 << 12); // 4k + + try (Script script = new Script("return KEYS[1]", false)) { + // 1 very big key + Object response = + regularClient + .invokeScript(script, ScriptOptions.builder().key(str1 + str2).build()) + .get(); + assertEquals(str1 + str2, response); + } + + try (Script script = new Script("return KEYS[1]", false)) { + // 2 big keys + Object response = + regularClient + .invokeScript(script, ScriptOptions.builder().key(str1).key(str2).build()) + .get(); + assertEquals(str1, response); + } + + try (Script script = new Script("return ARGV[1]", false)) { + // 1 very big arg + Object response = + regularClient + .invokeScript(script, ScriptOptions.builder().arg(str1 + str2).build()) + .get(); + assertEquals(str1 + str2, response); + } + + try (Script script = new Script("return ARGV[1]", false)) { + // 1 big arg + 1 big key + Object response = + regularClient + .invokeScript(script, ScriptOptions.builder().arg(str1).key(str2).build()) + .get(); + assertEquals(str2, response); + } + } + + @SneakyThrows + @Test + public void invokeScript_gs_test() { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + + try (Script script = new Script(gs("return 'Hello'"), true)) { + Object response = regularClient.invokeScript(script).get(); + assertEquals(gs("Hello"), response); + } + + try (Script script = new Script(gs("return redis.call('SET', KEYS[1], ARGV[1])"), true)) { + Object setResponse1 = + regularClient + .invokeScript( + script, ScriptOptionsGlideString.builder().key(key1).arg(gs("value1")).build()) + .get(); + assertEquals(OK, setResponse1); + + Object setResponse2 = + regularClient + .invokeScript( + script, ScriptOptionsGlideString.builder().key(key2).arg(gs("value2")).build()) + .get(); + assertEquals(OK, setResponse2); + } + + try (Script script = new Script(gs("return redis.call('GET', KEYS[1])"), true)) { + Object getResponse1 = + regularClient + .invokeScript(script, ScriptOptionsGlideString.builder().key(key1).build()) + .get(); + assertEquals(gs("value1"), getResponse1); + + // Use String in option but we still expect binary output (GlideString) + Object getResponse2 = + regularClient + .invokeScript(script, ScriptOptions.builder().key(key2.toString()).build()) + .get(); + assertEquals(gs("value2"), getResponse2); + } + } + + @SneakyThrows + public void scriptExists() { + Script script1 = new Script("return 'Hello'", true); + Script script2 = new Script("return 'World'", true); + Boolean[] expected = new Boolean[] {true, false, false}; + + // Load script1 to all nodes, do not load script2 and load script3 with a SlotKeyRoute + regularClient.invokeScript(script1).get(); + + // Get the SHA1 digests of the scripts + String sha1_1 = script1.getHash(); + String sha1_2 = script2.getHash(); + String nonExistentSha1 = "0".repeat(40); // A SHA1 that doesn't exist + + // Check existence of scripts + Boolean[] result = + regularClient.scriptExists(new String[] {sha1_1, sha1_2, nonExistentSha1}).get(); + assertArrayEquals(expected, result); + } + + @Test + @SneakyThrows + public void scriptExistsBinary() { + Script script1 = new Script(gs("return 'Hello'"), true); + Script script2 = new Script(gs("return 'World'"), true); + Boolean[] expected = new Boolean[] {true, false, false}; + + // Load script1 to all nodes, do not load script2 and load script3 with a SlotKeyRoute + regularClient.invokeScript(script1).get(); + + // Get the SHA1 digests of the scripts + GlideString sha1_1 = gs(script1.getHash()); + GlideString sha1_2 = gs(script2.getHash()); + GlideString nonExistentSha1 = gs("0".repeat(40)); // A SHA1 that doesn't exist + + // Check existence of scripts + Boolean[] result = + regularClient.scriptExists(new GlideString[] {sha1_1, sha1_2, nonExistentSha1}).get(); + assertArrayEquals(expected, result); + } + + @Test + @SneakyThrows + public void scriptFlush() { + Script script = new Script("return 'Hello'", true); + + // Load script + regularClient.invokeScript(script).get(); + + // Check existence of scripts + Boolean[] result = regularClient.scriptExists(new String[] {script.getHash()}).get(); + assertArrayEquals(new Boolean[] {true}, result); + + // flush the script cache + assertEquals(OK, regularClient.scriptFlush().get()); + + // check that the script no longer exists + result = regularClient.scriptExists(new String[] {script.getHash()}).get(); + assertArrayEquals(new Boolean[] {false}, result); + + // Test with ASYNC mode + regularClient.invokeScript(script).get(); + assertEquals(OK, regularClient.scriptFlush(FlushMode.ASYNC).get()); + result = regularClient.scriptExists(new String[] {script.getHash()}).get(); + assertArrayEquals(new Boolean[] {false}, result); + } + + @Test + @SneakyThrows + public void scriptKill() { + // Verify that script_kill raises an error when no script is running + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> regularClient.scriptKill().get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException + .getMessage() + .toLowerCase() + .contains("no scripts in execution right now")); + + CompletableFuture promise = new CompletableFuture<>(); + promise.complete(null); + + // create and load a long-running script + Script script = new Script(createLongRunningLuaScript(5, true), true); + + try (var testClient = + GlideClient.createClient(commonClientConfig().requestTimeout(10000).build()).get()) { + try { + testClient.invokeScript(script); + + Thread.sleep(1000); + + // Run script kill until it returns OK + boolean scriptKilled = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + assertEquals(OK, regularClient.scriptKill().get()); + scriptKilled = true; + break; + } catch (RequestException ignored) { + } + Thread.sleep(500); + timeout -= 500; + } + + assertTrue(scriptKilled); + } finally { + waitForNotBusy(regularClient); + } + } + + // Verify that script_kill raises an error when no script is running + executionException = + assertThrows(ExecutionException.class, () -> regularClient.scriptKill().get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException + .getMessage() + .toLowerCase() + .contains("no scripts in execution right now")); + } + + @SneakyThrows + @Test + public void scriptKill_unkillable() { + String key = UUID.randomUUID().toString(); + String code = createLongRunningLuaScript(5, false); + Script script = new Script(code, false); + + CompletableFuture promise = new CompletableFuture<>(); + promise.complete(null); + + try (var testClient = + GlideClient.createClient(commonClientConfig().requestTimeout(10000).build()).get()) { + try { + // run the script without await + promise = testClient.invokeScript(script, ScriptOptions.builder().key(key).build()); + + Thread.sleep(1000); + + boolean foundUnkillable = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + // valkey kills a script with 5 sec delay + // but this will always throw an error in the test + regularClient.scriptKill().get(); + } catch (ExecutionException execException) { + // looking for an error with "unkillable" in the message + // at that point we can break the loop + if (execException.getCause() instanceof RequestException + && execException.getMessage().toLowerCase().contains("unkillable")) { + foundUnkillable = true; + break; + } + } + Thread.sleep(500); + timeout -= 500; + } + assertTrue(foundUnkillable); + } finally { + // If script wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. + // wait for the script to complete (we cannot kill it) + try { + promise.get(); + } catch (Exception ignored) { + } + } + } + } }