Skip to content

Commit

Permalink
Java: Add transaction commands (valkey-io#895)
Browse files Browse the repository at this point in the history
* Java: Add transaction commands

Signed-off-by: Andrew Carbonetto <[email protected]>

* Clean up for review comments

Signed-off-by: Andrew Carbonetto <[email protected]>

* Fix CommandManagerTest.java

Signed-off-by: Andrew Carbonetto <[email protected]>

* All transactions require an argument (empty is fine)

Signed-off-by: Andrew Carbonetto <[email protected]>

* Add IT tests for transactions

Signed-off-by: Andrew Carbonetto <[email protected]>

* Renaming field

Signed-off-by: Andrew Carbonetto <[email protected]>

* Add IT tests for Transactions

Signed-off-by: Andrew Carbonetto <[email protected]>

* Update exec() command with route

Signed-off-by: Andrew Carbonetto <[email protected]>

* Update exec() command with route

Signed-off-by: Andrew Carbonetto <[email protected]>

* Remove failing tests

Signed-off-by: Andrew Carbonetto <[email protected]>

* Spotless

Signed-off-by: Andrew Carbonetto <[email protected]>

* Spotless

Signed-off-by: Andrew Carbonetto <[email protected]>

* Spotless

Signed-off-by: Andrew Carbonetto <[email protected]>

* Update cluster comments

Signed-off-by: Andrew Carbonetto <[email protected]>

---------

Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
  • Loading branch information
acarbonetto authored Feb 12, 2024
1 parent e6a0de5 commit 8823a1a
Show file tree
Hide file tree
Showing 15 changed files with 830 additions and 1 deletion.
4 changes: 4 additions & 0 deletions java/client/src/main/java/glide/api/BaseClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> ping() {
return commandManager.submitNewCommand(Ping, new String[0], this::handleStringResponse);
Expand Down
6 changes: 6 additions & 0 deletions java/client/src/main/java/glide/api/RedisClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -38,6 +39,11 @@ public CompletableFuture<Object> customCommand(@NonNull String[] args) {
return commandManager.submitNewCommand(CustomCommand, args, this::handleObjectOrNullResponse);
}

@Override
public CompletableFuture<Object[]> exec(Transaction transaction) {
return commandManager.submitNewCommand(transaction, this::handleArrayResponse);
}

@Override
public CompletableFuture<String> info() {
return commandManager.submitNewCommand(Info, new String[0], this::handleStringResponse);
Expand Down
22 changes: 21 additions & 1 deletion java/client/src/main/java/glide/api/RedisClusterClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -63,6 +66,24 @@ public CompletableFuture<ClusterValue<Object>> customCommand(String[] args, Rout
(Map<String, Object>) handleObjectOrNullResponse(response)));
}

@Override
public CompletableFuture<Object[]> exec(ClusterTransaction transaction) {
return commandManager.submitNewCommand(
transaction, Optional.empty(), this::handleArrayResponse);
}

@Override
public CompletableFuture<ClusterValue<Object>[]> exec(
ClusterTransaction transaction, Route route) {
return commandManager
.submitNewCommand(transaction, Optional.ofNullable(route), this::handleArrayResponse)
.thenApply(
objects ->
Arrays.stream(objects)
.map(ClusterValue::of)
.<ClusterValue<Object>>toArray(ClusterValue[]::new));
}

@Override
public CompletableFuture<String> ping(@NonNull Route route) {
return commandManager.submitNewCommand(Ping, new String[0], route, this::handleStringResponse);
Expand All @@ -80,7 +101,6 @@ public CompletableFuture<ClusterValue<String>> info() {
Info, new String[0], response -> ClusterValue.of(handleObjectResponse(response)));
}

@Override
public CompletableFuture<ClusterValue<String>> info(@NonNull Route route) {
return commandManager.submitNewCommand(
Info, new String[0], route, response -> ClusterValue.of(handleObjectResponse(response)));
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -51,4 +53,43 @@ public interface GenericClusterCommands {
* @return Response from Redis containing an <code>Object</code>.
*/
CompletableFuture<ClusterValue<Object>> customCommand(String[] args, Route route);

/**
* Execute a transaction by processing the queued commands.
*
* <p>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 <a href="https://redis.io/topics/Transactions/">redis.io</a> 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
* <ul>
* <li>If a command returns a value, it will be included in the list.
* <li>If a command doesn't return a value, the list entry will be empty.
* <li>If the transaction failed due to a <code>WATCH</code> command, <code>exec</code> will
* return <code>null</code>.
* </ul>
*/
CompletableFuture<Object[]> exec(ClusterTransaction transaction);

/**
* Execute a transaction by processing the queued commands.
*
* @see <a href="https://redis.io/topics/Transactions/">redis.io</a> 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 <code>route</code>.
* @return A list of results corresponding to the execution of each command in the transaction.
* @remarks
* <ul>
* <li>If a command returns a value, it will be included in the list.
* <li>If a command doesn't return a value, the list entry will be empty.
* <li>If the transaction failed due to a <code>WATCH</code> command, <code>exec</code> will
* return <code>null</code>.
* </ul>
*/
CompletableFuture<ClusterValue<Object>[]> exec(ClusterTransaction transaction, Route route);
}
18 changes: 18 additions & 0 deletions java/client/src/main/java/glide/api/commands/GenericCommands.java
Original file line number Diff line number Diff line change
@@ -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. */
Expand All @@ -24,4 +25,21 @@ public interface GenericCommands {
* @return Response from Redis containing an <code>Object</code>.
*/
CompletableFuture<Object> customCommand(String[] args);

/**
* Execute a transaction by processing the queued commands.
*
* @see <a href="https://redis.io/topics/Transactions/">redis.io</a> 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
* <ul>
* <li>If a command returns a value, it will be included in the list.
* <li>If a command doesn't return a value, the list entry will be empty.
* <li>If the transaction failed due to a <code>WATCH</code> command, <code>exec</code> will
* return <code>null</code>.
* </ul>
*/
CompletableFuture<Object[]> exec(Transaction transaction);
}
188 changes: 188 additions & 0 deletions java/client/src/main/java/glide/api/models/BaseTransaction.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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 <T> child typing for chaining method calls
*/
@Getter
public abstract class BaseTransaction<T extends BaseTransaction<T>> {
/** 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 <em>SUBSCRIBE</em>), or that return potentially more than a single
* response (such as <em>XREAD</em>), or that change the client's behavior (such as entering
* <em>pub</em>/<em>sub</em> mode on <em>RESP2</em> connections) shouldn't be called using
* this function.
* @example Returns a list of all pub/sub clients:
* <pre>
* Object result = client.customCommand("CLIENT","LIST","TYPE", "PUBSUB").get();
* </pre>
*
* @param args Arguments for the custom command.
* @return A response from Redis with an <code>Object</code>.
*/
public T customCommand(String... args) {

ArgsArray commandArgs = buildArgs(args);
protobufTransaction.addCommands(buildCommand(CustomCommand, commandArgs));
return getThis();
}

/**
* Ping the Redis server.
*
* @see <a href="https://redis.io/commands/ping/">redis.io</a> for details.
* @return A response from Redis with a <code>String</code>.
*/
public T ping() {
protobufTransaction.addCommands(buildCommand(Ping));
return getThis();
}

/**
* Ping the Redis server.
*
* @see <a href="https://redis.io/commands/ping/">redis.io</a> for details.
* @param msg The ping argument that will be returned.
* @return A response from Redis with a <code>String</code>.
*/
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 <a href="https://redis.io/commands/info/">redis.io</a> for details.
* @return A response from Redis with a <code>String</code>.
*/
public T info() {
protobufTransaction.addCommands(buildCommand(Info));
return getThis();
}

/**
* Get information and statistics about the Redis server.
*
* @see <a href="https://redis.io/commands/info/">redis.io</a> 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 <code>String</code> 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 <a href="https://redis.io/commands/get/">redis.io</a> for details.
* @param key The key to retrieve from the database.
* @return Response from Redis. <code>key</code> exists, returns the <code>value</code> of <code>
* key</code> as a String. Otherwise, return <code>null</code>.
*/
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 <a href="https://redis.io/commands/set/">redis.io</a> for details.
* @param key The key to store.
* @param value The value to store with the given <code>key</code>.
* @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 <a href="https://redis.io/commands/set/">redis.io</a> 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 <code>String</code> or <code>null</code> response. The old
* value as a <code>String</code> 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 <code>null</code>.
* Otherwise, return <code>OK</code>.
*/
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();
}
}
30 changes: 30 additions & 0 deletions java/client/src/main/java/glide/api/models/ClusterTransaction.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Command Response: An array of command responses is returned by the client <code>exec</code>
* command, in the order they were given. Each element in the array represents a command given to
* the <code>Transaction</code>. The response for each command depends on the executed Redis
* command. Specific response types are documented alongside each method.
*
* @example
* <pre>
* ClusterTransaction transaction = new ClusterTransaction();
* .set("key", "value");
* .get("key");
* ClusterValue[] result = client.exec(transaction, route).get();
* // result contains: OK and "value"
* </pre>
*/
@AllArgsConstructor
public class ClusterTransaction extends BaseTransaction<ClusterTransaction> {
@Override
protected ClusterTransaction getThis() {
return this;
}
}
Loading

0 comments on commit 8823a1a

Please sign in to comment.