Skip to content

Commit

Permalink
Java: Add FCALL command. (#1543)
Browse files Browse the repository at this point in the history
* Java: Add `FCALL` command. (#307)

Signed-off-by: Yury-Fridlyand <[email protected]>
  • Loading branch information
Yury-Fridlyand authored Jun 11, 2024
1 parent 529a3a3 commit 24fb145
Show file tree
Hide file tree
Showing 16 changed files with 523 additions and 33 deletions.
1 change: 1 addition & 0 deletions glide-core/src/protobuf/redis_request.proto
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ enum RequestType {
FunctionList = 151;
FunctionDelete = 152;
FunctionFlush = 153;
FCall = 154;
LMPop = 155;
ExpireTime = 156;
PExpireTime = 157;
Expand Down
3 changes: 3 additions & 0 deletions glide-core/src/request_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ pub enum RequestType {
FunctionList = 151,
FunctionDelete = 152,
FunctionFlush = 153,
FCall = 154,
LMPop = 155,
ExpireTime = 156,
PExpireTime = 157,
Expand Down Expand Up @@ -347,6 +348,7 @@ impl From<::protobuf::EnumOrUnknown<ProtobufRequestType>> for RequestType {
ProtobufRequestType::FunctionList => RequestType::FunctionList,
ProtobufRequestType::FunctionDelete => RequestType::FunctionDelete,
ProtobufRequestType::FunctionFlush => RequestType::FunctionFlush,
ProtobufRequestType::FCall => RequestType::FCall,
ProtobufRequestType::BitPos => RequestType::BitPos,
ProtobufRequestType::BitOp => RequestType::BitOp,
ProtobufRequestType::HStrlen => RequestType::HStrlen,
Expand Down Expand Up @@ -528,6 +530,7 @@ impl RequestType {
RequestType::FunctionList => Some(get_two_word_command("FUNCTION", "LIST")),
RequestType::FunctionDelete => Some(get_two_word_command("FUNCTION", "DELETE")),
RequestType::FunctionFlush => Some(get_two_word_command("FUNCTION", "FLUSH")),
RequestType::FCall => Some(cmd("FCALL")),
RequestType::BitPos => Some(cmd("BITPOS")),
RequestType::BitOp => Some(cmd("BITOP")),
RequestType::HStrlen => Some(cmd("HSTRLEN")),
Expand Down
13 changes: 12 additions & 1 deletion java/client/src/main/java/glide/api/BaseClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.Expire;
import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt;
import static redis_request.RedisRequestOuterClass.RequestType.ExpireTime;
import static redis_request.RedisRequestOuterClass.RequestType.FCall;
import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd;
import static redis_request.RedisRequestOuterClass.RequestType.GeoDist;
import static redis_request.RedisRequestOuterClass.RequestType.GeoHash;
Expand Down Expand Up @@ -153,6 +154,7 @@
import glide.api.commands.HashBaseCommands;
import glide.api.commands.HyperLogLogBaseCommands;
import glide.api.commands.ListBaseCommands;
import glide.api.commands.ScriptingAndFunctionsBaseCommands;
import glide.api.commands.SetBaseCommands;
import glide.api.commands.SortedSetBaseCommands;
import glide.api.commands.StreamBaseCommands;
Expand Down Expand Up @@ -221,7 +223,8 @@ public abstract class BaseClient
SortedSetBaseCommands,
StreamBaseCommands,
HyperLogLogBaseCommands,
GeospatialIndicesBaseCommands {
GeospatialIndicesBaseCommands,
ScriptingAndFunctionsBaseCommands {

/** Redis simple string response with "OK" */
public static final String OK = ConstantResponse.OK.toString();
Expand Down Expand Up @@ -1771,6 +1774,14 @@ public CompletableFuture<Long> sintercard(@NonNull String[] keys, long limit) {
return commandManager.submitNewCommand(SInterCard, arguments, this::handleLongResponse);
}

@Override
public CompletableFuture<Object> fcall(
@NonNull String function, @NonNull String[] keys, @NonNull String[] arguments) {
String[] args =
concatenateArrays(new String[] {function, Long.toString(keys.length)}, keys, arguments);
return commandManager.submitNewCommand(FCall, args, this::handleObjectOrNullResponse);
}

@Override
public CompletableFuture<Boolean> copy(
@NonNull String source, @NonNull String destination, boolean replace) {
Expand Down
5 changes: 5 additions & 0 deletions java/client/src/main/java/glide/api/RedisClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,11 @@ public CompletableFuture<String> functionDelete(@NonNull String libName) {
FunctionDelete, new String[] {libName}, this::handleStringResponse);
}

@Override
public CompletableFuture<Object> fcall(@NonNull String function) {
return fcall(function, new String[0], new String[0]);
}

@Override
public CompletableFuture<Boolean> copy(
@NonNull String source, @NonNull String destination, long destinationDB) {
Expand Down
32 changes: 32 additions & 0 deletions java/client/src/main/java/glide/api/RedisClusterClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand;
import static redis_request.RedisRequestOuterClass.RequestType.DBSize;
import static redis_request.RedisRequestOuterClass.RequestType.Echo;
import static redis_request.RedisRequestOuterClass.RequestType.FCall;
import static redis_request.RedisRequestOuterClass.RequestType.FlushAll;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush;
Expand Down Expand Up @@ -543,4 +544,35 @@ public CompletableFuture<String> functionDelete(@NonNull String libName, @NonNul
return commandManager.submitNewCommand(
FunctionDelete, new String[] {libName}, route, this::handleStringResponse);
}

@Override
public CompletableFuture<Object> fcall(@NonNull String function) {
return fcall(function, new String[0]);
}

@Override
public CompletableFuture<ClusterValue<Object>> fcall(
@NonNull String function, @NonNull Route route) {
return fcall(function, new String[0], route);
}

@Override
public CompletableFuture<Object> 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<ClusterValue<Object>> fcall(
@NonNull String function, @NonNull String[] arguments, @NonNull Route route) {
String[] args = concatenateArrays(new String[] {function, "0"}, arguments); // 0 - key count
return commandManager.submitNewCommand(
FCall,
args,
route,
response ->
route instanceof SingleNodeRoute
? ClusterValue.ofSingleValue(handleObjectOrNullResponse(response))
: ClusterValue.ofMultiValue(handleMapResponse(response)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
package glide.api.commands;

import java.util.concurrent.CompletableFuture;

/**
* Supports commands and transactions for the "Scripting and Function" group for standalone and
* cluster clients.
*
* @see <a href="https://redis.io/docs/latest/commands/?group=scripting">Scripting and Function
* Commands</a>
*/
public interface ScriptingAndFunctionsBaseCommands {

/**
* Invokes a previously loaded function.
*
* @apiNote When in cluster mode
* <ul>
* <li>all <code>keys</code> must map to the same hash slot.
* <li>if no <code>keys</code> are given, command will be routed to a random node.
* </ul>
*
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/docs/latest/commands/fcall/">redis.io</a> for details.
* @param function The function name.
* @param keys An <code>array</code> 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 <code>keys</code>.
* @param arguments An <code>array</code> of <code>function</code> arguments. <code>Arguments
* </code> should not represent names of keys.
* @return The invoked function's return value.
* @example
* <pre>{@code
* String[] args = new String[] { "Answer", "to", "the", "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything"};
* Object response = client.fcall("Deep_Thought", new String[0], args).get();
* assert response == 42L;
* }</pre>
*/
CompletableFuture<Object> fcall(String function, String[] keys, String[] arguments);
}
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,78 @@ CompletableFuture<ClusterValue<Map<String, Object>[]>> functionList(
* }</pre>
*/
CompletableFuture<String> functionDelete(String libName, Route route);

/**
* Invokes a previously loaded function.<br>
* The command will be routed to a random node.
*
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/docs/latest/commands/fcall/">redis.io</a> for details.
* @param function The function name.
* @return The invoked function's return value.
* @example
* <pre>{@code
* Object response = client.fcall("Deep_Thought").get();
* assert response == 42L;
* }</pre>
*/
CompletableFuture<Object> fcall(String function);

/**
* Invokes a previously loaded function.
*
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/docs/latest/commands/fcall/">redis.io</a> 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 <code>route</code>.
* @return The invoked function's return value wrapped by a {@link ClusterValue}.
* @example
* <pre>{@code
* ClusterValue<Object> response = client.fcall("Deep_Thought", ALL_NODES).get();
* for (Object nodeResponse : response.getMultiValue().values()) {
* assert nodeResponse == 42L;
* }
* }</pre>
*/
CompletableFuture<ClusterValue<Object>> fcall(String function, Route route);

/**
* Invokes a previously loaded function.<br>
* The command will be routed to a random node.
*
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/docs/latest/commands/fcall/">redis.io</a> for details.
* @param function The function name.
* @param arguments An <code>array</code> of <code>function</code> arguments. <code>Arguments
* </code> should not represent names of keys.
* @return The invoked function's return value.
* @example
* <pre>{@code
* String[] args = new String[] { "Answer", "to", "the", "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything" };
* Object response = client.fcall("Deep_Thought", args).get();
* assert response == 42L;
* }</pre>
*/
CompletableFuture<Object> fcall(String function, String[] arguments);

/**
* Invokes a previously loaded function.
*
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/docs/latest/commands/fcall/">redis.io</a> for details.
* @param function The function name.
* @param arguments An <code>array</code> of <code>function</code> arguments. <code>Arguments
* </code> 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 <code>route</code>.
* @return The invoked function's return value wrapped by a {@link ClusterValue}.
* @example
* <pre>{@code
* String[] args = new String[] { "Answer", "to", "the", "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything" };
* ClusterValue<Object> response = client.fcall("Deep_Thought", args, RANDOM).get();
* assert response.getSingleValue() == 42L;
* }</pre>
*/
CompletableFuture<ClusterValue<Object>> fcall(String function, String[] arguments, Route route);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import java.util.concurrent.CompletableFuture;

/**
* Supports commands and transactions for the "Scripting and Function" group for standalone and
* cluster clients.
* Supports commands and transactions for the "Scripting and Function" group for a standalone
* client.
*
* @see <a href="https://redis.io/docs/latest/commands/?group=scripting">Scripting and Function
* Commands</a>
Expand Down Expand Up @@ -127,4 +127,19 @@ public interface ScriptingAndFunctionsCommands {
* }</pre>
*/
CompletableFuture<String> functionDelete(String libName);

/**
* Invokes a previously loaded function.
*
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/docs/latest/commands/fcall/">redis.io</a> for details.
* @param function The function name.
* @return The invoked function's return value.
* @example
* <pre>{@code
* Object response = client.fcall("Deep_Thought").get();
* assert response == 42L;
* }</pre>
*/
CompletableFuture<Object> fcall(String function);
}
37 changes: 37 additions & 0 deletions java/client/src/main/java/glide/api/models/BaseTransaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.Expire;
import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt;
import static redis_request.RedisRequestOuterClass.RequestType.ExpireTime;
import static redis_request.RedisRequestOuterClass.RequestType.FCall;
import static redis_request.RedisRequestOuterClass.RequestType.FlushAll;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush;
Expand Down Expand Up @@ -3769,6 +3770,42 @@ public T functionList(@NonNull String libNamePattern, boolean withCode) {
return getThis();
}

/**
* Invokes a previously loaded function.
*
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/docs/latest/commands/fcall/">redis.io</a> for details.
* @param function The function name.
* @param keys An <code>array</code> of key arguments 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 <code>keys</code>.
* @param arguments An <code>array</code> of <code>function</code> arguments. <code>Arguments
* </code> should not represent names of keys.
* @return Command Response - The invoked function's return value.
*/
public T fcall(@NonNull String function, @NonNull String[] keys, @NonNull String[] arguments) {
ArgsArray commandArgs =
buildArgs(
concatenateArrays(
new String[] {function, Long.toString(keys.length)}, keys, arguments));
protobufTransaction.addCommands(buildCommand(FCall, commandArgs));
return getThis();
}

/**
* Invokes a previously loaded function.
*
* @since Redis 7.0 and above.
* @see <a href="https://redis.io/docs/latest/commands/fcall/">redis.io</a> for details.
* @param function The function name.
* @param arguments An <code>array</code> of <code>function</code> arguments. <code>Arguments
* </code> should not represent names of keys.
* @return Command Response - The invoked function's return value.
*/
public T fcall(@NonNull String function, @NonNull String[] arguments) {
return fcall(function, new String[0], arguments);
}

/**
* Sets or clears the bit at <code>offset</code> in the string value stored at <code>key</code>.
* The <code>offset</code> is a zero-based index, with <code>0</code> being the first element of
Expand Down
47 changes: 47 additions & 0 deletions java/client/src/test/java/glide/api/RedisClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.Expire;
import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt;
import static redis_request.RedisRequestOuterClass.RequestType.ExpireTime;
import static redis_request.RedisRequestOuterClass.RequestType.FCall;
import static redis_request.RedisRequestOuterClass.RequestType.FlushAll;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush;
Expand Down Expand Up @@ -5152,6 +5153,52 @@ public void functionDelete_returns_success() {
assertEquals(OK, payload);
}

@SneakyThrows
@Test
public void fcall_with_keys_and_args_returns_success() {
// setup
String function = "func";
String[] keys = new String[] {"key1", "key2"};
String[] arguments = new String[] {"1", "2"};
String[] args = new String[] {function, "2", "key1", "key2", "1", "2"};
Object value = "42";
CompletableFuture<Object> testResponse = new CompletableFuture<>();
testResponse.complete(value);

// match on protobuf request
when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse);

// exercise
CompletableFuture<Object> response = service.fcall(function, keys, arguments);
Object payload = response.get();

// verify
assertEquals(testResponse, response);
assertEquals(value, payload);
}

@SneakyThrows
@Test
public void fcall_returns_success() {
// setup
String function = "func";
String[] args = new String[] {function, "0"};
Object value = "42";
CompletableFuture<Object> testResponse = new CompletableFuture<>();
testResponse.complete(value);

// match on protobuf request
when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse);

// exercise
CompletableFuture<Object> response = service.fcall(function);
Object payload = response.get();

// verify
assertEquals(testResponse, response);
assertEquals(value, payload);
}

@SneakyThrows
@Test
public void bitcount_returns_success() {
Expand Down
Loading

0 comments on commit 24fb145

Please sign in to comment.