diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index b3356f0e61..58a8671a0e 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -3195,6 +3195,17 @@ public CompletableFuture fcall( return commandManager.submitNewCommand(FCall, args, this::handleObjectOrNullResponse); } + @Override + public CompletableFuture fcall( + @NonNull GlideString function, + @NonNull GlideString[] keys, + @NonNull GlideString[] arguments) { + GlideString[] args = + concatenateArrays( + new GlideString[] {function, gs(Long.toString(keys.length))}, keys, arguments); + return commandManager.submitNewCommand(FCall, args, this::handleObjectOrNullResponse); + } + @Override public CompletableFuture fcallReadOnly( @NonNull String function, @NonNull String[] keys, @NonNull String[] arguments) { @@ -3203,6 +3214,17 @@ public CompletableFuture fcallReadOnly( return commandManager.submitNewCommand(FCallReadOnly, args, this::handleObjectOrNullResponse); } + @Override + public CompletableFuture fcallReadOnly( + @NonNull GlideString function, + @NonNull GlideString[] keys, + @NonNull GlideString[] arguments) { + GlideString[] args = + concatenateArrays( + new GlideString[] {function, gs(Long.toString(keys.length))}, keys, arguments); + return commandManager.submitNewCommand(FCallReadOnly, args, this::handleObjectOrNullResponse); + } + @Override public CompletableFuture copy( @NonNull String source, @NonNull String destination, boolean replace) { diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index 96a7376ad5..6007d16ee5 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -350,11 +350,21 @@ public CompletableFuture fcall(@NonNull String function) { return fcall(function, new String[0], new String[0]); } + @Override + public CompletableFuture fcall(@NonNull GlideString function) { + return fcall(function, new GlideString[0], new GlideString[0]); + } + @Override public CompletableFuture fcallReadOnly(@NonNull String function) { return fcallReadOnly(function, new String[0], new String[0]); } + @Override + public CompletableFuture fcallReadOnly(@NonNull GlideString function) { + return fcallReadOnly(function, new GlideString[0], new GlideString[0]); + } + @Override public CompletableFuture copy( @NonNull String source, @NonNull String destination, long destinationDB) { diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index 5301f3bb34..d18e385f5f 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -717,18 +717,37 @@ public CompletableFuture fcall(@NonNull String function) { return fcall(function, new String[0]); } + @Override + public CompletableFuture fcall(@NonNull GlideString function) { + return fcall(function, new GlideString[0]); + } + @Override public CompletableFuture> fcall( @NonNull String function, @NonNull Route route) { return fcall(function, new String[0], route); } + @Override + public CompletableFuture> fcall( + @NonNull GlideString function, @NonNull Route route) { + return fcall(function, new GlideString[0], route); + } + @Override public CompletableFuture fcall(@NonNull String function, @NonNull String[] arguments) { String[] args = concatenateArrays(new String[] {function, "0"}, arguments); // 0 - key count return commandManager.submitNewCommand(FCall, args, this::handleObjectOrNullResponse); } + @Override + public CompletableFuture fcall( + @NonNull GlideString function, @NonNull GlideString[] arguments) { + GlideString[] args = + concatenateArrays(new GlideString[] {function, gs("0")}, arguments); // 0 - key count + return commandManager.submitNewCommand(FCall, args, this::handleObjectOrNullResponse); + } + @Override public CompletableFuture> fcall( @NonNull String function, @NonNull String[] arguments, @NonNull Route route) { @@ -743,17 +762,43 @@ public CompletableFuture> fcall( : ClusterValue.ofMultiValue(handleMapResponse(response))); } + @Override + public CompletableFuture> fcall( + @NonNull GlideString function, @NonNull GlideString[] arguments, @NonNull Route route) { + GlideString[] args = + concatenateArrays(new GlideString[] {function, gs("0")}, arguments); // 0 - key count + return commandManager.submitNewCommand( + FCall, + args, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleObjectOrNullResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } + @Override public CompletableFuture fcallReadOnly(@NonNull String function) { return fcallReadOnly(function, new String[0]); } + @Override + public CompletableFuture fcallReadOnly(@NonNull GlideString function) { + return fcallReadOnly(function, new GlideString[0]); + } + @Override public CompletableFuture> fcallReadOnly( @NonNull String function, @NonNull Route route) { return fcallReadOnly(function, new String[0], route); } + @Override + public CompletableFuture> fcallReadOnly( + @NonNull GlideString function, @NonNull Route route) { + return fcallReadOnly(function, new GlideString[0], route); + } + @Override public CompletableFuture fcallReadOnly( @NonNull String function, @NonNull String[] arguments) { @@ -761,6 +806,14 @@ public CompletableFuture fcallReadOnly( return commandManager.submitNewCommand(FCallReadOnly, args, this::handleObjectOrNullResponse); } + @Override + public CompletableFuture fcallReadOnly( + @NonNull GlideString function, @NonNull GlideString[] arguments) { + GlideString[] args = + concatenateArrays(new GlideString[] {function, gs("0")}, arguments); // 0 - key count + return commandManager.submitNewCommand(FCallReadOnly, args, this::handleObjectOrNullResponse); + } + @Override public CompletableFuture> fcallReadOnly( @NonNull String function, @NonNull String[] arguments, @NonNull Route route) { @@ -775,6 +828,21 @@ public CompletableFuture> fcallReadOnly( : ClusterValue.ofMultiValue(handleMapResponse(response))); } + @Override + public CompletableFuture> fcallReadOnly( + @NonNull GlideString function, @NonNull GlideString[] arguments, @NonNull Route route) { + GlideString[] args = + concatenateArrays(new GlideString[] {function, gs("0")}, arguments); // 0 - key count + return commandManager.submitNewCommand( + FCallReadOnly, + args, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleObjectOrNullResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } + @Override public CompletableFuture functionKill() { return commandManager.submitNewCommand(FunctionKill, new String[0], this::handleStringResponse); 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 d1063ed3e8..bcb1060bc7 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsBaseCommands.java @@ -1,6 +1,7 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; @@ -42,6 +43,36 @@ public interface ScriptingAndFunctionsBaseCommands { */ CompletableFuture fcall(String function, String[] keys, String[] arguments); + /** + * Invokes a previously loaded function.
+ * This command is routed to primary nodes only.
+ * To route to a replica please refer to {@link #fcallReadOnly}. + * + * @apiNote When in cluster mode + *
    + *
  • all keys must map to the same hash slot. + *
  • if no keys are given, command will be routed to a random primary node. + *
+ * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param function The function name. + * @param keys An array of keys accessed by the function. To ensure the correct + * execution of functions, both in standalone and clustered deployments, all names of keys + * that a function accesses must be explicitly provided as keys. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return The invoked function's return value. + * @example + *
{@code
+     * GlideString[] args = new GlideString[] { gs("Answer"), gs("to"), gs("the"), gs("Ultimate"), gs("Question"), gs("of"), gs("Life,"), gs("the"), gs("Universe,"), gs("and"), gs("Everything")};
+     * Object response = client.fcall(gs("Deep_Thought"), new GlideString[0], args).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcall( + GlideString function, GlideString[] keys, GlideString[] arguments); + /** * Invokes a previously loaded read-only function.
* This command is routed depending on the client's {@link ReadFrom} strategy. @@ -69,4 +100,33 @@ public interface ScriptingAndFunctionsBaseCommands { * } */ CompletableFuture fcallReadOnly(String function, String[] keys, String[] arguments); + + /** + * Invokes a previously loaded read-only function.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @apiNote When in cluster mode + *
    + *
  • all keys must map to the same hash slot. + *
  • if no keys are given, command will be routed to a random node. + *
+ * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param function The function name. + * @param keys An array of keys accessed by the function. To ensure the correct + * execution of functions, both in standalone and clustered deployments, all names of keys + * that a function accesses must be explicitly provided as keys. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return The invoked function's return value. + * @example + *
{@code
+     * GlideString[] args = new GlideString[] { gs("Answer"), gs("to"), gs("the"), gs("Ultimate"), gs("Question"), gs("of"), gs("Life,"), gs("the"), gs("Universe,"), gs("and"), gs("Everything")};
+     * Object response = client.fcallReadOnly(gs("Deep_Thought"), new GlideString[0], args).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcallReadOnly( + GlideString function, GlideString[] keys, GlideString[] arguments); } 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 a5125e8157..9235f52432 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java @@ -466,6 +466,23 @@ CompletableFuture functionRestore( */ CompletableFuture fcall(String function); + /** + * Invokes a previously loaded function.
+ * The command will be routed to a primary random node.
+ * To route to a replica please refer to {@link #fcallReadOnly(String)}. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param function The function name. + * @return The invoked function's return value. + * @example + *
{@code
+     * Object response = client.fcall(gs("Deep_Thought")).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcall(GlideString function); + /** * Invokes a previously loaded function. * @@ -485,6 +502,25 @@ CompletableFuture functionRestore( */ CompletableFuture> fcall(String function, Route route); + /** + * Invokes a previously loaded function. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param function The function name. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The invoked function's return value wrapped by a {@link ClusterValue}. + * @example + *
{@code
+     * ClusterValue response = client.fcall(gs("Deep_Thought"), ALL_NODES).get();
+     * for (Object nodeResponse : response.getMultiValue().values()) {
+     *   assert nodeResponse == 42L;
+     * }
+     * }
+     */
+    CompletableFuture> fcall(GlideString function, Route route);
+
     /**
      * Invokes a previously loaded function.
* The command will be routed to a random primary node.
@@ -505,6 +541,26 @@ CompletableFuture functionRestore( */ CompletableFuture fcall(String function, String[] arguments); + /** + * Invokes a previously loaded function.
+ * The command will be routed to a random primary node.
+ * To route to a replica please refer to {@link #fcallReadOnly(String, String[])}. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param function The function name. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return The invoked function's return value. + * @example + *
{@code
+     * GlideString[] args = new GlideString[] { gs("Answer"), gs("to"), gs("the"), gs("Ultimate"), gs("Question"), gs("of"), gs("Life,"), gs("the"), gs("Universe,"), gs("and"), gs("Everything")};
+     * Object response = client.fcall(gs("Deep_Thought"), args).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcall(GlideString function, GlideString[] arguments); + /** * Invokes a previously loaded function. * @@ -525,6 +581,27 @@ CompletableFuture functionRestore( */ CompletableFuture> fcall(String function, String[] arguments, Route route); + /** + * Invokes a previously loaded function. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param function The function name. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The invoked function's return value wrapped by a {@link ClusterValue}. + * @example + *
{@code
+     * GlideString[] args = new GlideString[] { gs("Answer"), gs("to"), gs("the"), gs("Ultimate"), gs("Question"), gs("of"), gs("Life,"), gs("the"), gs("Universe,"), gs("and"), gs("Everything")};
+     * ClusterValue response = client.fcall(gs("Deep_Thought"), args, RANDOM).get();
+     * assert response.getSingleValue() == 42L;
+     * }
+     */
+    CompletableFuture> fcall(
+            GlideString function, GlideString[] arguments, Route route);
+
     /**
      * Invokes a previously loaded read-only function.
* The command is routed to a random node depending on the client's {@link ReadFrom} strategy. @@ -541,6 +618,22 @@ CompletableFuture functionRestore( */ CompletableFuture fcallReadOnly(String function); + /** + * Invokes a previously loaded read-only function.
+ * The command is routed to a random node depending on the client's {@link ReadFrom} strategy. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param function The function name. + * @return The invoked function's return value. + * @example + *
{@code
+     * Object response = client.fcallReadOnly(gs("Deep_Thought")).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcallReadOnly(GlideString function); + /** * Invokes a previously loaded read-only function. * @@ -560,6 +653,25 @@ CompletableFuture functionRestore( */ CompletableFuture> fcallReadOnly(String function, Route route); + /** + * Invokes a previously loaded read-only function. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param function The function name. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The invoked function's return value wrapped by a {@link ClusterValue}. + * @example + *
{@code
+     * ClusterValue response = client.fcallReadOnly(gs("Deep_Thought"), ALL_NODES).get();
+     * for (Object nodeResponse : response.getMultiValue().values()) {
+     *   assert nodeResponse == 42L;
+     * }
+     * }
+     */
+    CompletableFuture> fcallReadOnly(GlideString function, Route route);
+
     /**
      * Invokes a previously loaded function.
* The command is routed to a random node depending on the client's {@link ReadFrom} strategy. @@ -579,6 +691,25 @@ CompletableFuture functionRestore( */ CompletableFuture fcallReadOnly(String function, String[] arguments); + /** + * Invokes a previously loaded function.
+ * The command is routed to a random node depending on the client's {@link ReadFrom} strategy. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param function The function name. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return The invoked function's return value. + * @example + *
{@code
+     * GlideString[] args = new GlideString[] { gs("Answer"), gs("to"), gs("the"), gs("Ultimate"), gs("Question"), gs("of"), gs("Life,"), gs("the"), gs("Universe,"), gs("and"), gs("Everything")};
+     * Object response = client.fcallReadOnly(gs("Deep_Thought"), args).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcallReadOnly(GlideString function, GlideString[] arguments); + /** * Invokes a previously loaded read-only function. * @@ -600,6 +731,27 @@ CompletableFuture functionRestore( CompletableFuture> fcallReadOnly( String function, String[] arguments, Route route); + /** + * Invokes a previously loaded read-only function. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param function The function name. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The invoked function's return value wrapped by a {@link ClusterValue}. + * @example + *
{@code
+     * GlideString[] args = new GlideString[] { gs("Answer"), gs("to"), gs("the"), gs("Ultimate"), gs("Question"), gs("of"), gs("Life,"), gs("the"), gs("Universe,"), gs("and"), gs("Everything")};
+     * ClusterValue response = client.fcallReadOnly(gs("Deep_Thought"), args, RANDOM).get();
+     * assert response.getSingleValue() == 42L;
+     * }
+     */
+    CompletableFuture> fcallReadOnly(
+            GlideString function, GlideString[] arguments, Route route);
+
     /**
      * Kills a function that is currently executing.
* FUNCTION KILL terminates read-only functions only.
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 25cdefd8ac..759460391e 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java @@ -228,6 +228,23 @@ public interface ScriptingAndFunctionsCommands { */ CompletableFuture fcall(String function); + /** + * Invokes a previously loaded function.
+ * This command is routed to primary nodes only.
+ * To route to a replica please refer to {@link #fcallReadOnly}. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param function The function name. + * @return The invoked function's return value. + * @example + *
{@code
+     * Object response = client.fcall(gs("Deep_Thought")).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcall(GlideString function); + /** * Invokes a previously loaded read-only function.
* This command is routed depending on the client's {@link ReadFrom} strategy. @@ -244,6 +261,22 @@ public interface ScriptingAndFunctionsCommands { */ CompletableFuture fcallReadOnly(String function); + /** + * Invokes a previously loaded read-only function.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param function The function name. + * @return The invoked function's return value. + * @example + *
{@code
+     * Object response = client.fcallReadOnly(gs("Deep_Thought")).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcallReadOnly(GlideString function); + /** * Kills a function that is currently executing.
* FUNCTION KILL terminates read-only functions only. diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index bba2f23087..a7490534ef 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -8734,6 +8734,31 @@ public void fcall_with_keys_and_args_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void fcall_with_keys_and_args_binary_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + GlideString[] arguments = new GlideString[] {gs("1"), gs("2")}; + GlideString[] args = + new GlideString[] {function, gs("2"), gs("key1"), gs("key2"), gs("1"), gs("2")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcall(function, keys, arguments); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void fcall_returns_success() { @@ -8756,6 +8781,28 @@ public void fcall_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void fcall_binary_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] args = new GlideString[] {function, gs("0")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcall(function); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void fcallReadOnly_with_keys_and_args_returns_success() { @@ -8781,6 +8828,32 @@ public void fcallReadOnly_with_keys_and_args_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void fcallReadOnly_with_keys_and_args_binary_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + GlideString[] arguments = new GlideString[] {gs("1"), gs("2")}; + GlideString[] args = + new GlideString[] {function, gs("2"), gs("key1"), gs("key2"), gs("1"), gs("2")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCallReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcallReadOnly(function, keys, arguments); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void fcallReadOnly_returns_success() { @@ -8804,6 +8877,29 @@ public void fcallReadOnly_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void fcallReadOnly_binary_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] args = new GlideString[] {function, gs("0")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCallReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcallReadOnly(function); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void functionKill_returns_success() { diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index e17c03f788..dd049c3504 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -1843,6 +1843,28 @@ public void fcall_without_keys_and_without_args_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void fcall_binary_without_keys_and_without_args_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] args = new GlideString[] {function, gs("0")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcall(function); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void fcall_without_keys_and_without_args_but_with_route_returns_success() { @@ -1867,6 +1889,30 @@ public void fcall_without_keys_and_without_args_but_with_route_returns_success() assertEquals(value, payload); } + @SneakyThrows + @Test + public void fcall_binary_without_keys_and_without_args_but_with_route_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] args = new GlideString[] {function, gs("0")}; + ClusterValue value = ClusterValue.ofSingleValue("42"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FCall), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.fcall(function, RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void fcall_without_keys_returns_success() { @@ -1890,6 +1936,29 @@ public void fcall_without_keys_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void fcall_binary_without_keys_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] arguments = new GlideString[] {gs("1"), gs("2")}; + GlideString[] args = new GlideString[] {function, gs("0"), gs("1"), gs("2")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcall(function, arguments); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void fcall_without_keys_and_with_route_returns_success() { @@ -1915,6 +1984,31 @@ public void fcall_without_keys_and_with_route_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void fcall_bianry_without_keys_and_with_route_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] arguments = new GlideString[] {gs("1"), gs("2")}; + GlideString[] args = new GlideString[] {function, gs("0"), gs("1"), gs("2")}; + ClusterValue value = ClusterValue.ofSingleValue("42"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FCall), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.fcall(function, arguments, RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void fcallReadOnly_without_keys_and_without_args_returns_success() { @@ -1938,6 +2032,29 @@ public void fcallReadOnly_without_keys_and_without_args_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void fcallReadOnly_binary_without_keys_and_without_args_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] args = new GlideString[] {function, gs("0")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCallReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcallReadOnly(function); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void functionKill_returns_success() { @@ -2030,6 +2147,30 @@ public void fcallReadOnly_without_keys_and_without_args_but_with_route_returns_s assertEquals(value, payload); } + @SneakyThrows + @Test + public void fcallReadOnly_binary_without_keys_and_without_args_but_with_route_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] args = new GlideString[] {function, gs("0")}; + ClusterValue value = ClusterValue.ofSingleValue("42"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FCallReadOnly), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.fcallReadOnly(function, RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void fcallReadOnly_without_keys_returns_success() { @@ -2054,6 +2195,30 @@ public void fcallReadOnly_without_keys_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void fcallReadOnly_binary_without_keys_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] arguments = new GlideString[] {gs("1"), gs("2")}; + GlideString[] args = new GlideString[] {function, gs("0"), gs("1"), gs("2")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCallReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcallReadOnly(function, arguments); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void fcallReadOnly_without_keys_and_with_route_returns_success() { @@ -2080,6 +2245,32 @@ public void fcallReadOnly_without_keys_and_with_route_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void fcallReadOnly_binary_without_keys_and_with_route_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] arguments = new GlideString[] {gs("1"), gs("2")}; + GlideString[] args = new GlideString[] {function, gs("0"), gs("1"), gs("2")}; + ClusterValue value = ClusterValue.ofSingleValue("42"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FCallReadOnly), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.fcallReadOnly(function, arguments, RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void functionStats_with_route_returns_success() { diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 0e1b6b641a..30a19e6e61 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -867,10 +867,24 @@ public static Stream callCrossSlotCommandsWhichShouldFail() { "fcall", "7.0.0", clusterClient.fcall("func", new String[] {"abc", "zxy", "lkn"}, new String[0])), + Arguments.of( + "fcall binary", + "7.0.0", + clusterClient.fcall( + gs("func"), + new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")}, + new GlideString[0])), Arguments.of( "fcallReadOnly", "7.0.0", clusterClient.fcallReadOnly("func", new String[] {"abc", "zxy", "lkn"}, new String[0])), + Arguments.of( + "fcallReadOnly binary", + "7.0.0", + clusterClient.fcallReadOnly( + gs("func"), + new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")}, + new GlideString[0])), Arguments.of( "xread", null, clusterClient.xread(Map.of("abc", "stream1", "zxy", "stream2"))), Arguments.of("copy", "6.2.0", clusterClient.copy("abc", "def", true)), @@ -973,6 +987,7 @@ public void flushall() { .contains("can't write against a read only replica")); } + // TODO: add a binary version of this test @SneakyThrows @ParameterizedTest(name = "functionLoad: singleNodeRoute = {0}") @ValueSource(booleans = {true, false}) @@ -1122,6 +1137,7 @@ public void function_commands_without_keys_with_route(boolean singleNodeRoute) { assertEquals(OK, clusterClient.functionFlush(route).get()); } + // TODO: add a binary version of this test @SneakyThrows @ParameterizedTest(name = "functionLoad: singleNodeRoute = {0}") @ValueSource(booleans = {true, false}) @@ -1139,7 +1155,7 @@ public void function_commands_without_keys_with_route_binary(boolean singleNodeR assertEquals(libName, clusterClient.functionLoad(code, false, route).get()); var fcallResult = - clusterClient.fcall(funcName.toString(), new String[] {"one", "two"}, route).get(); + clusterClient.fcall(funcName, new GlideString[] {gs("one"), gs("two")}, route).get(); if (route instanceof SingleNodeRoute) { assertEquals("one", fcallResult.getSingleValue()); } else { @@ -1148,7 +1164,9 @@ public void function_commands_without_keys_with_route_binary(boolean singleNodeR } } fcallResult = - clusterClient.fcallReadOnly(funcName.toString(), new String[] {"one", "two"}, route).get(); + clusterClient + .fcallReadOnly(funcName, new GlideString[] {gs("one"), gs("two")}, route) + .get(); if (route instanceof SingleNodeRoute) { assertEquals("one", fcallResult.getSingleValue()); } else { @@ -1271,7 +1289,7 @@ public void function_commands_without_keys_with_route_binary(boolean singleNodeR } fcallResult = - clusterClient.fcall(newFuncName.toString(), new String[] {"one", "two"}, route).get(); + clusterClient.fcall(newFuncName, new GlideString[] {gs("one"), gs("two")}, route).get(); if (route instanceof SingleNodeRoute) { assertEquals(2L, fcallResult.getSingleValue()); } else { @@ -1281,7 +1299,7 @@ public void function_commands_without_keys_with_route_binary(boolean singleNodeR } fcallResult = clusterClient - .fcallReadOnly(newFuncName.toString(), new String[] {"one", "two"}, route) + .fcallReadOnly(newFuncName, new GlideString[] {gs("one"), gs("two")}, route) .get(); if (route instanceof SingleNodeRoute) { assertEquals(2L, fcallResult.getSingleValue()); @@ -1395,9 +1413,10 @@ public void function_commands_without_keys_and_without_route_binary() { assertEquals(libName, clusterClient.functionLoad(code, false).get()); assertEquals( - "one", clusterClient.fcall(funcName.toString(), new String[] {"one", "two"}).get()); + "one", clusterClient.fcall(funcName, new GlideString[] {gs("one"), gs("two")}).get()); assertEquals( - "one", clusterClient.fcallReadOnly(funcName.toString(), new String[] {"one", "two"}).get()); + "one", + clusterClient.fcallReadOnly(funcName, new GlideString[] {gs("one"), gs("two")}).get()); var flist = clusterClient.functionList(false).get(); var expectedDescription = @@ -1469,9 +1488,10 @@ public void function_commands_without_keys_and_without_route_binary() { Optional.of(newCode.toString())); assertEquals( - 2L, clusterClient.fcall(newFuncName.toString(), new String[] {"one", "two"}).get()); + 2L, clusterClient.fcall(newFuncName, new GlideString[] {gs("one"), gs("two")}).get()); assertEquals( - 2L, clusterClient.fcallReadOnly(newFuncName.toString(), new String[] {"one", "two"}).get()); + 2L, + clusterClient.fcallReadOnly(newFuncName, new GlideString[] {gs("one"), gs("two")}).get()); assertEquals(OK, clusterClient.functionFlush(ASYNC).get()); } @@ -1517,6 +1537,54 @@ public void fcall_with_keys(String prefix) { assertEquals(OK, clusterClient.functionDelete(libName, route).get()); } + @ParameterizedTest + @ValueSource(strings = {"abc", "xyz", "kln"}) + @SneakyThrows + public void fcall_binary_with_keys(String prefix) { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + + String key = "{" + prefix + "}-fcall_with_keys-"; + SingleNodeRoute route = new SlotKeyRoute(key, PRIMARY); + String libName = "mylib_with_keys"; + GlideString funcName = gs("myfunc_with_keys"); + // function $funcName returns array with first two arguments + String code = + generateLuaLibCode(libName, Map.of(funcName.toString(), "return {keys[1], keys[2]}"), true); + + // loading function to the node where key is stored + assertEquals(libName, clusterClient.functionLoad(code, false, route).get()); + + // due to common prefix, all keys are mapped to the same hash slot + var functionResult = + clusterClient + .fcall(funcName, new GlideString[] {gs(key + 1), gs(key + 2)}, new GlideString[0]) + .get(); + assertArrayEquals(new Object[] {key + 1, key + 2}, (Object[]) functionResult); + functionResult = + clusterClient + .fcallReadOnly( + funcName, new GlideString[] {gs(key + 1), gs(key + 2)}, new GlideString[0]) + .get(); + assertArrayEquals(new Object[] {key + 1, key + 2}, (Object[]) functionResult); + + // TODO: change to binary transaction version once available: + // var transaction = + // new ClusterTransaction() + // .fcall(funcName, new String[] {key + 1, key + 2}, new String[0]) + // .fcallReadOnly(funcName, new String[] {key + 1, key + 2}, new String[0]); + + // // check response from a routed transaction request + // assertDeepEquals( + // new Object[][] {{key + 1, key + 2}, {key + 1, key + 2}}, + // clusterClient.exec(transaction, route).get()); + // // if no route given, GLIDE should detect it automatically + // assertDeepEquals( + // new Object[][] {{key + 1, key + 2}, {key + 1, key + 2}}, + // clusterClient.exec(transaction).get()); + + assertEquals(OK, clusterClient.functionDelete(libName, route).get()); + } + @SneakyThrows @Test public void fcall_readonly_function() { @@ -1572,6 +1640,61 @@ public void fcall_readonly_function() { assertEquals(OK, clusterClient.functionDelete(libName).get()); } + @SneakyThrows + @Test + public void fcall_readonly_binary_function() { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + + String libName = "fcall_readonly_function"; + // intentionally using a REPLICA route + Route replicaRoute = new SlotKeyRoute(libName, REPLICA); + Route primaryRoute = new SlotKeyRoute(libName, PRIMARY); + GlideString funcName = gs("fcall_readonly_function"); + + // function $funcName returns a magic number + String code = generateLuaLibCode(libName, Map.of(funcName.toString(), "return 42"), false); + + assertEquals(libName, clusterClient.functionLoad(code, false).get()); + + // fcall on a replica node should fail, because a function isn't guaranteed to be RO + var executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.fcall(funcName, replicaRoute).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException.getMessage().contains("You can't write against a read only replica.")); + + // fcall_ro also fails + executionException = + assertThrows( + ExecutionException.class, + () -> clusterClient.fcallReadOnly(funcName, replicaRoute).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException.getMessage().contains("You can't write against a read only replica.")); + + // fcall_ro also fails to run it even on primary - another error + executionException = + assertThrows( + ExecutionException.class, + () -> clusterClient.fcallReadOnly(funcName, primaryRoute).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException + .getMessage() + .contains("Can not execute a script with write flag using *_ro command.")); + + // create the same function, but with RO flag + code = generateLuaLibCode(libName, Map.of(funcName.toString(), "return 42"), true); + + assertEquals(libName, clusterClient.functionLoad(code, true).get()); + + // fcall should succeed now + assertEquals(42L, clusterClient.fcall(funcName, replicaRoute).get().getSingleValue()); + + assertEquals(OK, clusterClient.functionDelete(libName).get()); + } + @Test @SneakyThrows public void functionStats_and_functionKill_without_route() { diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 2c1cf86aab..a01da0d41e 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -587,11 +587,13 @@ public void function_commands_binary() { assertEquals(libName, regularClient.functionLoad(code, false).get()); var functionResult = - regularClient.fcall(funcName.toString(), new String[0], new String[] {"one", "two"}).get(); + regularClient + .fcall(funcName, new GlideString[0], new GlideString[] {gs("one"), gs("two")}) + .get(); assertEquals("one", functionResult); functionResult = regularClient - .fcallReadOnly(funcName.toString(), new String[0], new String[] {"one", "two"}) + .fcallReadOnly(funcName, new GlideString[0], new GlideString[] {gs("one"), gs("two")}) .get(); assertEquals("one", functionResult); @@ -665,12 +667,13 @@ public void function_commands_binary() { functionResult = regularClient - .fcall(newFuncName.toString(), new String[0], new String[] {"one", "two"}) + .fcall(newFuncName, new GlideString[0], new GlideString[] {gs("one"), gs("two")}) .get(); assertEquals(2L, functionResult); functionResult = regularClient - .fcallReadOnly(newFuncName.toString(), new String[0], new String[] {"one", "two"}) + .fcallReadOnly( + newFuncName, new GlideString[0], new GlideString[] {gs("one"), gs("two")}) .get(); assertEquals(2L, functionResult);