From 8823a1a5388f00982763eb9a4865fba396505749 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Mon, 12 Feb 2024 12:24:53 -0800 Subject: [PATCH] Java: Add transaction commands (#895) * Java: Add transaction commands Signed-off-by: Andrew Carbonetto * Clean up for review comments Signed-off-by: Andrew Carbonetto * Fix CommandManagerTest.java Signed-off-by: Andrew Carbonetto * All transactions require an argument (empty is fine) Signed-off-by: Andrew Carbonetto * Add IT tests for transactions Signed-off-by: Andrew Carbonetto * Renaming field Signed-off-by: Andrew Carbonetto * Add IT tests for Transactions Signed-off-by: Andrew Carbonetto * Update exec() command with route Signed-off-by: Andrew Carbonetto * Update exec() command with route Signed-off-by: Andrew Carbonetto * Remove failing tests Signed-off-by: Andrew Carbonetto * Spotless Signed-off-by: Andrew Carbonetto * Spotless Signed-off-by: Andrew Carbonetto * Spotless Signed-off-by: Andrew Carbonetto * Update cluster comments Signed-off-by: Andrew Carbonetto --------- Signed-off-by: Andrew Carbonetto Signed-off-by: Andrew Carbonetto --- .../src/main/java/glide/api/BaseClient.java | 4 + .../src/main/java/glide/api/RedisClient.java | 6 + .../java/glide/api/RedisClusterClient.java | 22 +- .../api/commands/GenericClusterCommands.java | 41 ++++ .../glide/api/commands/GenericCommands.java | 18 ++ .../glide/api/models/BaseTransaction.java | 188 ++++++++++++++++++ .../glide/api/models/ClusterTransaction.java | 30 +++ .../java/glide/api/models/Transaction.java | 30 +++ .../java/glide/managers/CommandManager.java | 66 ++++++ .../api/models/ClusterTransactionTests.java | 71 +++++++ .../glide/api/models/TransactionTests.java | 70 +++++++ .../glide/managers/CommandManagerTest.java | 81 ++++++++ .../src/test/java/glide/TestUtilities.java | 24 +++ .../cluster/ClusterTransactionTests.java | 82 ++++++++ .../glide/standalone/TransactionTests.java | 98 +++++++++ 15 files changed, 830 insertions(+), 1 deletion(-) create mode 100644 java/client/src/main/java/glide/api/models/BaseTransaction.java create mode 100644 java/client/src/main/java/glide/api/models/ClusterTransaction.java create mode 100644 java/client/src/main/java/glide/api/models/Transaction.java create mode 100644 java/client/src/test/java/glide/api/models/ClusterTransactionTests.java create mode 100644 java/client/src/test/java/glide/api/models/TransactionTests.java create mode 100644 java/integTest/src/test/java/glide/TestUtilities.java create mode 100644 java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java create mode 100644 java/integTest/src/test/java/glide/standalone/TransactionTests.java diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index b7096c82d1..3d513c512c 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -149,6 +149,10 @@ protected String handleStringOrNullResponse(Response response) throws RedisExcep return handleRedisResponse(String.class, true, response); } + protected Object[] handleArrayResponse(Response response) { + return handleRedisResponse(Object[].class, true, response); + } + @Override public CompletableFuture ping() { return commandManager.submitNewCommand(Ping, new String[0], 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 faf8b98d6b..744d582d8a 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -6,6 +6,7 @@ import glide.api.commands.GenericCommands; import glide.api.commands.ServerManagementCommands; +import glide.api.models.Transaction; import glide.api.models.commands.InfoOptions; import glide.api.models.configuration.RedisClientConfiguration; import glide.managers.CommandManager; @@ -38,6 +39,11 @@ public CompletableFuture customCommand(@NonNull String[] args) { return commandManager.submitNewCommand(CustomCommand, args, this::handleObjectOrNullResponse); } + @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); diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index fd6b66b138..ddbb5d6e25 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -8,13 +8,16 @@ import glide.api.commands.ConnectionManagementClusterCommands; import glide.api.commands.GenericClusterCommands; import glide.api.commands.ServerManagementClusterCommands; +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.managers.CommandManager; import glide.managers.ConnectionManager; +import java.util.Arrays; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import lombok.NonNull; @@ -63,6 +66,24 @@ public CompletableFuture> customCommand(String[] args, Rout (Map) handleObjectOrNullResponse(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) + .thenApply( + objects -> + Arrays.stream(objects) + .map(ClusterValue::of) + .>toArray(ClusterValue[]::new)); + } + @Override public CompletableFuture ping(@NonNull Route route) { return commandManager.submitNewCommand(Ping, new String[0], route, this::handleStringResponse); @@ -80,7 +101,6 @@ public CompletableFuture> info() { Info, new String[0], response -> ClusterValue.of(handleObjectResponse(response))); } - @Override public CompletableFuture> info(@NonNull Route route) { return commandManager.submitNewCommand( Info, new String[0], route, response -> ClusterValue.of(handleObjectResponse(response))); diff --git a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java index 7ee33b81c5..2ab104de70 100644 --- a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.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; @@ -51,4 +53,43 @@ public interface GenericClusterCommands { * @return Response from Redis containing an Object. */ CompletableFuture> customCommand(String[] args, Route route); + + /** + * Execute a transaction by processing the queued commands. + * + *

The transaction will be routed to the slot owner of the first key found in the transaction. + * If no key is found, the command will be sent to a random node. + * + * @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 transaction. The client will route the transaction + * to the nodes defined by route. + * @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, Route route); } diff --git a/java/client/src/main/java/glide/api/commands/GenericCommands.java b/java/client/src/main/java/glide/api/commands/GenericCommands.java index 4f214f36cc..6df32ba506 100644 --- a/java/client/src/main/java/glide/api/commands/GenericCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericCommands.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; /** Generic Commands interface to handle generic command and transaction requests. */ @@ -24,4 +25,21 @@ public interface GenericCommands { * @return Response from Redis containing an Object. */ 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/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java new file mode 100644 index 0000000000..92248e5047 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -0,0 +1,188 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +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 protobufTransaction = 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("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); + protobufTransaction.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() { + protobufTransaction.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); + + protobufTransaction.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() { + protobufTransaction.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()); + + protobufTransaction.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); + + protobufTransaction.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); + + protobufTransaction.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())); + + protobufTransaction.addCommands(buildCommand(SetString, commandArgs)); + return getThis(); + } + + /** Build protobuf {@link Command} object for given command and arguments. */ + protected Command buildCommand(RequestType requestType) { + return buildCommand(requestType, buildArgs()); + } + + /** 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..e2c4820057 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/ClusterTransaction.java @@ -0,0 +1,30 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +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/Transaction.java b/java/client/src/main/java/glide/api/models/Transaction.java new file mode 100644 index 0000000000..219ee7e934 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/Transaction.java @@ -0,0 +1,30 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +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/managers/CommandManager.java b/java/client/src/main/java/glide/managers/CommandManager.java index 6945da63dd..770830ce2f 100644 --- a/java/client/src/main/java/glide/managers/CommandManager.java +++ b/java/client/src/main/java/glide/managers/CommandManager.java @@ -1,6 +1,8 @@ /** 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,6 +10,7 @@ import glide.api.models.exceptions.ClosingException; import glide.connectors.handlers.CallbackDispatcher; import glide.connectors.handlers.ChannelHandler; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import redis_request.RedisRequestOuterClass; @@ -66,6 +69,37 @@ public CompletableFuture submitNewCommand( return submitCommandToChannel(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 submitCommandToChannel(command, responseHandler); + } + + /** + * Build a Transaction and send. + * + * @param transaction Redis Transaction request with multiple commands + * @param route Transaction 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 submitCommandToChannel(command, responseHandler); + } + /** * Take a redis request and send to channel. * @@ -110,6 +144,38 @@ protected RedisRequest.Builder prepareRedisRequest( return prepareRedisRequestRoute(builder, route); } + /** + * 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) { + + RedisRequest.Builder builder = + RedisRequest.newBuilder().setTransaction(transaction.getProtobufTransaction().build()); + + return builder; + } + + /** + * 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) { + + RedisRequest.Builder builder = + RedisRequest.newBuilder().setTransaction(transaction.getProtobufTransaction().build()); + + return route.isPresent() ? prepareRedisRequestRoute(builder, route.get()) : builder; + } + /** * Build a protobuf command request object. * 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..cbdcd4632c --- /dev/null +++ b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java @@ -0,0 +1,71 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +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.getProtobufTransaction().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..5cd4c52df4 --- /dev/null +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -0,0 +1,70 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +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.getProtobufTransaction().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 9f239a7fa5..0f7f539ebb 100644 --- a/java/client/src/test/java/glide/managers/CommandManagerTest.java +++ b/java/client/src/test/java/glide/managers/CommandManagerTest.java @@ -14,12 +14,16 @@ 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.connectors.handlers.ChannelHandler; +import java.util.LinkedList; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeEach; @@ -222,4 +226,81 @@ public void prepare_request_with_unknown_route_type() { () -> service.submitNewCommand(CustomCommand, new String[0], () -> 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 (redis_request.RedisRequestOuterClass.Command command : + requestBuilder.getTransaction().getCommandsList()) { + assertEquals(CustomCommand, command.getRequestType()); + assertEquals("GETSTRING", command.getArgsArray().getArgs(0)); + assertEquals(resultPayloads.pop(), command.getArgsArray().getArgs(1)); + } + } + + @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().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(); + + var protobufToClientRouteMapping = + Map.of( + SimpleRoutes.AllNodes, SimpleRoute.ALL_NODES, + SimpleRoutes.AllPrimaries, SimpleRoute.ALL_PRIMARIES, + SimpleRoutes.Random, SimpleRoute.RANDOM); + + assertAll( + () -> assertTrue(requestBuilder.hasRoute()), + () -> assertTrue(requestBuilder.getRoute().hasSimpleRoutes()), + () -> + assertEquals( + routeType, + protobufToClientRouteMapping.get(requestBuilder.getRoute().getSimpleRoutes())), + () -> assertFalse(requestBuilder.getRoute().hasSlotIdRoute()), + () -> assertFalse(requestBuilder.getRoute().hasSlotKeyRoute())); + } } diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java new file mode 100644 index 0000000000..2b01067cc2 --- /dev/null +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -0,0 +1,24 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide; + +import glide.api.models.BaseTransaction; +import glide.api.models.commands.SetOptions; +import java.util.UUID; + +public class TestUtilities { + + public static BaseTransaction transactionTest(BaseTransaction baseTransaction) { + String key1 = "{key}" + UUID.randomUUID(); + String key2 = "{key}" + UUID.randomUUID(); + + baseTransaction.set(key1, "bar"); + baseTransaction.set(key2, "baz", SetOptions.builder().returnOldValue(true).build()); + baseTransaction.customCommand("MGET", key1, key2); + + return baseTransaction; + } + + public static Object[] transactionTestResult() { + return new Object[] {"OK", null, new String[] {"bar", "baz"}}; + } +} diff --git a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java new file mode 100644 index 0000000000..2e0a903d32 --- /dev/null +++ b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java @@ -0,0 +1,82 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.cluster; + +import static glide.TestUtilities.transactionTest; +import static glide.TestUtilities.transactionTestResult; +import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.RANDOM; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import glide.TestConfiguration; +import glide.api.RedisClusterClient; +import glide.api.models.ClusterTransaction; +import glide.api.models.ClusterValue; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.RedisClusterClientConfiguration; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ClusterTransactionTests { + + private static RedisClusterClient clusterClient = null; + + @BeforeAll + @SneakyThrows + public static void init() { + clusterClient = + RedisClusterClient.CreateClient( + RedisClusterClientConfiguration.builder() + .address(NodeAddress.builder().port(TestConfiguration.CLUSTER_PORTS[0]).build()) + .requestTimeout(5000) + .build()) + .get(10, TimeUnit.SECONDS); + } + + @AfterAll + @SneakyThrows + public static void teardown() { + clusterClient.close(); + } + + @Test + @SneakyThrows + public void custom_command_info() { + ClusterTransaction transaction = new ClusterTransaction().customCommand("info"); + Object[] result = clusterClient.exec(transaction).get(10, TimeUnit.SECONDS); + assertTrue(((String) result[0]).contains("# Stats")); + } + + @Test + @SneakyThrows + public void info_simple_route_test() { + ClusterTransaction transaction = new ClusterTransaction().info().info(); + ClusterValue[] result = + clusterClient.exec(transaction, RANDOM).get(10, TimeUnit.SECONDS); + + // check single-value result + assertTrue(result[0].hasSingleData()); + assertTrue(((String) result[0].getSingleValue()).contains("# Stats")); + + assertTrue(result[1].hasSingleData()); + assertTrue(((String) result[1].getSingleValue()).contains("# Stats")); + } + + @SneakyThrows + @Test + public void test_cluster_transactions() { + ClusterTransaction transaction = (ClusterTransaction) transactionTest(new ClusterTransaction()); + Object[] expectedResult = transactionTestResult(); + + ClusterValue[] clusterValues = + clusterClient.exec(transaction, RANDOM).get(10, TimeUnit.SECONDS); + Object[] results = + Arrays.stream(clusterValues) + .map(v -> v.hasSingleData() ? v.getSingleValue() : v.getMultiValue()) + .toArray(Object[]::new); + assertArrayEquals(expectedResult, results); + } +} diff --git a/java/integTest/src/test/java/glide/standalone/TransactionTests.java b/java/integTest/src/test/java/glide/standalone/TransactionTests.java new file mode 100644 index 0000000000..8939969388 --- /dev/null +++ b/java/integTest/src/test/java/glide/standalone/TransactionTests.java @@ -0,0 +1,98 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.standalone; + +import static glide.TestUtilities.transactionTest; +import static glide.TestUtilities.transactionTestResult; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import glide.TestConfiguration; +import glide.api.RedisClient; +import glide.api.models.Transaction; +import glide.api.models.commands.InfoOptions; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.RedisClientConfiguration; +import java.util.concurrent.TimeUnit; +import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class TransactionTests { + + private static RedisClient client = null; + + @BeforeAll + @SneakyThrows + public static void init() { + client = + RedisClient.CreateClient( + RedisClientConfiguration.builder() + .address( + NodeAddress.builder().port(TestConfiguration.STANDALONE_PORTS[0]).build()) + .build()) + .get(10, TimeUnit.SECONDS); + } + + @AfterAll + @SneakyThrows + public static void teardown() { + client.close(); + } + + @Test + @SneakyThrows + public void custom_command_info() { + Transaction transaction = new Transaction().customCommand("info"); + Object[] result = client.exec(transaction).get(10, TimeUnit.SECONDS); + assertTrue(((String) result[0]).contains("# Stats")); + } + + @Test + @SneakyThrows + public void info_test() { + Transaction transaction = + new Transaction() + .info() + .info(InfoOptions.builder().section(InfoOptions.Section.CLUSTER).build()); + Object[] result = client.exec(transaction).get(10, TimeUnit.SECONDS); + + // sanity check + assertTrue(((String) result[0]).contains("# Stats")); + assertFalse(((String) result[1]).contains("# Stats")); + } + + @Test + @SneakyThrows + public void ping_tests() { + Transaction transaction = new Transaction(); + int numberOfPings = 100; + for (int idx = 0; idx < numberOfPings; idx++) { + if ((idx % 2) == 0) { + transaction.ping(); + } else { + transaction.ping(Integer.toString(idx)); + } + } + Object[] result = client.exec(transaction).get(10, TimeUnit.SECONDS); + for (int idx = 0; idx < numberOfPings; idx++) { + if ((idx % 2) == 0) { + assertEquals("PONG", result[idx]); + } else { + assertEquals(Integer.toString(idx), result[idx]); + } + } + } + + @SneakyThrows + @Test + public void test_standalone_transactions() { + Transaction transaction = (Transaction) transactionTest(new Transaction()); + Object[] expectedResult = transactionTestResult(); + + Object[] result = client.exec(transaction).get(10, TimeUnit.SECONDS); + assertArrayEquals(expectedResult, result); + } +}