Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java: Add FCALL command. #1543

Merged
merged 6 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -346,6 +347,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 @@ -526,6 +528,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 @@ -152,6 +153,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 @@ -220,7 +222,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 @@ -1750,6 +1753,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 @@ -3707,6 +3708,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) {
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -5087,6 +5088,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
Loading