diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 386ca2e047..dcc07a0229 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -2,39 +2,36 @@ package glide.api; import static glide.ffi.resolvers.SocketListenerResolver.getSocket; +import static redis_request.RedisRequestOuterClass.RequestType.GetString; +import static redis_request.RedisRequestOuterClass.RequestType.Ping; +import static redis_request.RedisRequestOuterClass.RequestType.SetString; +import glide.api.commands.ConnectionCommands; +import glide.api.commands.StringCommands; +import glide.api.models.commands.SetOptions; import glide.api.models.configuration.BaseClientConfiguration; +import glide.api.models.exceptions.RedisException; import glide.connectors.handlers.CallbackDispatcher; import glide.connectors.handlers.ChannelHandler; import glide.ffi.resolvers.RedisValueResolver; import glide.managers.BaseCommandResponseResolver; import glide.managers.CommandManager; import glide.managers.ConnectionManager; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.BiFunction; import lombok.AllArgsConstructor; +import org.apache.commons.lang3.ArrayUtils; import response.ResponseOuterClass.Response; /** Base Client class for Redis */ @AllArgsConstructor -public abstract class BaseClient implements AutoCloseable { +public abstract class BaseClient implements AutoCloseable, StringCommands, ConnectionCommands { protected final ConnectionManager connectionManager; protected final CommandManager commandManager; - /** - * Extracts the response from the Protobuf response and either throws an exception or returns the - * appropriate response as an Object - * - * @param response Redis protobuf message - * @return Response Object - */ - protected Object handleObjectResponse(Response response) { - // convert protobuf response into Object and then Object into T - return new BaseCommandResponseResolver(RedisValueResolver::valueFromPointer).apply(response); - } - /** * Async request for an async (non-blocking) Redis client. * @@ -66,8 +63,8 @@ protected static CompletableFuture CreateClient( * Closes this resource, relinquishing any underlying resources. This method is invoked * automatically on objects managed by the try-with-resources statement. * - *

see: AutoCloseable::close() + * @see AutoCloseable::close() */ @Override public void close() throws ExecutionException { @@ -91,4 +88,115 @@ protected static ConnectionManager buildConnectionManager(ChannelHandler channel protected static CommandManager buildCommandManager(ChannelHandler channelHandler) { return new CommandManager(channelHandler); } + + /** + * Extracts the response from the Protobuf response and either throws an exception or returns the + * appropriate response as an Object. + * + * @param response Redis protobuf message + * @return Response Object + */ + protected Object handleObjectResponse(Response response) { + // convert protobuf response into Object + return new BaseCommandResponseResolver(RedisValueResolver::valueFromPointer).apply(response); + } + + /** + * Checks that the Response is empty. + * + * @return An empty response + */ + protected Void handleVoidResponse(Response response) { + Object value = handleObjectResponse(response); + if (value == null) { + return null; + } + throw new RedisException( + "Unexpected return type from Redis: got " + + value.getClass().getSimpleName() + + " expected null"); + } + + /** + * Extracts the response value from the Redis response and either throws an exception or returns + * the value as a String. + * + * @param response Redis protobuf message + * @return Response as a String + */ + protected String handleStringResponse(Response response) { + Object value = handleObjectResponse(response); + if (value instanceof String) { + return (String) value; + } + throw new RedisException( + "Unexpected return type from Redis: got " + + value.getClass().getSimpleName() + + " expected String"); + } + + /** + * Extracts the response value from the Redis response and either throws an exception or returns + * the value as an Object[]. + * + * @param response Redis protobuf message + * @return Response as an Object[] + */ + protected Object[] handleArrayResponse(Response response) { + Object value = handleObjectResponse(response); + if (value instanceof Object[]) { + return (Object[]) value; + } + throw new RedisException( + "Unexpected return type from Redis: got " + + value.getClass().getSimpleName() + + " expected Object[]"); + } + + /** + * Extracts the response value from the Redis response and either throws an exception or returns + * the value as a Map. + * + * @param response Redis protobuf message + * @return Response as a Map. + */ + @SuppressWarnings("unchecked") + protected Map handleMapResponse(Response response) { + Object value = handleObjectResponse(response); + if (value instanceof Map) { + return (Map) value; + } + throw new RedisException( + "Unexpected return type from Redis: got " + + value.getClass().getSimpleName() + + " expected Map"); + } + + @Override + public CompletableFuture ping() { + return commandManager.submitNewCommand(Ping, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture ping(String msg) { + return commandManager.submitNewCommand(Ping, new String[] {msg}, this::handleStringResponse); + } + + @Override + public CompletableFuture get(String key) { + return commandManager.submitNewCommand( + GetString, new String[] {key}, this::handleStringResponse); + } + + @Override + public CompletableFuture set(String key, String value) { + return commandManager.submitNewCommand( + SetString, new String[] {key, value}, this::handleVoidResponse); + } + + @Override + public CompletableFuture set(String key, String value, SetOptions options) { + String[] arguments = ArrayUtils.addAll(new String[] {key, value}, options.toArgs()); + return commandManager.submitNewCommand(SetString, arguments, this::handleStringResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index c39ebfb753..23da34a58a 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -1,18 +1,26 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; +import static redis_request.RedisRequestOuterClass.RequestType.Info; + import glide.api.commands.BaseCommands; +import glide.api.commands.ConnectionCommands; +import glide.api.commands.ServerCommands; +import glide.api.models.Transaction; +import glide.api.models.commands.InfoOptions; import glide.api.models.configuration.RedisClientConfiguration; +import glide.connectors.handlers.ChannelHandler; import glide.managers.CommandManager; import glide.managers.ConnectionManager; -import glide.managers.models.Command; import java.util.concurrent.CompletableFuture; /** * Async (non-blocking) client for Redis in Standalone mode. Use {@link #CreateClient} to request a * client to Redis. */ -public class RedisClient extends BaseClient implements BaseCommands { +public class RedisClient extends BaseClient + implements BaseCommands, ConnectionCommands, ServerCommands { protected RedisClient(ConnectionManager connectionManager, CommandManager commandManager) { super(connectionManager, commandManager); @@ -22,16 +30,42 @@ protected RedisClient(ConnectionManager connectionManager, CommandManager comman * Async request for an async (non-blocking) Redis client in Standalone mode. * * @param config Redis client Configuration - * @return a Future to connect and return a RedisClient + * @return A Future to connect and return a RedisClient */ public static CompletableFuture CreateClient(RedisClientConfiguration config) { - return CreateClient(config, RedisClient::new); + try { + ChannelHandler channelHandler = buildChannelHandler(); + ConnectionManager connectionManager = buildConnectionManager(channelHandler); + CommandManager commandManager = buildCommandManager(channelHandler); + // TODO: Support exception throwing, including interrupted exceptions + return connectionManager + .connectToRedis(config) + .thenApply(ignore -> new RedisClient(connectionManager, commandManager)); + } catch (InterruptedException e) { + // Something bad happened while we were establishing netty connection to UDS + var future = new CompletableFuture(); + future.completeExceptionally(e); + return future; + } + } + + @Override + public CompletableFuture customCommand(String... args) { + return commandManager.submitNewCommand(CustomCommand, args, this::handleStringResponse); + } + + @Override + public CompletableFuture exec(Transaction transaction) { + return commandManager.submitNewCommand(transaction, this::handleArrayResponse); + } + + @Override + public CompletableFuture info() { + return commandManager.submitNewCommand(Info, new String[0], this::handleStringResponse); } @Override - public CompletableFuture customCommand(String[] args) { - Command command = - Command.builder().requestType(Command.RequestType.CUSTOM_COMMAND).arguments(args).build(); - return commandManager.submitNewCommand(command, this::handleObjectResponse); + public CompletableFuture info(InfoOptions options) { + return commandManager.submitNewCommand(Info, options.toArgs(), this::handleStringResponse); } } diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index 948ee7240b..8eb789404a 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -1,21 +1,29 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; +import static redis_request.RedisRequestOuterClass.RequestType.Info; + import glide.api.commands.ClusterBaseCommands; +import glide.api.commands.ClusterServerCommands; +import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; +import glide.api.models.commands.InfoOptions; import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.Route; +import glide.connectors.handlers.ChannelHandler; import glide.managers.CommandManager; import glide.managers.ConnectionManager; -import glide.managers.models.Command; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; /** * Async (non-blocking) client for Redis in Cluster mode. Use {@link #CreateClient} to request a * client to Redis. */ -public class RedisClusterClient extends BaseClient implements ClusterBaseCommands { +public class RedisClusterClient extends BaseClient + implements ClusterBaseCommands, ClusterServerCommands { protected RedisClusterClient(ConnectionManager connectionManager, CommandManager commandManager) { super(connectionManager, commandManager); @@ -25,37 +33,94 @@ protected RedisClusterClient(ConnectionManager connectionManager, CommandManager * Async request for an async (non-blocking) Redis client in Cluster mode. * * @param config Redis cluster client Configuration - * @return a Future to connect and return a ClusterClient + * @return A Future to connect and return a RedisClusterClient */ public static CompletableFuture CreateClient( RedisClusterClientConfiguration config) { - return CreateClient(config, RedisClusterClient::new); + try { + ChannelHandler channelHandler = buildChannelHandler(); + ConnectionManager connectionManager = buildConnectionManager(channelHandler); + CommandManager commandManager = buildCommandManager(channelHandler); + // TODO: Support exception throwing, including interrupted exceptions + return connectionManager + .connectToRedis(config) + .thenApply(ignored -> new RedisClusterClient(connectionManager, commandManager)); + } catch (InterruptedException e) { + // Something bad happened while we were establishing netty connection to UDS + var future = new CompletableFuture(); + future.completeExceptionally(e); + return future; + } } @Override - public CompletableFuture> customCommand(String[] args) { - Command command = - Command.builder().requestType(Command.RequestType.CUSTOM_COMMAND).arguments(args).build(); + public CompletableFuture> customCommand(String... args) { // TODO if a command returns a map as a single value, ClusterValue misleads user return commandManager.submitNewCommand( - command, response -> ClusterValue.of(handleObjectResponse(response))); + CustomCommand, + args, + Optional.empty(), + response -> ClusterValue.of(handleObjectResponse(response))); } @Override @SuppressWarnings("unchecked") - public CompletableFuture> customCommand(String[] args, Route route) { - Command command = - Command.builder() - .requestType(Command.RequestType.CUSTOM_COMMAND) - .arguments(args) - .route(route) - .build(); - + public CompletableFuture> customCommand(Route route, String... args) { return commandManager.submitNewCommand( - command, + CustomCommand, + args, + Optional.ofNullable(route), response -> route.isSingleNodeRoute() ? ClusterValue.ofSingleValue(handleObjectResponse(response)) : ClusterValue.ofMultiValue((Map) handleObjectResponse(response))); } + + @Override + public CompletableFuture exec(ClusterTransaction transaction) { + return commandManager.submitNewCommand( + transaction, Optional.empty(), this::handleArrayResponse); + } + + @Override + public CompletableFuture exec(ClusterTransaction transaction, Route route) { + return commandManager.submitNewCommand( + transaction, Optional.ofNullable(route), this::handleArrayResponse); + } + + @Override + public CompletableFuture> info() { + return commandManager.submitNewCommand( + Info, + new String[0], + Optional.empty(), + response -> ClusterValue.of(handleStringResponse(response))); + } + + @Override + public CompletableFuture> info(Route route) { + return commandManager.submitNewCommand( + Info, + new String[0], + Optional.ofNullable(route), + response -> ClusterValue.of(handleStringResponse(response))); + } + + @Override + public CompletableFuture> info(InfoOptions options) { + return commandManager.submitNewCommand( + Info, + options.toArgs(), + Optional.empty(), + response -> ClusterValue.of(handleStringResponse(response))); + } + + @Override + public CompletableFuture> info(InfoOptions options, Route route) { + return commandManager.submitNewCommand( + Info, + options.toArgs(), + Optional.ofNullable(route), + response -> ClusterValue.of(handleStringResponse(response))); + } } diff --git a/java/client/src/main/java/glide/api/commands/BaseCommands.java b/java/client/src/main/java/glide/api/commands/BaseCommands.java index e47bf70fce..4b57e756f8 100644 --- a/java/client/src/main/java/glide/api/commands/BaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/BaseCommands.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.Transaction; import java.util.concurrent.CompletableFuture; /** Base Commands interface to handle generic command and transaction requests. */ @@ -11,16 +12,34 @@ public interface BaseCommands { * subcommands, should be added as a separate value in args. * * @remarks This function should only be used for single-response commands. Commands that don't - * return response (such as SUBSCRIBE), or that return potentially more than a single response - * (such as XREAD), or that change the client's behavior (such as entering pub/sub mode on - * RESP2 connections) shouldn't be called using this function. + * return response (such as SUBSCRIBE), or that return potentially more than a single + * response (such as XREAD), or that change the client's behavior (such as entering + * pub/sub mode on RESP2 connections) shouldn't be called using + * this function. * @example Returns a list of all pub/sub clients: *
-     * Object result = client.customCommand(new String[]{"CLIENT","LIST","TYPE", "PUBSUB"}).get();
+     * Object result = client.customCommand("CLIENT","LIST","TYPE", "PUBSUB").get();
      * 
* - * @param args arguments for the custom command - * @return a CompletableFuture with response result from Redis + * @param args Arguments for the custom command. + * @return Response from Redis containing an Object. */ - CompletableFuture customCommand(String[] args); + CompletableFuture customCommand(String... args); + + /** + * Execute a transaction by processing the queued commands. + * + * @see redis.io for details on Redis + * Transactions. + * @param transaction A {@link Transaction} object containing a list of commands to be executed. + * @return A list of results corresponding to the execution of each command in the transaction. + * @remarks + *
    + *
  • If a command returns a value, it will be included in the list. If a command doesn't + * return a value, the list entry will be empty. + *
  • If the transaction failed due to a WATCH command, exec will + * return null. + *
+ */ + CompletableFuture exec(Transaction transaction); } diff --git a/java/client/src/main/java/glide/api/commands/ClusterBaseCommands.java b/java/client/src/main/java/glide/api/commands/ClusterBaseCommands.java index b58808e0fe..88daefd26c 100644 --- a/java/client/src/main/java/glide/api/commands/ClusterBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ClusterBaseCommands.java @@ -1,7 +1,9 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; +import glide.api.models.Transaction; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import java.util.concurrent.CompletableFuture; @@ -21,12 +23,12 @@ public interface ClusterBaseCommands { * this function. * @example Returns a list of all pub/sub clients: *

- * Object result = client.customCommand(new String[]{ "CLIENT", "LIST", "TYPE", "PUBSUB" }).get(); + * Object result = client.customCommand("CLIENT", "LIST", "TYPE", "PUBSUB").get(); * - * @param args Arguments for the custom command including the command name - * @return A CompletableFuture with response result from Redis + * @param args Arguments for the custom command including the command name. + * @return Response from Redis containing an Object. */ - CompletableFuture> customCommand(String[] args); + CompletableFuture> customCommand(String... args); /** * Executes a single command, without checking inputs. Every part of the command, including @@ -39,11 +41,46 @@ public interface ClusterBaseCommands { * this function. * @example Returns a list of all pub/sub clients: *

- * Object result = client.customCommand(new String[]{ "CLIENT", "LIST", "TYPE", "PUBSUB" }).get(); + * Object result = client.customCommand("CLIENT", "LIST", "TYPE", "PUBSUB").get(); * + * @param route Routing configuration for the command * @param args Arguments for the custom command including the command name + * @return Response from Redis containing an Object. + */ + CompletableFuture> customCommand(Route route, String... args); + + /** + * Execute a transaction by processing the queued commands. + * + * @see redis.io for details on Redis + * Transactions. + * @param transaction A {@link Transaction} object containing a list of commands to be executed. + * @return A list of results corresponding to the execution of each command in the transaction. + * @remarks + *

    + *
  • If a command returns a value, it will be included in the list. If a command doesn't + * return a value, the list entry will be empty. + *
  • If the transaction failed due to a WATCH command, exec will + * return null. + *
+ */ + CompletableFuture exec(ClusterTransaction transaction); + + /** + * Execute a transaction by processing the queued commands. + * + * @see redis.io for details on Redis + * Transactions. + * @param transaction A {@link Transaction} object containing a list of commands to be executed. * @param route Routing configuration for the command - * @return A CompletableFuture with response result from Redis + * @return A list of results corresponding to the execution of each command in the transaction. + * @remarks + *
    + *
  • If a command returns a value, it will be included in the list. If a command doesn't + * return a value, the list entry will be empty. + *
  • If the transaction failed due to a WATCH command, exec will + * return null. + *
*/ - CompletableFuture> customCommand(String[] args, Route route); + CompletableFuture exec(ClusterTransaction transaction, Route route); } diff --git a/java/client/src/main/java/glide/api/commands/ClusterServerCommands.java b/java/client/src/main/java/glide/api/commands/ClusterServerCommands.java new file mode 100644 index 0000000000..de47e0b59b --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/ClusterServerCommands.java @@ -0,0 +1,57 @@ +package glide.api.commands; + +import glide.api.models.ClusterValue; +import glide.api.models.commands.InfoOptions; +import glide.api.models.configuration.RequestRoutingConfiguration.Route; +import java.util.concurrent.CompletableFuture; + +/** + * Server Management Commands interface. + * + * @see Server Management Commands + */ +public interface ClusterServerCommands { + + /** + * Get information and statistics about the Redis server. DEFAULT option is assumed + * + * @see redis.io for details. {@link + * InfoOptions.Section#DEFAULT} option is assumed. + * @return Response from Redis cluster containing a String. + */ + CompletableFuture> info(); + + /** + * Get information and statistics about the Redis server. DEFAULT option is assumed + * + * @see redis.io for details. + * @param route Routing configuration for the command + * @return Response from Redis cluster containing a String. + */ + CompletableFuture> info(Route route); + + /** + * Get information and statistics about the Redis server. + * + * @see redis.io for details. + * @param options - A list of {@link InfoOptions.Section} values specifying which sections of + * information to retrieve. When no parameter is provided, the {@link + * InfoOptions.Section#DEFAULT} option is assumed. + * @return Response from Redis cluster containing a String with the requested + * Sections. + */ + CompletableFuture> info(InfoOptions options); + + /** + * Get information and statistics about the Redis server. + * + * @see redis.io for details. + * @param options - A list of {@link InfoOptions.Section} values specifying which sections of + * information to retrieve. When no parameter is provided, the {@link + * InfoOptions.Section#DEFAULT} option is assumed. + * @param route Routing configuration for the command + * @return Response from Redis cluster containing a String with the requested + * Sections. + */ + CompletableFuture> info(InfoOptions options, Route route); +} diff --git a/java/client/src/main/java/glide/api/commands/ConnectionCommands.java b/java/client/src/main/java/glide/api/commands/ConnectionCommands.java new file mode 100644 index 0000000000..4e3f22eaac --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/ConnectionCommands.java @@ -0,0 +1,28 @@ +package glide.api.commands; + +import java.util.concurrent.CompletableFuture; + +/** + * Connection Management Commands interface. + * + * @see: Connection Management Commands + */ +public interface ConnectionCommands { + + /** + * Ping the Redis server. + * + * @see redis.io for details. + * @return Response from Redis containing a String. + */ + CompletableFuture ping(); + + /** + * Ping the Redis server. + * + * @see redis.io for details. + * @param msg The ping argument that will be returned. + * @return Response from Redis containing a String. + */ + CompletableFuture ping(String msg); +} diff --git a/java/client/src/main/java/glide/api/commands/ServerCommands.java b/java/client/src/main/java/glide/api/commands/ServerCommands.java new file mode 100644 index 0000000000..ac6160e76c --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/ServerCommands.java @@ -0,0 +1,32 @@ +package glide.api.commands; + +import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.InfoOptions.Section; +import java.util.concurrent.CompletableFuture; + +/** + * Server Management Commands interface. + * + * @see Server Management Commands + */ +public interface ServerCommands { + + /** + * Get information and statistics about the Redis server. No argument is provided, so the {@link + * Section#DEFAULT} option is assumed. + * + * @see redis.io for details. + * @return Response from Redis containing a String. + */ + CompletableFuture info(); + + /** + * Get information and statistics about the Redis server. + * + * @see redis.io for details. + * @param options A list of {@link Section} values specifying which sections of information to + * retrieve. When no parameter is provided, the {@link Section#DEFAULT} option is assumed. + * @return Response from Redis containing a String with the requested sections. + */ + CompletableFuture info(InfoOptions options); +} diff --git a/java/client/src/main/java/glide/api/commands/StringCommands.java b/java/client/src/main/java/glide/api/commands/StringCommands.java new file mode 100644 index 0000000000..d441c5e6af --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/StringCommands.java @@ -0,0 +1,45 @@ +package glide.api.commands; + +import glide.api.models.commands.SetOptions; +import glide.api.models.commands.SetOptions.ConditionalSet; +import glide.api.models.commands.SetOptions.SetOptionsBuilder; +import java.util.concurrent.CompletableFuture; + +/** String Commands interface to handle single commands that return Strings. */ +public interface StringCommands { + + /** + * Get the value associated with the given key, or null if no such value exists. + * + * @see redis.io for details. + * @param key The key to retrieve from the database. + * @return Response from Redis. key exists, returns the value of + * key as a String. Otherwise, return null. + */ + CompletableFuture get(String key); + + /** + * Set the given key with the given value. + * + * @see redis.io for details. + * @param key The key to store. + * @param value The value to store with the given key. + * @return Response from Redis. + */ + CompletableFuture set(String key, String value); + + /** + * Set the given key with the given value. Return value is dependent on the passed options. + * + * @see redis.io for details. + * @param key The key to store. + * @param value The value to store with the given key. + * @param options The Set options. + * @return Response from Redis containing a String or null response. The + * old value as a String if {@link SetOptionsBuilder#returnOldValue(boolean)} is + * set. Otherwise, if the value isn't set because of {@link ConditionalSet#ONLY_IF_EXISTS} or + * {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST} conditions, return null. + * Otherwise, return OK. + */ + CompletableFuture set(String key, String value, SetOptions options); +} diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java new file mode 100644 index 0000000000..3c717fc860 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -0,0 +1,187 @@ +package glide.api.models; + +import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; +import static redis_request.RedisRequestOuterClass.RequestType.GetString; +import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.Ping; +import static redis_request.RedisRequestOuterClass.RequestType.SetString; + +import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.InfoOptions.Section; +import glide.api.models.commands.SetOptions; +import glide.api.models.commands.SetOptions.ConditionalSet; +import glide.api.models.commands.SetOptions.SetOptionsBuilder; +import lombok.Getter; +import org.apache.commons.lang3.ArrayUtils; +import redis_request.RedisRequestOuterClass.Command; +import redis_request.RedisRequestOuterClass.Command.ArgsArray; +import redis_request.RedisRequestOuterClass.RequestType; +import redis_request.RedisRequestOuterClass.Transaction; + +/** + * Base class encompassing shared commands for both standalone and cluster mode implementations in a + * transaction. Transactions allow the execution of a group of commands in a single step. + * + *

Command Response: An array of command responses is returned by the client exec command, in the + * order they were given. Each element in the array represents a command given to the transaction. + * The response for each command depends on the executed Redis command. Specific response types are + * documented alongside each method. + * + * @param child typing for chaining method calls + */ +@Getter +public abstract class BaseTransaction> { + /** Command class to send a single request to Redis. */ + protected final Transaction.Builder transactionBuilder = Transaction.newBuilder(); + + protected abstract T getThis(); + + /** + * Executes a single command, without checking inputs. Every part of the command, including + * subcommands, should be added as a separate value in args. + * + * @remarks This function should only be used for single-response commands. Commands that don't + * return response (such as SUBSCRIBE), or that return potentially more than a single + * response (such as XREAD), or that change the client's behavior (such as entering + * pub/sub mode on RESP2 connections) shouldn't be called using + * this function. + * @example Returns a list of all pub/sub clients: + *

+     * Object result = client.customCommand(new String[]{"CLIENT","LIST","TYPE", "PUBSUB"}).get();
+     * 
+ * + * @param args Arguments for the custom command. + * @return A response from Redis with an Object. + */ + public T customCommand(String[] args) { + ArgsArray commandArgs = buildArgs(args); + + transactionBuilder.addCommands(buildCommand(CustomCommand, commandArgs)); + return getThis(); + } + + /** + * Ping the Redis server. + * + * @see redis.io for details. + * @return A response from Redis with a String. + */ + public T ping() { + transactionBuilder.addCommands(buildCommand(Ping)); + return getThis(); + } + + /** + * Ping the Redis server. + * + * @see redis.io for details. + * @param msg The ping argument that will be returned. + * @return A response from Redis with a String. + */ + public T ping(String msg) { + ArgsArray commandArgs = buildArgs(msg); + + transactionBuilder.addCommands(buildCommand(Ping, commandArgs)); + return getThis(); + } + + /** + * Get information and statistics about the Redis server. No argument is provided, so the {@link + * Section#DEFAULT} option is assumed. + * + * @see redis.io for details. + * @return A response from Redis with a String. + */ + public T info() { + transactionBuilder.addCommands(buildCommand(Info)); + return getThis(); + } + + /** + * Get information and statistics about the Redis server. + * + * @see redis.io for details. + * @param options A list of {@link Section} values specifying which sections of information to + * retrieve. When no parameter is provided, the {@link Section#DEFAULT} option is assumed. + * @return Response from Redis with a String containing the requested {@link + * Section}s. + */ + public T info(InfoOptions options) { + ArgsArray commandArgs = buildArgs(options.toArgs()); + + transactionBuilder.addCommands(buildCommand(Info, commandArgs)); + return getThis(); + } + + /** + * Get the value associated with the given key, or null if no such value exists. + * + * @see redis.io for details. + * @param key The key to retrieve from the database. + * @return Response from Redis. key exists, returns the value of + * key as a String. Otherwise, return null. + */ + public T get(String key) { + ArgsArray commandArgs = buildArgs(key); + + transactionBuilder.addCommands(buildCommand(GetString, commandArgs)); + return getThis(); + } + + /** + * Set the given key with the given value. + * + * @see redis.io for details. + * @param key The key to store. + * @param value The value to store with the given key. + * @return Response from Redis. + */ + public T set(String key, String value) { + ArgsArray commandArgs = buildArgs(key, value); + + transactionBuilder.addCommands(buildCommand(SetString, commandArgs)); + return getThis(); + } + + /** + * Set the given key with the given value. Return value is dependent on the passed options. + * + * @see redis.io for details. + * @param key The key to store. + * @param value The value to store with the given key. + * @param options The Set options. + * @return Response from Redis with a String or null response. The old + * value as a String if {@link SetOptionsBuilder#returnOldValue(boolean)} is set. + * Otherwise, if the value isn't set because of {@link ConditionalSet#ONLY_IF_EXISTS} or + * {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST} conditions, return null. + * Otherwise, return OK. + */ + public T set(String key, String value, SetOptions options) { + ArgsArray commandArgs = + buildArgs(ArrayUtils.addAll(new String[] {key, value}, options.toArgs())); + + transactionBuilder.addCommands(buildCommand(SetString, commandArgs)); + return getThis(); + } + + /** Build protobuf {@link Command} object for given command and arguments. */ + protected Command buildCommand(RequestType requestType) { + return Command.newBuilder().setRequestType(requestType).build(); + } + + /** Build protobuf {@link Command} object for given command and arguments. */ + protected Command buildCommand(RequestType requestType, ArgsArray args) { + return Command.newBuilder().setRequestType(requestType).setArgsArray(args).build(); + } + + /** Build protobuf {@link ArgsArray} object for given arguments. */ + protected ArgsArray buildArgs(String... stringArgs) { + ArgsArray.Builder commandArgs = ArgsArray.newBuilder(); + + for (String string : stringArgs) { + commandArgs.addArgs(string); + } + + return commandArgs.build(); + } +} diff --git a/java/client/src/main/java/glide/api/models/ClusterTransaction.java b/java/client/src/main/java/glide/api/models/ClusterTransaction.java new file mode 100644 index 0000000000..8f3977e171 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/ClusterTransaction.java @@ -0,0 +1,29 @@ +package glide.api.models; + +import lombok.AllArgsConstructor; + +/** + * Extends BaseTransaction class for cluster mode commands. Transactions allow the execution of a + * group of commands in a single step. + * + *

Command Response: An array of command responses is returned by the client exec + * command, in the order they were given. Each element in the array represents a command given to + * the Transaction. The response for each command depends on the executed Redis + * command. Specific response types are documented alongside each method. + * + * @example + *

+ *  ClusterTransaction transaction = new ClusterTransaction();
+ *    .set("key", "value");
+ *    .get("key");
+ *  ClusterValue[] result = client.exec(transaction, route).get();
+ *  // result contains: OK and "value"
+ *  
+ */ +@AllArgsConstructor +public class ClusterTransaction extends BaseTransaction { + @Override + protected ClusterTransaction getThis() { + return this; + } +} diff --git a/java/client/src/main/java/glide/api/models/ClusterValue.java b/java/client/src/main/java/glide/api/models/ClusterValue.java index a690f154c5..9a425c80fd 100644 --- a/java/client/src/main/java/glide/api/models/ClusterValue.java +++ b/java/client/src/main/java/glide/api/models/ClusterValue.java @@ -1,14 +1,21 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import glide.api.models.configuration.RequestRoutingConfiguration.Route; import java.util.Map; /** - * union-like type which can store single-value or multi-value retrieved from Redis. The - * multi-value, if defined, contains the routed value as a Map containing a cluster - * node address to cluster node value. + * Represents a Response object from a Redis server with cluster-mode enabled. The response type may + * depend on the submitted {@link Route}. * - * @param The wrapped data type + * @remark ClusterValue stores values in a union-like object. It contains a single-value or + * multi-value response from Redis. If the command's routing is to one node use {@link + * #getSingleValue()} to return a response of type T. Otherwise, use {@link + * #getMultiValue()} to return a Map of address: nodeResponse where + * address is of type string and nodeResponse is of type + * T. + * @see Redis cluster specification + * @param The wrapped response type */ public class ClusterValue { private Map multiValue = null; diff --git a/java/client/src/main/java/glide/api/models/Transaction.java b/java/client/src/main/java/glide/api/models/Transaction.java new file mode 100644 index 0000000000..c6f009cedc --- /dev/null +++ b/java/client/src/main/java/glide/api/models/Transaction.java @@ -0,0 +1,29 @@ +package glide.api.models; + +import lombok.AllArgsConstructor; + +/** + * Extends BaseTransaction class for Redis standalone commands. Transactions allow the execution of + * a group of commands in a single step. + * + *

Command Response: An array of command responses is returned by the client exec + * command, in the order they were given. Each element in the array represents a command given to + * the Transaction. The response for each command depends on the executed Redis + * command. Specific response types are documented alongside each method. + * + * @example + *

+ *  Transaction transaction = new Transaction()
+ *    .transaction.set("key", "value");
+ *    .transaction.get("key");
+ *  Object[] result = client.exec(transaction).get();
+ *  // result contains: OK and "value"
+ *  
+ */ +@AllArgsConstructor +public class Transaction extends BaseTransaction { + @Override + protected Transaction getThis() { + return this; + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/InfoOptions.java b/java/client/src/main/java/glide/api/models/commands/InfoOptions.java new file mode 100644 index 0000000000..e94340a984 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/InfoOptions.java @@ -0,0 +1,63 @@ +package glide.api.models.commands; + +import glide.api.commands.ServerCommands; +import java.util.List; +import lombok.Builder; +import lombok.Singular; + +/** + * Optional arguments to {@link ServerCommands#info(InfoOptions)} + * + * @see redis.io + */ +@Builder +public final class InfoOptions { + + @Singular private final List
sections; + + public enum Section { + /** SERVER: General information about the Redis server */ + SERVER, + /** CLIENTS: Client connections section */ + CLIENTS, + /** MEMORY: Memory consumption related information */ + MEMORY, + /** PERSISTENCE: RDB and AOF related information */ + PERSISTENCE, + /** STATS: General statistics */ + STATS, + /** REPLICATION: Master/replica replication information */ + REPLICATION, + /** CPU: CPU consumption statistics */ + CPU, + /** COMMANDSTATS: Redis command statistics */ + COMMANDSTATS, + /** LATENCYSTATS: Redis command latency percentile distribution statistics */ + LATENCYSTATS, + /** SENTINEL: Redis Sentinel section (only applicable to Sentinel instances) */ + SENTINEL, + /** CLUSTER: Redis Cluster section */ + CLUSTER, + /** MODULES: Modules section */ + MODULES, + /** KEYSPACE: Database related statistics */ + KEYSPACE, + /** ERRORSTATS: Redis error statistics */ + ERRORSTATS, + /** ALL: Return all sections (excluding module generated ones) */ + ALL, + /** DEFAULT: Return only the default set of sections */ + DEFAULT, + /** EVERYTHING: Includes all and modules */ + EVERYTHING, + } + + /** + * Converts options enum into a String[] to add to a Redis request. + * + * @return String[] + */ + public String[] toArgs() { + return sections.stream().map(Object::toString).toArray(String[]::new); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/SetOptions.java b/java/client/src/main/java/glide/api/models/commands/SetOptions.java new file mode 100644 index 0000000000..261076891e --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SetOptions.java @@ -0,0 +1,129 @@ +package glide.api.models.commands; + +import glide.api.commands.StringCommands; +import glide.api.models.exceptions.RequestException; +import java.util.LinkedList; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import redis_request.RedisRequestOuterClass.Command; + +/** + * Optional arguments for {@link StringCommands#set(String, String, SetOptions)} command. + * + * @see redis.io + */ +@Builder +public final class SetOptions { + + /** + * If conditionalSet is not set the value will be set regardless of prior value + * existence. If value isn't set because of the condition, command will return null. + */ + private final ConditionalSet conditionalSet; + + /** + * Set command to return the old string stored at key, or null if + * key did not exist. An error is returned and SET aborted if the value stored + * at key + * is not a string. Equivalent to GET in the Redis API. + */ + private final boolean returnOldValue; + + /** If not set, no expiry time will be set for the value. */ + private final TimeToLive expiry; + + /** Conditions which define whether new value should be set or not. */ + @RequiredArgsConstructor + @Getter + public enum ConditionalSet { + /** + * Only set the key if it does not already exist. Equivalent to XX in the Redis + * API. + */ + ONLY_IF_EXISTS("XX"), + /** Only set the key if it already exists. Equivalent to NX in the Redis API. */ + ONLY_IF_DOES_NOT_EXIST("NX"); + + private final String redisApi; + } + + /** Configuration of value lifetime. */ + @Builder + public static final class TimeToLive { + /** Expiry type for the time to live */ + @NonNull private TimeToLiveType type; + + /** + * The amount of time to live before the key expires. Ignored when {@link + * TimeToLiveType#KEEP_EXISTING} type is set. + */ + private Integer count; + } + + /** Types of value expiration configuration. */ + @RequiredArgsConstructor + @Getter + public enum TimeToLiveType { + /** + * Retain the time to live associated with the key. Equivalent to KEEPTTL in the + * Redis API. + */ + KEEP_EXISTING("KEEPTTL"), + /** + * Set the specified expire time, in seconds. Equivalent to EX in the Redis API. + */ + SECONDS("EX"), + /** + * Set the specified expire time, in milliseconds. Equivalent to PX in the Redis + * API. + */ + MILLISECONDS("PX"), + /** + * Set the specified Unix time at which the key will expire, in seconds. Equivalent to + * EXAT in the Redis API. + */ + UNIX_SECONDS("EXAT"), + /** + * Set the specified Unix time at which the key will expire, in milliseconds. Equivalent to + * PXAT in the Redis API. + */ + UNIX_MILLISECONDS("PXAT"); + + private final String redisApi; + } + + /** String representation of {@link #returnOldValue} when set. */ + public static String RETURN_OLD_VALUE = "GET"; + + /** + * Converts SetOptions into a String[] to add to a {@link Command} arguments. + * + * @return String[] + */ + public String[] toArgs() { + List optionArgs = new LinkedList<>(); + if (conditionalSet != null) { + optionArgs.add(conditionalSet.redisApi); + } + + if (returnOldValue) { + optionArgs.add(RETURN_OLD_VALUE); + } + + if (expiry != null) { + optionArgs.add(expiry.type.redisApi); + if (expiry.type != TimeToLiveType.KEEP_EXISTING) { + if (expiry.count == null) { + throw new RequestException( + "Set command received expiry type " + expiry.type + ", but count was not set."); + } + optionArgs.add(expiry.count.toString()); + } + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/configuration/RequestRoutingConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/RequestRoutingConfiguration.java index acb729af72..cfaf7dc5ca 100644 --- a/java/client/src/main/java/glide/api/models/configuration/RequestRoutingConfiguration.java +++ b/java/client/src/main/java/glide/api/models/configuration/RequestRoutingConfiguration.java @@ -3,6 +3,8 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +import redis_request.RedisRequestOuterClass.SimpleRoutes; +import redis_request.RedisRequestOuterClass.SlotTypes; /** Request routing configuration. */ public class RequestRoutingConfiguration { @@ -20,13 +22,17 @@ public interface Route { boolean isSingleNodeRoute(); } + @RequiredArgsConstructor + @Getter public enum SimpleRoute implements Route { /** Route request to all nodes. */ - ALL_NODES, + ALL_NODES(SimpleRoutes.AllNodes), /** Route request to all primary nodes. */ - ALL_PRIMARIES, + ALL_PRIMARIES(SimpleRoutes.AllPrimaries), /** Route request to a random node. */ - RANDOM; + RANDOM(SimpleRoutes.Random); + + private final SimpleRoutes protobufMapping; @Override public boolean isSingleNodeRoute() { @@ -34,9 +40,13 @@ public boolean isSingleNodeRoute() { } } + @RequiredArgsConstructor + @Getter public enum SlotType { - PRIMARY, - REPLICA, + PRIMARY(SlotTypes.Primary), + REPLICA(SlotTypes.Replica); + + private final SlotTypes slotTypes; } /** diff --git a/java/client/src/main/java/glide/connectors/handlers/ChannelHandler.java b/java/client/src/main/java/glide/connectors/handlers/ChannelHandler.java index 869f0f7c78..f726ad0be2 100644 --- a/java/client/src/main/java/glide/connectors/handlers/ChannelHandler.java +++ b/java/client/src/main/java/glide/connectors/handlers/ChannelHandler.java @@ -69,18 +69,18 @@ public ChannelHandler( /** * Complete a protobuf message and write it to the channel (to UDS). * - * @param request Incomplete request, function completes it by setting callback ID + * @param requestBuilder Incomplete request, function completes it by setting callback ID * @param flush True to flush immediately * @return A response promise */ - public CompletableFuture write(RedisRequest.Builder request, boolean flush) { + public CompletableFuture write(RedisRequest.Builder requestBuilder, boolean flush) { var commandId = callbackDispatcher.registerRequest(); - request.setCallbackIdx(commandId.getKey()); + requestBuilder.setCallbackIdx(commandId.getKey()); if (flush) { - channel.writeAndFlush(request.build()); + channel.writeAndFlush(requestBuilder.build()); } else { - channel.write(request.build()); + channel.write(requestBuilder.build()); } return commandId.getValue(); } diff --git a/java/client/src/main/java/glide/connectors/resources/ThreadPoolAllocator.java b/java/client/src/main/java/glide/connectors/resources/ThreadPoolAllocator.java index e6a00b4d50..bef56b9c05 100644 --- a/java/client/src/main/java/glide/connectors/resources/ThreadPoolAllocator.java +++ b/java/client/src/main/java/glide/connectors/resources/ThreadPoolAllocator.java @@ -40,7 +40,6 @@ public static EventLoopGroup createOrGetNettyThreadPool( () -> new EpollEventLoopGroup(threadCount, new DefaultThreadFactory(name, true))); } // TODO support IO-Uring and NIO - throw new RuntimeException("Current platform supports no known thread pool types"); } diff --git a/java/client/src/main/java/glide/managers/CommandManager.java b/java/client/src/main/java/glide/managers/CommandManager.java index 36663893d1..6fb2ee97ed 100644 --- a/java/client/src/main/java/glide/managers/CommandManager.java +++ b/java/client/src/main/java/glide/managers/CommandManager.java @@ -1,6 +1,7 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.managers; +import glide.api.models.ClusterTransaction; +import glide.api.models.Transaction; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute; import glide.api.models.configuration.RequestRoutingConfiguration.SlotIdRoute; @@ -8,17 +9,16 @@ import glide.api.models.exceptions.ClosingException; import glide.connectors.handlers.CallbackDispatcher; import glide.connectors.handlers.ChannelHandler; -import glide.managers.models.Command; import java.util.Optional; import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import redis_request.RedisRequestOuterClass; +import redis_request.RedisRequestOuterClass.Command; import redis_request.RedisRequestOuterClass.Command.ArgsArray; import redis_request.RedisRequestOuterClass.RedisRequest; import redis_request.RedisRequestOuterClass.RequestType; import redis_request.RedisRequestOuterClass.Routes; import redis_request.RedisRequestOuterClass.SimpleRoutes; -import redis_request.RedisRequestOuterClass.SlotTypes; import response.ResponseOuterClass.Response; /** @@ -34,21 +34,83 @@ public class CommandManager { /** * Build a command and send. * - * @param command The command to execute + * @param requestType Redis command type + * @param arguments Redis command arguments + * @param route Command routing parameters * @param responseHandler The handler for the response object * @return A result promise of type T */ public CompletableFuture submitNewCommand( - Command command, RedisExceptionCheckedFunction responseHandler) { + RequestType requestType, + String[] arguments, + Optional route, + RedisExceptionCheckedFunction responseHandler) { + + RedisRequest.Builder command = prepareRedisRequest(requestType, arguments, route); + return submitNewCommand(command, responseHandler); + } + + /** + * Build a command and send. + * + * @param requestType Redis command type + * @param arguments Redis command arguments + * @param responseHandler The handler for the response object + * @return A result promise of type T + */ + public CompletableFuture submitNewCommand( + RedisRequestOuterClass.RequestType requestType, + String[] arguments, + RedisExceptionCheckedFunction responseHandler) { + + RedisRequest.Builder command = prepareRedisRequest(requestType, arguments); + return submitNewCommand(command, responseHandler); + } + + /** + * Build a Transaction and send. + * + * @param transaction Redis Transaction request with multiple commands + * @param responseHandler The handler for the response object + * @return A result promise of type T + */ + public CompletableFuture submitNewCommand( + Transaction transaction, RedisExceptionCheckedFunction responseHandler) { + + RedisRequest.Builder command = prepareRedisRequest(transaction); + return submitNewCommand(command, responseHandler); + } + + /** + * Build a Transaction and send. + * + * @param transaction Redis Transaction request with multiple commands + * @param route Command routing parameters + * @param responseHandler The handler for the response object + * @return A result promise of type T + */ + public CompletableFuture submitNewCommand( + ClusterTransaction transaction, + Optional route, + RedisExceptionCheckedFunction responseHandler) { + + RedisRequest.Builder command = prepareRedisRequest(transaction, route); + return submitNewCommand(command, responseHandler); + } + + /** + * Take a redis request and send to channel. + * + * @param command The Redis command request as a builder to execute + * @param responseHandler The handler for the response object + * @return A result promise of type T + */ + protected CompletableFuture submitNewCommand( + RedisRequest.Builder command, RedisExceptionCheckedFunction responseHandler) { // write command request to channel // when complete, convert the response to our expected type T using the given responseHandler return channel - .write( - prepareRedisRequest( - command.getRequestType(), - command.getArguments(), - Optional.ofNullable(command.getRoute())), - true) + .write(command, true) .exceptionally(this::exceptionHandler) .thenApplyAsync(responseHandler::apply); } @@ -71,68 +133,91 @@ private Response exceptionHandler(Throwable e) { } /** - * Build a protobuf command/transaction request object.
- * Used by {@link CommandManager}. + * Build a protobuf command request object with routing options.
* - * @param command - Redis command - * @param args - Redis command arguments as string array - * @return An uncompleted request. CallbackDispatcher is responsible to complete it by adding a - * callback id. + * @param requestType Redis command type + * @param arguments Redis command arguments + * @param route Command routing parameters + * @return An uncompleted request. {@link CallbackDispatcher} is responsible to complete it by + * adding a callback id. */ - private RedisRequestOuterClass.RedisRequest.Builder prepareRedisRequest( - Command.RequestType command, String[] args) { - RedisRequestOuterClass.Command.ArgsArray.Builder commandArgs = - RedisRequestOuterClass.Command.ArgsArray.newBuilder(); - for (var arg : args) { + protected RedisRequest.Builder prepareRedisRequest( + RequestType requestType, String[] arguments, Optional route) { + ArgsArray.Builder commandArgs = ArgsArray.newBuilder(); + for (var arg : arguments) { commandArgs.addArgs(arg); } - // TODO: set route properly when no RouteOptions given - return RedisRequestOuterClass.RedisRequest.newBuilder() - .setSingleCommand( - RedisRequestOuterClass.Command.newBuilder() - .setRequestType(mapRequestTypes(command)) - .setArgsArray(commandArgs.build()) - .build()) - .setRoute( - RedisRequestOuterClass.Routes.newBuilder() - .setSimpleRoutes(RedisRequestOuterClass.SimpleRoutes.AllNodes) - .build()); - } + var builder = + RedisRequest.newBuilder() + .setSingleCommand( + Command.newBuilder() + .setRequestType(requestType) + .setArgsArray(commandArgs.build()) + .build()); - private RequestType mapRequestTypes(Command.RequestType inType) { - switch (inType) { - case CUSTOM_COMMAND: - return RequestType.CustomCommand; - } - throw new RuntimeException("Unsupported request type"); + return prepareRedisRequestRoute(builder, route); } /** - * Build a protobuf command/transaction request object with routing options.
- * Used by {@link CommandManager}. + * Build a protobuf command request object with routing options.
* - * @param command Redis command type - * @param args Redis command arguments - * @param route Command routing parameters + * @param requestType Redis command type + * @param arguments Redis command arguments * @return An uncompleted request. {@link CallbackDispatcher} is responsible to complete it by * adding a callback id. */ - private RedisRequest.Builder prepareRedisRequest( - Command.RequestType command, String[] args, Optional route) { + protected RedisRequest.Builder prepareRedisRequest(RequestType requestType, String[] arguments) { ArgsArray.Builder commandArgs = ArgsArray.newBuilder(); - for (var arg : args) { + for (var arg : arguments) { commandArgs.addArgs(arg); } var builder = RedisRequest.newBuilder() .setSingleCommand( - RedisRequestOuterClass.Command.newBuilder() - .setRequestType(mapRequestTypes(command)) + Command.newBuilder() + .setRequestType(requestType) .setArgsArray(commandArgs.build()) .build()); + return prepareRedisRequestRoute(builder, Optional.empty()); + } + + /** + * Build a protobuf transaction request object with routing options.
+ * + * @param transaction Redis transaction with commands + * @return An uncompleted request. {@link CallbackDispatcher} is responsible to complete it by + * adding a callback id. + */ + protected RedisRequest.Builder prepareRedisRequest(Transaction transaction) { + + var builder = + RedisRequest.newBuilder().setTransaction(transaction.getTransactionBuilder().build()); + + return prepareRedisRequestRoute(builder, Optional.empty()); + } + + /** + * Build a protobuf transaction request object with routing options.
+ * + * @param transaction Redis transaction with commands + * @param route Command routing parameters + * @return An uncompleted request. {@link CallbackDispatcher} is responsible to complete it by + * adding a callback id. + */ + protected RedisRequest.Builder prepareRedisRequest( + ClusterTransaction transaction, Optional route) { + + var builder = + RedisRequest.newBuilder().setTransaction(transaction.getTransactionBuilder().build()); + + return prepareRedisRequestRoute(builder, route); + } + + private RedisRequest.Builder prepareRedisRequestRoute( + RedisRequest.Builder builder, Optional route) { if (route.isEmpty()) { return builder; } @@ -149,7 +234,7 @@ private RedisRequest.Builder prepareRedisRequest( RedisRequestOuterClass.SlotIdRoute.newBuilder() .setSlotId(((SlotIdRoute) route.get()).getSlotId()) .setSlotType( - SlotTypes.forNumber( + RedisRequestOuterClass.SlotTypes.forNumber( ((SlotIdRoute) route.get()).getSlotType().ordinal())))); } else if (route.get() instanceof SlotKeyRoute) { builder.setRoute( @@ -158,7 +243,7 @@ private RedisRequest.Builder prepareRedisRequest( RedisRequestOuterClass.SlotKeyRoute.newBuilder() .setSlotKey(((SlotKeyRoute) route.get()).getSlotKey()) .setSlotType( - SlotTypes.forNumber( + RedisRequestOuterClass.SlotTypes.forNumber( ((SlotKeyRoute) route.get()).getSlotType().ordinal())))); } else { throw new IllegalArgumentException("Unknown type of route"); diff --git a/java/client/src/main/java/glide/managers/models/Command.java b/java/client/src/main/java/glide/managers/models/Command.java index 4b45f38593..8b13789179 100644 --- a/java/client/src/main/java/glide/managers/models/Command.java +++ b/java/client/src/main/java/glide/managers/models/Command.java @@ -1,29 +1 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.managers.models; -import glide.api.models.configuration.RequestRoutingConfiguration.Route; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NonNull; - -/** Base Command class to send a single request to Redis. */ -@Builder -@Getter -@EqualsAndHashCode -public class Command { - - /** Redis command request type */ - @NonNull final RequestType requestType; - - /** Request routing configuration */ - final Route route; - - /** List of Arguments for the Redis command request */ - @Builder.Default final String[] arguments = new String[] {}; - - public enum RequestType { - /** Call a custom command with list of string arguments */ - CUSTOM_COMMAND, - } -} diff --git a/java/client/src/test/java/glide/ExceptionHandlingTests.java b/java/client/src/test/java/glide/ExceptionHandlingTests.java index df77fa72ef..771f6aed75 100644 --- a/java/client/src/test/java/glide/ExceptionHandlingTests.java +++ b/java/client/src/test/java/glide/ExceptionHandlingTests.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static response.ResponseOuterClass.RequestErrorType.Disconnect; import static response.ResponseOuterClass.RequestErrorType.ExecAbort; import static response.ResponseOuterClass.RequestErrorType.Timeout; @@ -24,8 +25,6 @@ import glide.managers.BaseCommandResponseResolver; import glide.managers.CommandManager; import glide.managers.ConnectionManager; -import glide.managers.models.Command; -import glide.managers.models.Command.RequestType; import io.netty.channel.ChannelFuture; import java.io.IOException; import java.util.concurrent.CancellationException; @@ -84,7 +83,7 @@ public void channel_is_closed_when_disconnected_on_command() { var channelHandler = new TestChannelHandler(callbackDispatcher); var commandManager = new CommandManager(channelHandler); - var future = commandManager.submitNewCommand(createDummyCommand(), r -> null); + var future = commandManager.submitNewCommand(CustomCommand, new String[0], r -> null); callbackDispatcher.completeRequest(null); var exception = assertThrows(ExecutionException.class, future::get); // a ClosingException thrown from CallbackDispatcher::completeRequest and then @@ -101,7 +100,7 @@ public void channel_is_not_closed_when_error_was_in_command_pipeline() { var channelHandler = new TestChannelHandler(callbackDispatcher); var commandManager = new CommandManager(channelHandler); - var future = commandManager.submitNewCommand(createDummyCommand(), r -> null); + var future = commandManager.submitNewCommand(CustomCommand, new String[0], r -> null); callbackDispatcher.completeRequest(null); var exception = assertThrows(ExecutionException.class, future::get); // a RequestException thrown from CallbackDispatcher::completeRequest and then @@ -118,7 +117,7 @@ public void command_manager_rethrows_non_RedisException_too() { var channelHandler = new TestChannelHandler(callbackDispatcher); var commandManager = new CommandManager(channelHandler); - var future = commandManager.submitNewCommand(createDummyCommand(), r -> null); + var future = commandManager.submitNewCommand(CustomCommand, new String[0], r -> null); callbackDispatcher.completeRequest(null); var exception = assertThrows(ExecutionException.class, future::get); // a IOException thrown from CallbackDispatcher::completeRequest and then wrapped @@ -199,7 +198,7 @@ public void dont_close_connection_when_callback_dispatcher_receives_response_wit var channelHandler = new TestChannelHandler(callbackDispatcher); var commandManager = new CommandManager(channelHandler); - var future = commandManager.submitNewCommand(createDummyCommand(), r -> null); + var future = commandManager.submitNewCommand(CustomCommand, new String[0], r -> null); var response = Response.newBuilder() .setCallbackIdx(0) @@ -269,10 +268,6 @@ private static RedisClientConfiguration createDummyConfig() { return RedisClientConfiguration.builder().build(); } - private static Command createDummyCommand() { - return Command.builder().requestType(RequestType.CUSTOM_COMMAND).build(); - } - /** Test ChannelHandler extension which allows to validate whether the channel was closed. */ private static class TestChannelHandler extends ChannelHandler { diff --git a/java/client/src/test/java/glide/api/RedisClientCreateTest.java b/java/client/src/test/java/glide/api/RedisClientCreateTest.java index 463e4db9db..4d1d12d275 100644 --- a/java/client/src/test/java/glide/api/RedisClientCreateTest.java +++ b/java/client/src/test/java/glide/api/RedisClientCreateTest.java @@ -73,7 +73,7 @@ public void createClient_with_config_successfully_returns_RedisClient() { @SneakyThrows @Test - public void createClient_error_on_connection_throws_ExecutionException() { + public void createClient_throws_ExecutionException() { // setup CompletableFuture connectToRedisFuture = new CompletableFuture<>(); ClosingException exception = new ClosingException("disconnected"); diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index ff8c356cff..3bfe4afa12 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -1,18 +1,35 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; +import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; +import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; +import static glide.api.models.commands.SetOptions.TimeToLiveType.KEEP_EXISTING; +import static glide.api.models.commands.SetOptions.TimeToLiveType.UNIX_SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; +import static redis_request.RedisRequestOuterClass.RequestType.GetString; +import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.Ping; +import static redis_request.RedisRequestOuterClass.RequestType.SetString; +import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SetOptions; import glide.managers.CommandManager; import glide.managers.ConnectionManager; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; +import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import redis_request.RedisRequestOuterClass; public class RedisClientTest { @@ -29,18 +46,26 @@ public void setUp() { service = new RedisClient(connectionManager, commandManager); } + @SneakyThrows @Test - public void customCommand_success() throws ExecutionException, InterruptedException { + public void customCommand_returns_success() { // setup String key = "testKey"; Object value = "testValue"; String cmd = "GETSTRING"; + String[] arguments = new String[] {cmd, key}; CompletableFuture testResponse = mock(CompletableFuture.class); when(testResponse.get()).thenReturn(value); - when(commandManager.submitNewCommand(any(), any())).thenReturn(testResponse); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(RedisRequestOuterClass.RedisRequest.Builder.class); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(CustomCommand), eq(arguments), any())) + .thenReturn(testResponse); // exercise - CompletableFuture response = service.customCommand(new String[] {cmd, key}); + CompletableFuture response = service.customCommand(arguments); String payload = (String) response.get(); // verify @@ -48,27 +73,243 @@ public void customCommand_success() throws ExecutionException, InterruptedExcept assertEquals(value, payload); } + @SneakyThrows @Test - public void customCommand_interruptedException() throws ExecutionException, InterruptedException { + public void customCommand_throws_InterruptedException() { // setup String key = "testKey"; Object value = "testValue"; String cmd = "GETSTRING"; + String[] arguments = new String[] {cmd, key}; CompletableFuture testResponse = mock(CompletableFuture.class); InterruptedException interruptedException = new InterruptedException(); when(testResponse.get()).thenThrow(interruptedException); - when(commandManager.submitNewCommand(any(), any())).thenReturn(testResponse); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(CustomCommand), eq(arguments), any())) + .thenReturn(testResponse); // exercise InterruptedException exception = assertThrows( InterruptedException.class, () -> { - CompletableFuture response = service.customCommand(new String[] {cmd, key}); + CompletableFuture response = service.customCommand(arguments); response.get(); }); // verify assertEquals(interruptedException, exception); } + + @SneakyThrows + @Test + public void ping_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn("PONG"); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals("PONG", payload); + } + + @SneakyThrows + @Test + public void ping_with_message_returns_success() { + // setup + String message = "RETURN OF THE PONG"; + String[] arguments = new String[] {message}; + CompletableFuture testResponse = new CompletableFuture(); + testResponse.complete(message); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(message); + String pong = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, pong); + } + + @SneakyThrows + @Test + public void info_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + String testPayload = "Key: Value"; + when(testResponse.get()).thenReturn(testPayload); + when(commandManager.submitNewCommand(eq(Info), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.info(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(testPayload, payload); + } + + @SneakyThrows + @Test + public void info_with_multiple_InfoOptions_returns_success() { + // setup + String[] arguments = + new String[] {InfoOptions.Section.ALL.toString(), InfoOptions.Section.DEFAULT.toString()}; + CompletableFuture testResponse = mock(CompletableFuture.class); + String testPayload = "Key: Value"; + when(testResponse.get()).thenReturn(testPayload); + when(commandManager.submitNewCommand(eq(Info), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + InfoOptions options = + InfoOptions.builder() + .section(InfoOptions.Section.ALL) + .section(InfoOptions.Section.DEFAULT) + .build(); + CompletableFuture response = service.info(options); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(testPayload, payload); + } + + @SneakyThrows + @Test + public void info_with_empty_InfoOptions_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + String testPayload = "Key: Value"; + when(testResponse.get()).thenReturn(testPayload); + when(commandManager.submitNewCommand(eq(Info), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.info(InfoOptions.builder().build()); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(testPayload, payload); + } + + @SneakyThrows + @Test + public void get_returns_success() { + // setup + String key = "testKey"; + String value = "testValue"; + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + when(commandManager.submitNewCommand(eq(GetString), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.get(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void set_returns_success() { + // setup + String key = "testKey"; + String value = "testValue"; + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(null); + when(commandManager.submitNewCommand(eq(SetString), eq(new String[] {key, value}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.set(key, value); + Object nullResponse = response.get(); + + // verify + assertEquals(testResponse, response); + assertNull(nullResponse); + } + + @SneakyThrows + @Test + public void set_with_SetOptions_OnlyIfExists_returns_success() { + // setup + String key = "testKey"; + String value = "testValue"; + SetOptions setOptions = + SetOptions.builder() + .conditionalSet(ONLY_IF_EXISTS) + .returnOldValue(false) + .expiry( + SetOptions.TimeToLive.builder() + .type(SetOptions.TimeToLiveType.KEEP_EXISTING) + .build()) + .build(); + String[] arguments = + new String[] {key, value, ONLY_IF_EXISTS.getRedisApi(), KEEP_EXISTING.getRedisApi()}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(null); + when(commandManager.submitNewCommand(eq(SetString), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.set(key, value, setOptions); + + // verify + assertEquals(testResponse, response); + assertNull(response.get()); + } + + @SneakyThrows + @Test + public void set_with_SetOptions_OnlyIfDoesNotExist_returns_success() { + // setup + String key = "testKey"; + String value = "testValue"; + SetOptions setOptions = + SetOptions.builder() + .conditionalSet(ONLY_IF_DOES_NOT_EXIST) + .returnOldValue(true) + .expiry(SetOptions.TimeToLive.builder().type(UNIX_SECONDS).count(60).build()) + .build(); + String[] arguments = + new String[] { + key, + value, + ONLY_IF_DOES_NOT_EXIST.getRedisApi(), + RETURN_OLD_VALUE, + UNIX_SECONDS.getRedisApi(), + "60" + }; + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + when(commandManager.submitNewCommand(eq(SetString), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.set(key, value, setOptions); + + // verify + assertNotNull(response); + assertEquals(value, response.get()); + } } diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index 32c459cafa..abba4a9587 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -1,30 +1,57 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_NODES; +import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.RANDOM; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; +import static redis_request.RedisRequestOuterClass.RequestType.Info; -import glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute; +import glide.api.models.ClusterValue; +import glide.api.models.commands.InfoOptions; +import glide.api.models.configuration.RequestRoutingConfiguration; import glide.managers.CommandManager; +import glide.managers.ConnectionManager; import glide.managers.RedisExceptionCheckedFunction; -import glide.managers.models.Command; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import redis_request.RedisRequestOuterClass; import response.ResponseOuterClass.Response; public class RedisClusterClientTest { + RedisClusterClient service; + + ConnectionManager connectionManager; + + CommandManager commandManager; + + @BeforeEach + public void setUp() { + connectionManager = mock(ConnectionManager.class); + commandManager = mock(CommandManager.class); + service = new RedisClusterClient(connectionManager, commandManager); + } + @Test @SneakyThrows - public void custom_command_returns_single_value() { + public void customCommand_returns_single_value() { var commandManager = new TestCommandManager(null); var client = new TestClient(commandManager, "TEST"); - var value = client.customCommand(new String[0]).get(); + var value = client.customCommand().get(); assertAll( () -> assertTrue(value.hasSingleData()), () -> assertEquals("TEST", value.getSingleValue())); @@ -32,13 +59,13 @@ public void custom_command_returns_single_value() { @Test @SneakyThrows - public void custom_command_returns_multi_value() { + public void customCommand_returns_multi_value() { var commandManager = new TestCommandManager(null); var data = Map.of("key1", "value1", "key2", "value2"); var client = new TestClient(commandManager, data); - var value = client.customCommand(new String[0]).get(); + var value = client.customCommand().get(); assertAll( () -> assertTrue(value.hasMultiData()), () -> assertEquals(data, value.getMultiValue())); } @@ -46,26 +73,26 @@ public void custom_command_returns_multi_value() { @Test @SneakyThrows // test checks that even a map returned as a single value when single node route is used - public void custom_command_with_single_node_route_returns_single_value() { + public void customCommand_with_single_node_route_returns_single_value() { var commandManager = new TestCommandManager(null); var data = Map.of("key1", "value1", "key2", "value2"); var client = new TestClient(commandManager, data); - var value = client.customCommand(new String[0], SimpleRoute.RANDOM).get(); + var value = client.customCommand(RANDOM).get(); assertAll( () -> assertTrue(value.hasSingleData()), () -> assertEquals(data, value.getSingleValue())); } @Test @SneakyThrows - public void custom_command_with_multi_node_route_returns_multi_value() { + public void customCommand_with_multi_node_route_returns_multi_value() { var commandManager = new TestCommandManager(null); var data = Map.of("key1", "value1", "key2", "value2"); var client = new TestClient(commandManager, data); - var value = client.customCommand(new String[0], SimpleRoute.ALL_NODES).get(); + var value = client.customCommand(ALL_NODES).get(); assertAll( () -> assertTrue(value.hasMultiData()), () -> assertEquals(data, value.getMultiValue())); } @@ -96,8 +123,139 @@ public TestCommandManager(Response responseToReturn) { @Override public CompletableFuture submitNewCommand( - Command command, RedisExceptionCheckedFunction responseHandler) { + RedisRequestOuterClass.RedisRequest.Builder command, + RedisExceptionCheckedFunction responseHandler) { return CompletableFuture.supplyAsync(() -> responseHandler.apply(response)); } } + + @SneakyThrows + @Test + public void customCommand_success() { + // setup + String key = "testKey"; + String value = "testValue"; + String cmd = "GETSTRING"; + String[] arguments = new String[] {cmd, key}; + CompletableFuture> testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(ClusterValue.of(value)); + when(commandManager.>submitNewCommand( + eq(CustomCommand), any(), any(), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.customCommand(arguments); + + // verify + assertEquals(testResponse, response); + ClusterValue clusterValue = response.get(); + assertTrue(clusterValue.hasSingleData()); + String payload = (String) clusterValue.getSingleValue(); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void customCommand_interruptedException() { + // setup + String key = "testKey"; + String cmd = "GETSTRING"; + String[] arguments = new String[] {cmd, key}; + CompletableFuture> testResponse = mock(CompletableFuture.class); + InterruptedException interruptedException = new InterruptedException(); + when(testResponse.get()).thenThrow(interruptedException); + when(commandManager.>submitNewCommand( + eq(CustomCommand), any(), any(), any())) + .thenReturn(testResponse); + + // exercise + InterruptedException exception = + assertThrows( + InterruptedException.class, + () -> { + CompletableFuture> response = service.customCommand(arguments); + response.get(); + }); + + // verify + assertEquals(interruptedException, exception); + } + + @SneakyThrows + @Test + public void info_returns_string() { + // setup + CompletableFuture> testResponse = mock(CompletableFuture.class); + Map testPayload = new HashMap(); + testPayload.put("addr1", "value1"); + testPayload.put("addr1", "value2"); + testPayload.put("addr1", "value3"); + when(testResponse.get()).thenReturn(ClusterValue.of(testPayload)); + when(commandManager.>submitNewCommand( + eq(Info), eq(new String[0]), any(), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.info(); + + // verify + assertEquals(testResponse, response); + ClusterValue clusterValue = response.get(); + assertTrue(clusterValue.hasMultiData()); + Map payload = clusterValue.getMultiValue(); + assertEquals(testPayload, payload); + } + + @SneakyThrows + @Test + public void info_with_route_returns_string() { + // setup + CompletableFuture> testResponse = mock(CompletableFuture.class); + Map testClusterValue = Map.of("addr1", "addr1 result", "addr2", "addr2 result"); + RequestRoutingConfiguration.Route route = RequestRoutingConfiguration.SimpleRoute.ALL_NODES; + when(testResponse.get()).thenReturn(ClusterValue.of(testClusterValue)); + when(commandManager.>submitNewCommand(eq(Info), any(), any(), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.info(route); + + // verify + assertEquals(testResponse, response); + ClusterValue clusterValue = response.get(); + assertTrue(clusterValue.hasMultiData()); + Map clusterMap = clusterValue.getMultiValue(); + assertEquals("addr1 result", clusterMap.get("addr1")); + assertEquals("addr2 result", clusterMap.get("addr2")); + } + + @SneakyThrows + @Test + public void info_with_route_with_infoOptions_returns_string() { + // setup + String[] infoArguments = new String[] {"ALL", "DEFAULT"}; + CompletableFuture> testResponse = mock(CompletableFuture.class); + Map testClusterValue = Map.of("addr1", "addr1 result", "addr2", "addr2 result"); + when(testResponse.get()).thenReturn(ClusterValue.of(testClusterValue)); + RequestRoutingConfiguration.Route route = RequestRoutingConfiguration.SimpleRoute.ALL_PRIMARIES; + when(commandManager.>submitNewCommand( + eq(Info), eq(infoArguments), any(), any())) + .thenReturn(testResponse); + + // exercise + InfoOptions options = + InfoOptions.builder() + .section(InfoOptions.Section.ALL) + .section(InfoOptions.Section.DEFAULT) + .build(); + CompletableFuture> response = service.info(options, route); + + // verify + assertEquals(testResponse, response); + ClusterValue clusterValue = response.get(); + assertTrue(clusterValue.hasMultiData()); + Map clusterMap = clusterValue.getMultiValue(); + assertEquals("addr1 result", clusterMap.get("addr1")); + assertEquals("addr2 result", clusterMap.get("addr2")); + } } diff --git a/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java new file mode 100644 index 0000000000..ef423d5c6c --- /dev/null +++ b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java @@ -0,0 +1,70 @@ +package glide.api.models; + +import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static redis_request.RedisRequestOuterClass.RequestType.GetString; +import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.Ping; +import static redis_request.RedisRequestOuterClass.RequestType.SetString; + +import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SetOptions; +import java.util.LinkedList; +import java.util.List; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.Test; +import redis_request.RedisRequestOuterClass.Command; +import redis_request.RedisRequestOuterClass.Command.ArgsArray; +import redis_request.RedisRequestOuterClass.RequestType; + +public class ClusterTransactionTests { + @Test + public void transaction_builds_protobuf_request() { + + ClusterTransaction transaction = new ClusterTransaction(); + + List> results = new LinkedList<>(); + + transaction.get("key"); + results.add(Pair.of(GetString, ArgsArray.newBuilder().addArgs("key").build())); + + transaction.set("key", "value"); + results.add(Pair.of(SetString, ArgsArray.newBuilder().addArgs("key").addArgs("value").build())); + + transaction.set("key", "value", SetOptions.builder().returnOldValue(true).build()); + results.add( + Pair.of( + SetString, + ArgsArray.newBuilder() + .addArgs("key") + .addArgs("value") + .addArgs(RETURN_OLD_VALUE) + .build())); + + transaction.ping(); + results.add(Pair.of(Ping, ArgsArray.newBuilder().build())); + + transaction.ping("KING PONG"); + results.add(Pair.of(Ping, ArgsArray.newBuilder().addArgs("KING PONG").build())); + + transaction.info(); + results.add(Pair.of(Info, ArgsArray.newBuilder().build())); + + transaction.info(InfoOptions.builder().section(InfoOptions.Section.EVERYTHING).build()); + results.add( + Pair.of( + Info, + ArgsArray.newBuilder().addArgs(InfoOptions.Section.EVERYTHING.toString()).build())); + + var protobufTransaction = transaction.getTransactionBuilder().build(); + + for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { + Command protobuf = protobufTransaction.getCommands(idx); + + assertEquals(results.get(idx).getLeft(), protobuf.getRequestType()); + assertEquals( + results.get(idx).getRight().getArgsCount(), protobuf.getArgsArray().getArgsCount()); + assertEquals(results.get(idx).getRight(), protobuf.getArgsArray()); + } + } +} diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java new file mode 100644 index 0000000000..7479a9072f --- /dev/null +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -0,0 +1,69 @@ +package glide.api.models; + +import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static redis_request.RedisRequestOuterClass.RequestType.GetString; +import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.Ping; +import static redis_request.RedisRequestOuterClass.RequestType.SetString; + +import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SetOptions; +import java.util.LinkedList; +import java.util.List; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.Test; +import redis_request.RedisRequestOuterClass.Command; +import redis_request.RedisRequestOuterClass.Command.ArgsArray; +import redis_request.RedisRequestOuterClass.RequestType; + +public class TransactionTests { + @Test + public void transaction_builds_protobuf_request() { + Transaction transaction = new Transaction(); + + List> results = new LinkedList<>(); + + transaction.get("key"); + results.add(Pair.of(GetString, ArgsArray.newBuilder().addArgs("key").build())); + + transaction.set("key", "value"); + results.add(Pair.of(SetString, ArgsArray.newBuilder().addArgs("key").addArgs("value").build())); + + transaction.set("key", "value", SetOptions.builder().returnOldValue(true).build()); + results.add( + Pair.of( + SetString, + ArgsArray.newBuilder() + .addArgs("key") + .addArgs("value") + .addArgs(RETURN_OLD_VALUE) + .build())); + + transaction.ping(); + results.add(Pair.of(Ping, ArgsArray.newBuilder().build())); + + transaction.ping("KING PONG"); + results.add(Pair.of(Ping, ArgsArray.newBuilder().addArgs("KING PONG").build())); + + transaction.info(); + results.add(Pair.of(Info, ArgsArray.newBuilder().build())); + + transaction.info(InfoOptions.builder().section(InfoOptions.Section.EVERYTHING).build()); + results.add( + Pair.of( + Info, + ArgsArray.newBuilder().addArgs(InfoOptions.Section.EVERYTHING.toString()).build())); + + var protobufTransaction = transaction.getTransactionBuilder().build(); + + for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { + Command protobuf = protobufTransaction.getCommands(idx); + + assertEquals(results.get(idx).getLeft(), protobuf.getRequestType()); + assertEquals( + results.get(idx).getRight().getArgsCount(), protobuf.getArgsArray().getArgsCount()); + assertEquals(results.get(idx).getRight(), protobuf.getArgsArray()); + } + } +} diff --git a/java/client/src/test/java/glide/managers/CommandManagerTest.java b/java/client/src/test/java/glide/managers/CommandManagerTest.java index 8e5010a905..e1ac05c2c9 100644 --- a/java/client/src/test/java/glide/managers/CommandManagerTest.java +++ b/java/client/src/test/java/glide/managers/CommandManagerTest.java @@ -12,24 +12,28 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; +import glide.api.models.ClusterTransaction; +import glide.api.models.Transaction; import glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute; import glide.api.models.configuration.RequestRoutingConfiguration.SlotIdRoute; import glide.api.models.configuration.RequestRoutingConfiguration.SlotKeyRoute; import glide.api.models.configuration.RequestRoutingConfiguration.SlotType; +import glide.api.models.exceptions.ClosingException; import glide.connectors.handlers.ChannelHandler; -import glide.managers.models.Command; -import java.util.Map; +import java.util.LinkedList; +import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.mockito.ArgumentCaptor; +import redis_request.RedisRequestOuterClass; import redis_request.RedisRequestOuterClass.RedisRequest; -import redis_request.RedisRequestOuterClass.SimpleRoutes; -import redis_request.RedisRequestOuterClass.SlotTypes; import response.ResponseOuterClass.Response; public class CommandManagerTest { @@ -38,12 +42,8 @@ public class CommandManagerTest { CommandManager service; - Command command; - @BeforeEach void init() { - command = Command.builder().requestType(Command.RequestType.CUSTOM_COMMAND).build(); - channelHandler = mock(ChannelHandler.class); service = new CommandManager(channelHandler); } @@ -64,7 +64,10 @@ public void submitNewCommand_return_Object_result() { // exercise CompletableFuture result = service.submitNewCommand( - command, new BaseCommandResponseResolver((ptr) -> ptr == pointer ? respObject : null)); + CustomCommand, + new String[0], + Optional.empty(), + new BaseCommandResponseResolver((ptr) -> ptr == pointer ? respObject : null)); Object respPointer = result.get(); // verify @@ -83,7 +86,10 @@ public void submitNewCommand_return_Null_result() { // exercise CompletableFuture result = service.submitNewCommand( - command, new BaseCommandResponseResolver((p) -> new RuntimeException(""))); + CustomCommand, + new String[0], + Optional.empty(), + new BaseCommandResponseResolver((p) -> new RuntimeException(""))); Object respPointer = result.get(); // verify @@ -107,7 +113,10 @@ public void submitNewCommand_return_String_result() { // exercise CompletableFuture result = service.submitNewCommand( - command, new BaseCommandResponseResolver((p) -> p == pointer ? testString : null)); + CustomCommand, + new String[0], + Optional.empty(), + new BaseCommandResponseResolver((p) -> p == pointer ? testString : null)); Object respPointer = result.get(); // verify @@ -115,24 +124,45 @@ public void submitNewCommand_return_String_result() { assertEquals(testString, respPointer); } + @SneakyThrows + @Test + public void submitNewCommand_throws_ClosingException() { + // setup + String errorMsg = "Closing"; + Response closingErrorResponse = Response.newBuilder().setClosingError(errorMsg).build(); + BaseCommandResponseResolver handler = + new BaseCommandResponseResolver((ptr) -> closingErrorResponse); + + CompletableFuture futureResponse = new CompletableFuture<>(); + when(channelHandler.write(any(), anyBoolean())).thenReturn(futureResponse); + ClosingException closingException = new ClosingException(errorMsg); + futureResponse.completeExceptionally(closingException); + + // exercise + ExecutionException e = + assertThrows( + ExecutionException.class, + () -> { + CompletableFuture result = + service.submitNewCommand(CustomCommand, new String[0], Optional.empty(), handler); + result.get(); + }); + + // verify + assertEquals(closingException, e.getCause()); + assertEquals(errorMsg, e.getCause().getMessage()); + } + @ParameterizedTest @EnumSource(value = SimpleRoute.class) public void prepare_request_with_simple_routes(SimpleRoute routeType) { CompletableFuture future = new CompletableFuture<>(); when(channelHandler.write(any(), anyBoolean())).thenReturn(future); - var command = - Command.builder().requestType(Command.RequestType.CUSTOM_COMMAND).route(routeType).build(); ArgumentCaptor captor = ArgumentCaptor.forClass(RedisRequest.Builder.class); - var protobufToClientRouteMapping = - Map.of( - SimpleRoutes.AllNodes, SimpleRoute.ALL_NODES, - SimpleRoutes.AllPrimaries, SimpleRoute.ALL_PRIMARIES, - SimpleRoutes.Random, SimpleRoute.RANDOM); - - service.submitNewCommand(command, r -> null); + service.submitNewCommand(CustomCommand, new String[0], Optional.of(routeType), r -> null); verify(channelHandler).write(captor.capture(), anyBoolean()); var requestBuilder = captor.getValue(); @@ -141,8 +171,7 @@ public void prepare_request_with_simple_routes(SimpleRoute routeType) { () -> assertTrue(requestBuilder.getRoute().hasSimpleRoutes()), () -> assertEquals( - routeType, - protobufToClientRouteMapping.get(requestBuilder.getRoute().getSimpleRoutes())), + routeType.getProtobufMapping(), requestBuilder.getRoute().getSimpleRoutes()), () -> assertFalse(requestBuilder.getRoute().hasSlotIdRoute()), () -> assertFalse(requestBuilder.getRoute().hasSlotKeyRoute())); } @@ -152,32 +181,21 @@ public void prepare_request_with_simple_routes(SimpleRoute routeType) { public void prepare_request_with_slot_id_routes(SlotType slotType) { CompletableFuture future = new CompletableFuture<>(); when(channelHandler.write(any(), anyBoolean())).thenReturn(future); - var command = - Command.builder() - .requestType(Command.RequestType.CUSTOM_COMMAND) - .route(new SlotIdRoute(42, slotType)) - .build(); ArgumentCaptor captor = ArgumentCaptor.forClass(RedisRequest.Builder.class); - service.submitNewCommand(command, r -> null); + service.submitNewCommand( + CustomCommand, new String[0], Optional.of(new SlotIdRoute(42, slotType)), r -> null); verify(channelHandler).write(captor.capture(), anyBoolean()); var requestBuilder = captor.getValue(); - var protobufToClientRouteMapping = - Map.of( - SlotTypes.Primary, SlotType.PRIMARY, - SlotTypes.Replica, SlotType.REPLICA); - assertAll( () -> assertTrue(requestBuilder.hasRoute()), () -> assertTrue(requestBuilder.getRoute().hasSlotIdRoute()), () -> assertEquals( - slotType, - protobufToClientRouteMapping.get( - requestBuilder.getRoute().getSlotIdRoute().getSlotType())), + slotType.getSlotTypes(), requestBuilder.getRoute().getSlotIdRoute().getSlotType()), () -> assertEquals(42, requestBuilder.getRoute().getSlotIdRoute().getSlotId()), () -> assertFalse(requestBuilder.getRoute().hasSimpleRoutes()), () -> assertFalse(requestBuilder.getRoute().hasSlotKeyRoute())); @@ -188,32 +206,21 @@ public void prepare_request_with_slot_id_routes(SlotType slotType) { public void prepare_request_with_slot_key_routes(SlotType slotType) { CompletableFuture future = new CompletableFuture<>(); when(channelHandler.write(any(), anyBoolean())).thenReturn(future); - var command = - Command.builder() - .requestType(Command.RequestType.CUSTOM_COMMAND) - .route(new SlotKeyRoute("TEST", slotType)) - .build(); ArgumentCaptor captor = ArgumentCaptor.forClass(RedisRequest.Builder.class); - service.submitNewCommand(command, r -> null); + service.submitNewCommand( + CustomCommand, new String[0], Optional.of(new SlotKeyRoute("TEST", slotType)), r -> null); verify(channelHandler).write(captor.capture(), anyBoolean()); var requestBuilder = captor.getValue(); - var protobufToClientRouteMapping = - Map.of( - SlotTypes.Primary, SlotType.PRIMARY, - SlotTypes.Replica, SlotType.REPLICA); - assertAll( () -> assertTrue(requestBuilder.hasRoute()), () -> assertTrue(requestBuilder.getRoute().hasSlotKeyRoute()), () -> assertEquals( - slotType, - protobufToClientRouteMapping.get( - requestBuilder.getRoute().getSlotKeyRoute().getSlotType())), + slotType.getSlotTypes(), requestBuilder.getRoute().getSlotKeyRoute().getSlotType()), () -> assertEquals("TEST", requestBuilder.getRoute().getSlotKeyRoute().getSlotKey()), () -> assertFalse(requestBuilder.getRoute().hasSimpleRoutes()), () -> assertFalse(requestBuilder.getRoute().hasSlotIdRoute())); @@ -223,15 +230,121 @@ public void prepare_request_with_slot_key_routes(SlotType slotType) { public void prepare_request_with_unknown_route_type() { CompletableFuture future = new CompletableFuture<>(); when(channelHandler.write(any(), anyBoolean())).thenReturn(future); - var command = - Command.builder() - .requestType(Command.RequestType.CUSTOM_COMMAND) - .route(() -> false) - .build(); var exception = assertThrows( - IllegalArgumentException.class, () -> service.submitNewCommand(command, r -> null)); + IllegalArgumentException.class, + () -> + service.submitNewCommand( + CustomCommand, new String[0], Optional.of(() -> false), r -> null)); assertEquals("Unknown type of route", exception.getMessage()); } + + @SneakyThrows + @Test + public void submitNewCommand_with_Transaction_sends_protobuf_request() { + // setup + String[] arg1 = new String[] {"GETSTRING", "one"}; + String[] arg2 = new String[] {"GETSTRING", "two"}; + String[] arg3 = new String[] {"GETSTRING", "three"}; + Transaction trans = new Transaction(); + trans.customCommand(arg1).customCommand(arg2).customCommand(arg3); + + CompletableFuture future = new CompletableFuture<>(); + when(channelHandler.write(any(), anyBoolean())).thenReturn(future); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(RedisRequest.Builder.class); + + // exercise + service.submitNewCommand(trans, r -> null); + + // verify + verify(channelHandler).write(captor.capture(), anyBoolean()); + var requestBuilder = captor.getValue(); + + // verify + assertTrue(requestBuilder.hasTransaction()); + assertEquals(3, requestBuilder.getTransaction().getCommandsCount()); + + LinkedList resultPayloads = new LinkedList<>(); + resultPayloads.add("one"); + resultPayloads.add("two"); + resultPayloads.add("three"); + for (RedisRequestOuterClass.Command command : + requestBuilder.getTransaction().getCommandsList()) { + assertEquals(CustomCommand, command.getRequestType()); + assertEquals("GETSTRING", command.getArgsArray().getArgs(0)); + assertEquals(resultPayloads.pop(), command.getArgsArray().getArgs(1)); + } + } + + @SneakyThrows + @Test + public void submitNewCommand_with_Transaction_throws_interruptedException() { + // setup + String[] arg1 = new String[] {"GETSTRING", "one"}; + String[] arg2 = new String[] {"GETSTRING", "two"}; + String[] arg3 = new String[] {"GETSTRING", "two"}; + Transaction trans = new Transaction(); + trans.customCommand(arg1).customCommand(arg2).customCommand(arg3); + + CompletableFuture futureResponse = mock(CompletableFuture.class); + CompletableFuture futureObject = mock(CompletableFuture.class); + when(channelHandler.write(any(), anyBoolean())).thenReturn(futureResponse); + InterruptedException interruptedException = new InterruptedException(); + when(futureResponse.exceptionally(any())).thenReturn(futureResponse); + when(futureResponse.thenApplyAsync(any())).thenReturn(futureObject); + when(futureObject.get()).thenThrow(new ExecutionException(interruptedException)); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(RedisRequest.Builder.class); + + // exercise + ExecutionException exception = + assertThrows( + ExecutionException.class, + () -> { + CompletableFuture response = service.submitNewCommand(trans, r -> null); + response.get(); + }); + verify(channelHandler).write(captor.capture(), anyBoolean()); + var requestBuilder = captor.getValue(); + + // verify + assertTrue(requestBuilder.hasTransaction()); + assertEquals(3, requestBuilder.getTransaction().getCommandsCount()); + assertEquals(interruptedException, exception.getCause()); + } + + @ParameterizedTest + @EnumSource(value = SimpleRoute.class) + public void submitNewCommand_with_ClusterTransaction_with_route_sends_protobuf_request( + SimpleRoute routeType) { + + String[] arg1 = new String[] {"GETSTRING", "one"}; + String[] arg2 = new String[] {"GETSTRING", "two"}; + String[] arg3 = new String[] {"GETSTRING", "two"}; + ClusterTransaction trans = new ClusterTransaction(); + trans.customCommand(arg1).customCommand(arg2).customCommand(arg3); + + CompletableFuture future = new CompletableFuture<>(); + when(channelHandler.write(any(), anyBoolean())).thenReturn(future); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(RedisRequest.Builder.class); + + service.submitNewCommand(trans, Optional.of(routeType), r -> null); + verify(channelHandler).write(captor.capture(), anyBoolean()); + var requestBuilder = captor.getValue(); + + assertAll( + () -> assertTrue(requestBuilder.hasRoute()), + () -> assertTrue(requestBuilder.getRoute().hasSimpleRoutes()), + () -> + assertEquals( + routeType.getProtobufMapping(), requestBuilder.getRoute().getSimpleRoutes()), + () -> assertFalse(requestBuilder.getRoute().hasSlotIdRoute()), + () -> assertFalse(requestBuilder.getRoute().hasSlotKeyRoute())); + } }