From 8823a1a5388f00982763eb9a4865fba396505749 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Mon, 12 Feb 2024 12:24:53 -0800 Subject: [PATCH 01/22] 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); + } +} From 0fbd53f1021730744a9c141f6d7c817f7f44ca54 Mon Sep 17 00:00:00 2001 From: ort-bot Date: Tue, 13 Feb 2024 00:19:23 +0000 Subject: [PATCH 02/22] Updated attribution files --- glide-core/THIRD_PARTY_LICENSES_RUST | 2 +- node/THIRD_PARTY_LICENSES_NODE | 2 +- python/THIRD_PARTY_LICENSES_PYTHON | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index 4462bfe00d..d50e7daa9d 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -5587,7 +5587,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crc32fast:1.3.2 +Package: crc32fast:1.4.0 The following copyrights and licenses were found in the source code of this package: diff --git a/node/THIRD_PARTY_LICENSES_NODE b/node/THIRD_PARTY_LICENSES_NODE index 9586a37aee..698c6030d7 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -5716,7 +5716,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crc32fast:1.3.2 +Package: crc32fast:1.4.0 The following copyrights and licenses were found in the source code of this package: diff --git a/python/THIRD_PARTY_LICENSES_PYTHON b/python/THIRD_PARTY_LICENSES_PYTHON index bc41f2f03f..286957166e 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -5587,7 +5587,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crc32fast:1.3.2 +Package: crc32fast:1.4.0 The following copyrights and licenses were found in the source code of this package: From 341eaafd4493a7e680a191fbc57b7f5b9bebc762 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Mon, 12 Feb 2024 17:00:44 -0800 Subject: [PATCH 03/22] Fix CI. Signed-off-by: Yury-Fridlyand --- .github/workflows/ort.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ort.yml b/.github/workflows/ort.yml index ddfd3bb55c..99929e42af 100644 --- a/.github/workflows/ort.yml +++ b/.github/workflows/ort.yml @@ -19,6 +19,7 @@ on: required: true jobs: run-ort: + if: github.repository_owner == 'aws' name: Create attribution files runs-on: ubuntu-latest strategy: @@ -47,7 +48,7 @@ jobs: ref: ${{ env.BASE_BRANCH }} - name: Set up JDK 11 for the ORT package - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "temurin" java-version: 11 @@ -103,7 +104,7 @@ jobs: ### NodeJS ### - name: Set up Node.js 16.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16.x @@ -133,7 +134,7 @@ jobs: ### Python ### - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" From 0a233c1be517168d2aac2d068b7b65c6aac2c7cd Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:21:23 +0200 Subject: [PATCH 04/22] Python: adds TYPE command (#945) --- CHANGELOG.md | 1 + glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/socket_listener.rs | 1 + python/python/glide/async_commands/core.py | 20 +++++++++++++ .../glide/async_commands/transaction.py | 15 ++++++++++ python/python/tests/test_async_client.py | 30 +++++++++++++++++++ python/python/tests/test_transaction.py | 2 ++ 7 files changed, 70 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90a50a9f6b..10a3186bac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Python, Node: Added RPOPCOUNT and LPOPCOUNT to transaction ([#874](https://github.com/aws/glide-for-redis/pull/874)) * Standalone client: Improve connection errors. ([#854](https://github.com/aws/glide-for-redis/pull/854)) * Python, Node: When recieving LPOP/RPOP with count, convert result to Array. ([#811](https://github.com/aws/glide-for-redis/pull/811)) +* Python: Added TYPE command ([#945](https://github.com/aws/glide-for-redis/pull/945)) #### Features * Python, Node: Added support in Lua Scripts ([#775](https://github.com/aws/glide-for-redis/pull/775), [#860](https://github.com/aws/glide-for-redis/pull/860)) diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 0f5d54eac6..c89654a47f 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -103,6 +103,7 @@ enum RequestType { Zcount = 65; ZIncrBy = 66; ZScore = 67; + Type = 68; } message Command { diff --git a/glide-core/src/socket_listener.rs b/glide-core/src/socket_listener.rs index 945d4131a9..7373827113 100644 --- a/glide-core/src/socket_listener.rs +++ b/glide-core/src/socket_listener.rs @@ -346,6 +346,7 @@ fn get_command(request: &Command) -> Option { RequestType::Zcount => Some(cmd("ZCOUNT")), RequestType::ZIncrBy => Some(cmd("ZINCRBY")), RequestType::ZScore => Some(cmd("ZSCORE")), + RequestType::Type => Some(cmd("TYPE")), } } diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 4993aac2db..f8a86d6051 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -1100,6 +1100,26 @@ async def ttl(self, key: str) -> int: """ return cast(int, await self._execute_command(RequestType.TTL, [key])) + async def type(self, key: str) -> str: + """ + Returns the string representation of the type of the value stored at `key`. + + See https://redis.io/commands/type/ for more details. + + Args: + key (str): The key to check its data type. + + Returns: + str: If the key exists, the type of the stored value is returned. + Otherwise, a "none" string is returned. + + Examples: + >>> await client.set("key", "value") + >>> await client.type("key") + 'string' + """ + return cast(str, await self._execute_command(RequestType.Type, [key])) + async def zadd( self, key: str, diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 37ad50c3d8..73ea272961 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -803,6 +803,21 @@ def ttl(self, key: str): """ self.append_command(RequestType.TTL, [key]) + def type(self, key: str): + """ + Returns the string representation of the type of the value stored at `key`. + + See https://redis.io/commands/type/ for more details. + + Args: + key (str): The key to check its data type. + + Commands response: + str: If the key exists, the type of the stored value is returned. + Otherwise, a "none" string is returned. + """ + self.append_command(RequestType.Type, [key]) + def zadd( self, key: str, diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index ffc0bca23e..1b87d9f19f 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -1174,6 +1174,36 @@ async def test_zscore(self, redis_client: TRedisClient): await redis_client.zscore("non_existing_key", "non_existing_member") == None ) + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_type(self, redis_client: TRedisClient): + key = get_random_string(10) + assert await redis_client.set(key, "value") == OK + assert (await redis_client.type(key)).lower() == "string" + assert await redis_client.delete([key]) == 1 + + assert await redis_client.lpush(key, ["value"]) == 1 + assert (await redis_client.type(key)).lower() == "list" + assert await redis_client.delete([key]) == 1 + + assert await redis_client.sadd(key, ["value"]) == 1 + assert (await redis_client.type(key)).lower() == "set" + assert await redis_client.delete([key]) == 1 + + assert await redis_client.zadd(key, {"member": 1.0}) == 1 + assert (await redis_client.type(key)).lower() == "zset" + assert await redis_client.delete([key]) == 1 + + assert await redis_client.hset(key, {"field": "value"}) == 1 + assert (await redis_client.type(key)).lower() == "hash" + assert await redis_client.delete([key]) == 1 + + await redis_client.custom_command(["XADD", key, "*", "field", "value"]) + assert await redis_client.type(key) == "stream" + assert await redis_client.delete([key]) == 1 + + assert (await redis_client.type(key)).lower() == "none" + class TestCommandsUnitTests: def test_expiry_cmd_args(self): diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 4840f94399..1248053199 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -35,6 +35,7 @@ def transaction_test( transaction.set(key, value) transaction.get(key) + transaction.type(key) transaction.exists([key]) @@ -98,6 +99,7 @@ def transaction_test( return [ OK, value, + "string", 1, 1, None, From 396bb6d618a6bcf3d44f9652008db08dfd7641ba Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:40:16 +0200 Subject: [PATCH 05/22] Python: adds HLEN command (#944) --- CHANGELOG.md | 1 + glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/socket_listener.rs | 1 + python/python/glide/async_commands/core.py | 21 +++++++++++++++++++ .../glide/async_commands/transaction.py | 15 +++++++++++++ python/python/tests/test_async_client.py | 19 +++++++++++++++++ python/python/tests/test_transaction.py | 2 ++ 7 files changed, 60 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10a3186bac..0881bb1013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * Standalone client: Improve connection errors. ([#854](https://github.com/aws/glide-for-redis/pull/854)) * Python, Node: When recieving LPOP/RPOP with count, convert result to Array. ([#811](https://github.com/aws/glide-for-redis/pull/811)) * Python: Added TYPE command ([#945](https://github.com/aws/glide-for-redis/pull/945)) +* Python: Added HLEN command ([#944](https://github.com/aws/glide-for-redis/pull/944)) #### Features * Python, Node: Added support in Lua Scripts ([#775](https://github.com/aws/glide-for-redis/pull/775), [#860](https://github.com/aws/glide-for-redis/pull/860)) diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index c89654a47f..244aefbb86 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -104,6 +104,7 @@ enum RequestType { ZIncrBy = 66; ZScore = 67; Type = 68; + HLen = 69; } message Command { diff --git a/glide-core/src/socket_listener.rs b/glide-core/src/socket_listener.rs index 7373827113..481a07ec80 100644 --- a/glide-core/src/socket_listener.rs +++ b/glide-core/src/socket_listener.rs @@ -347,6 +347,7 @@ fn get_command(request: &Command) -> Option { RequestType::ZIncrBy => Some(cmd("ZINCRBY")), RequestType::ZScore => Some(cmd("ZSCORE")), RequestType::Type => Some(cmd("TYPE")), + RequestType::HLen => Some(cmd("HLEN")), } } diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index f8a86d6051..752bb5f17f 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -589,6 +589,27 @@ async def hdel(self, key: str, fields: List[str]) -> int: int, await self._execute_command(RequestType.HashDel, [key] + fields) ) + async def hlen(self, key: str) -> int: + """ + Returns the number of fields contained in the hash stored at `key`. + + See https://redis.io/commands/hlen/ for more details. + + Args: + key (str): The key of the hash. + + Returns: + int: The number of fields in the hash, or 0 when the key does not exist. + If `key` holds a value that is not a hash, an error is returned. + + Examples: + >>> await client.hlen("my_hash") + 3 + >>> await client.hlen("non_existing_key") + 0 + """ + return cast(int, await self._execute_command(RequestType.HLen, [key])) + async def lpush(self, key: str, elements: List[str]) -> int: """Insert all the specified values at the head of the list stored at `key`. `elements` are inserted one after the other to the head of the list, from the leftmost element diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 73ea272961..d7b9b5262d 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -366,6 +366,21 @@ def hexists(self, key: str, field: str): """ self.append_command(RequestType.HashExists, [key, field]) + def hlen(self, key: str): + """ + Returns the number of fields contained in the hash stored at `key`. + + See https://redis.io/commands/hlen/ for more details. + + Args: + key (str): The key of the hash. + + Command response: + int: The number of fields in the hash, or 0 when the key does not exist. + If `key` holds a value that is not a hash, the transaction fails with an error. + """ + self.append_command(RequestType.HLen, [key]) + def client_getname(self): """ Get the name of the connection on which the transaction is being executed. diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 1b87d9f19f..290613e453 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -718,6 +718,25 @@ async def test_hexist(self, redis_client: TRedisClient): assert await redis_client.hexists(key, "nonExistingField") == False assert await redis_client.hexists("nonExistingKey", field2) == False + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_hlen(self, redis_client: TRedisClient): + key = get_random_string(10) + key2 = get_random_string(5) + field = get_random_string(5) + field2 = get_random_string(5) + field_value_map = {field: "value", field2: "value2"} + + assert await redis_client.hset(key, field_value_map) == 2 + assert await redis_client.hlen(key) == 2 + assert await redis_client.hdel(key, [field]) == 1 + assert await redis_client.hlen(key) == 1 + assert await redis_client.hlen("non_existing_hash") == 0 + + assert await redis_client.set(key2, "value") == OK + with pytest.raises(RequestError): + await redis_client.hlen(key2) + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_lpush_lpop_lrange(self, redis_client: TRedisClient): diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 1248053199..35bd6b68c2 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -62,6 +62,7 @@ def transaction_test( transaction.hset(key4, {key: value, key2: value2}) transaction.hget(key4, key2) + transaction.hlen(key4) transaction.hincrby(key4, key3, 5) transaction.hincrbyfloat(key4, key3, 5.5) @@ -116,6 +117,7 @@ def transaction_test( {"timeout": "1000"}, 2, value2, + 2, 5, 10.5, True, From 82116ed7f1b938c47d9f91eaf98c2461cb68d270 Mon Sep 17 00:00:00 2001 From: Shachar Langbeheim Date: Tue, 13 Feb 2024 17:31:11 +0200 Subject: [PATCH 06/22] node benchmark: use lowerCamelCase. (#951) Replace the old, non-idiomatic snake_case. --- benchmarks/node/node_benchmark.ts | 259 +++++++++++++++--------------- benchmarks/utilities/fill_db.ts | 4 +- benchmarks/utilities/utils.ts | 6 +- 3 files changed, 133 insertions(+), 136 deletions(-) diff --git a/benchmarks/node/node_benchmark.ts b/benchmarks/node/node_benchmark.ts index a68ed17f58..9450bdee1c 100644 --- a/benchmarks/node/node_benchmark.ts +++ b/benchmarks/node/node_benchmark.ts @@ -10,9 +10,9 @@ import percentile from "percentile"; import { RedisClientType, createClient, createCluster } from "redis"; import { stdev } from "stats-lite"; import { - generate_key_get, - generate_key_set, - generate_value, + generateKeyGet, + generateKeySet, + generateValue, getAddress, receivedOptions, } from "../utilities/utils"; @@ -29,16 +29,16 @@ enum ChosenAction { // and save the logs to a file with the name of the results file. Logger.setLoggerConfig("info", parse(receivedOptions.resultsFile).name); -let started_tasks_counter = 0; -const running_tasks: Promise[] = []; -const bench_json_results: object[] = []; +let startedTasksCounter = 0; +const runningTasks: Promise[] = []; +const benchJsonResults: object[] = []; interface IAsyncClient { set: (key: string, value: string) => Promise; get: (key: string) => Promise; } -function choose_action(): ChosenAction { +function chooseAction(): ChosenAction { if (Math.random() > PROB_GET) { return ChosenAction.SET; } @@ -50,77 +50,77 @@ function choose_action(): ChosenAction { return ChosenAction.GET_EXISTING; } -function calculate_latency(latency_list: number[], percentile_point: number) { - const percentile_calculation = percentile(percentile_point, latency_list); - const percentile_value = Array.isArray(percentile_calculation) - ? percentile_calculation[0] - : percentile_calculation; - return Math.round(percentile_value * 100.0) / 100.0; // round to 2 decimal points +function calculateLatency(latencyList: number[], percentile_point: number) { + const percentileCalculation = percentile(percentile_point, latencyList); + const percentileValue = Array.isArray(percentileCalculation) + ? percentileCalculation[0] + : percentileCalculation; + return Math.round(percentileValue * 100.0) / 100.0; // round to 2 decimal points } -function print_results(resultsFile: string) { - writeFileSync(resultsFile, JSON.stringify(bench_json_results)); +function printResults(resultsFile: string) { + writeFileSync(resultsFile, JSON.stringify(benchJsonResults)); } -async function redis_benchmark( +async function redisBenchmark( clients: IAsyncClient[], - total_commands: number, + totalCommands: number, data: string, - action_latencies: Record + actionLatencies: Record ) { - while (started_tasks_counter < total_commands) { - started_tasks_counter += 1; - const chosen_action = choose_action(); + while (startedTasksCounter < totalCommands) { + startedTasksCounter += 1; + const chosen_action = chooseAction(); const tic = process.hrtime(); - const client = clients[started_tasks_counter % clients.length]; + const client = clients[startedTasksCounter % clients.length]; switch (chosen_action) { case ChosenAction.GET_EXISTING: - await client.get(generate_key_set()); + await client.get(generateKeySet()); break; case ChosenAction.GET_NON_EXISTING: - await client.get(generate_key_get()); + await client.get(generateKeyGet()); break; case ChosenAction.SET: - await client.set(generate_key_set(), data); + await client.set(generateKeySet(), data); break; } const toc = process.hrtime(tic); - const latency_list = action_latencies[chosen_action]; - latency_list.push(toc[0] * 1000 + toc[1] / 1000000); + const latencyList = actionLatencies[chosen_action]; + latencyList.push(toc[0] * 1000 + toc[1] / 1000000); } } -async function create_bench_tasks( +async function createBenchTasks( clients: IAsyncClient[], - total_commands: number, - num_of_concurrent_tasks: number, + totalCommands: number, + numOfConcurrentTasks: number, data: string, - action_latencies: Record + actionLatencies: Record ) { - started_tasks_counter = 0; + startedTasksCounter = 0; const tic = process.hrtime(); - for (let i = 0; i < num_of_concurrent_tasks; i++) { - running_tasks.push( - redis_benchmark(clients, total_commands, data, action_latencies) + for (let i = 0; i < numOfConcurrentTasks; i++) { + runningTasks.push( + redisBenchmark(clients, totalCommands, data, actionLatencies) ); } - await Promise.all(running_tasks); + await Promise.all(runningTasks); const toc = process.hrtime(tic); return toc[0] + toc[1] / 1000000000; } -function latency_results( +function latencyResults( prefix: string, latencies: number[] ): Record { const result: Record = {}; - result[prefix + "_p50_latency"] = calculate_latency(latencies, 50); - result[prefix + "_p90_latency"] = calculate_latency(latencies, 90); - result[prefix + "_p99_latency"] = calculate_latency(latencies, 99); + result[prefix + "_p50_latency"] = calculateLatency(latencies, 50); + result[prefix + "_p90_latency"] = calculateLatency(latencies, 90); + result[prefix + "_p99_latency"] = calculateLatency(latencies, 99); result[prefix + "_average_latency"] = latencies.reduce((a, b) => a + b, 0) / latencies.length; result[prefix + "_std_dev"] = stdev(latencies); @@ -128,65 +128,65 @@ function latency_results( return result; } -async function run_clients( +async function runClients( clients: IAsyncClient[], - client_name: string, - total_commands: number, - num_of_concurrent_tasks: number, - data_size: number, + clientName: string, + totalCommands: number, + numOfConcurrentTasks: number, + dataSize: number, data: string, clientDisposal: (client: IAsyncClient) => void, - is_cluster: boolean + isCluster: boolean ) { const now = new Date(); console.log( - `Starting ${client_name} data size: ${data_size} concurrency: ${num_of_concurrent_tasks} client count: ${ + `Starting ${clientName} data size: ${dataSize} concurrency: ${numOfConcurrentTasks} client count: ${ clients.length - } is_cluster: ${is_cluster} ${now.toLocaleTimeString()}` + } isCluster: ${isCluster} ${now.toLocaleTimeString()}` ); - const action_latencies = { + const actionLatencies = { [ChosenAction.SET]: [], [ChosenAction.GET_NON_EXISTING]: [], [ChosenAction.GET_EXISTING]: [], }; - const time = await create_bench_tasks( + const time = await createBenchTasks( clients, - total_commands, - num_of_concurrent_tasks, + totalCommands, + numOfConcurrentTasks, data, - action_latencies + actionLatencies ); - const tps = Math.round(started_tasks_counter / time); + const tps = Math.round(startedTasksCounter / time); - const get_non_existing_latencies = - action_latencies[ChosenAction.GET_NON_EXISTING]; - const get_non_existing_latency_results = latency_results( + const getNonExistingLatencies = + actionLatencies[ChosenAction.GET_NON_EXISTING]; + const getNonExistingLatencyResults = latencyResults( "get_non_existing", - get_non_existing_latencies + getNonExistingLatencies ); - const get_existing_latencies = action_latencies[ChosenAction.GET_EXISTING]; - const get_existing_latency_results = latency_results( + const getExistingLatencies = actionLatencies[ChosenAction.GET_EXISTING]; + const getExistingLatencyResults = latencyResults( "get_existing", - get_existing_latencies + getExistingLatencies ); - const set_latencies = action_latencies[ChosenAction.SET]; - const set_latency_results = latency_results("set", set_latencies); + const setLatencies = actionLatencies[ChosenAction.SET]; + const setLatencyResults = latencyResults("set", setLatencies); - const json_res = { - client: client_name, - num_of_tasks: num_of_concurrent_tasks, - data_size, + const jsonRes = { + client: clientName, + num_of_tasks: numOfConcurrentTasks, + data_size: dataSize, tps, client_count: clients.length, - is_cluster, - ...set_latency_results, - ...get_existing_latency_results, - ...get_non_existing_latency_results, + is_cluster: isCluster, + ...setLatencyResults, + ...getExistingLatencyResults, + ...getNonExistingLatencyResults, }; - bench_json_results.push(json_res); + benchJsonResults.push(jsonRes); Promise.all(clients.map((client) => clientDisposal(client))); } @@ -202,19 +202,19 @@ function createClients( } async function main( - total_commands: number, - num_of_concurrent_tasks: number, - data_size: number, - clients_to_run: "all" | "glide", + totalCommands: number, + numOfConcurrentTasks: number, + dataSize: number, + clientsToRun: "all" | "glide", host: string, clientCount: number, useTLS: boolean, clusterModeEnabled: boolean, port: number ) { - const data = generate_value(data_size); + const data = generateValue(dataSize); - if (clients_to_run == "all" || clients_to_run == "glide") { + if (clientsToRun == "all" || clientsToRun == "glide") { const clientClass = clusterModeEnabled ? RedisClusterClient : RedisClient; @@ -224,12 +224,12 @@ async function main( useTLS, }) ); - await run_clients( + await runClients( clients, "glide", - total_commands, - num_of_concurrent_tasks, - data_size, + totalCommands, + numOfConcurrentTasks, + dataSize, data, (client) => { (client as RedisClient).close(); @@ -239,34 +239,31 @@ async function main( await new Promise((resolve) => setTimeout(resolve, 100)); } - if (clients_to_run == "all") { - const node_redis_clients = await createClients( - clientCount, - async () => { - const node = { - url: getAddress(host, useTLS, port), - }; - const node_redis_client = clusterModeEnabled - ? createCluster({ - rootNodes: [{ socket: { host, port, tls: useTLS } }], - defaults: { - socket: { - tls: useTLS, - }, + if (clientsToRun == "all") { + const nodeRedisClients = await createClients(clientCount, async () => { + const node = { + url: getAddress(host, useTLS, port), + }; + const nodeRedisClient = clusterModeEnabled + ? createCluster({ + rootNodes: [{ socket: { host, port, tls: useTLS } }], + defaults: { + socket: { + tls: useTLS, }, - useReplicas: true, - }) - : createClient(node); - await node_redis_client.connect(); - return node_redis_client; - } - ); - await run_clients( - node_redis_clients, + }, + useReplicas: true, + }) + : createClient(node); + await nodeRedisClient.connect(); + return nodeRedisClient; + }); + await runClients( + nodeRedisClients, "node_redis", - total_commands, - num_of_concurrent_tasks, - data_size, + totalCommands, + numOfConcurrentTasks, + dataSize, data, (client) => { (client as RedisClientType).disconnect(); @@ -276,8 +273,8 @@ async function main( await new Promise((resolve) => setTimeout(resolve, 100)); const tls = useTLS ? {} : undefined; - const ioredis_clients = await createClients(clientCount, async () => { - const ioredis_client = clusterModeEnabled + const ioredisClients = await createClients(clientCount, async () => { + const ioredisClient = clusterModeEnabled ? new Cluster([{ host, port }], { dnsLookup: (address, callback) => callback(null, address), scaleReads: "all", @@ -288,14 +285,14 @@ async function main( : new Redis(port, host, { tls, }); - return ioredis_client; + return ioredisClient; }); - await run_clients( - ioredis_clients, + await runClients( + ioredisClients, "ioredis", - total_commands, - num_of_concurrent_tasks, - data_size, + totalCommands, + numOfConcurrentTasks, + dataSize, data, (client) => { (client as RedisClientType).disconnect(); @@ -305,14 +302,14 @@ async function main( } } -const number_of_iterations = (num_of_concurrent_tasks: number) => - Math.min(Math.max(100000, num_of_concurrent_tasks * 10000), 10000000); +const numberOfIterations = (numOfConcurrentTasks: number) => + Math.min(Math.max(100000, numOfConcurrentTasks * 10000), 10000000); Promise.resolve() // just added to clean the indentation of the rest of the calls .then(async () => { - const data_size = parseInt(receivedOptions.dataSize); - const concurrent_tasks: string[] = receivedOptions.concurrentTasks; - const clients_to_run = receivedOptions.clients; + const dataSize = parseInt(receivedOptions.dataSize); + const concurrentTasks: string[] = receivedOptions.concurrentTasks; + const clientsToRun = receivedOptions.clients; const clientCount: string[] = receivedOptions.clientCount; const lambda: ( numOfClients: string, @@ -320,27 +317,27 @@ Promise.resolve() // just added to clean the indentation of the rest of the call ) => [number, number, number] = ( numOfClients: string, concurrentTasks: string - ) => [parseInt(concurrentTasks), data_size, parseInt(numOfClients)]; - const product: [number, number, number][] = concurrent_tasks + ) => [parseInt(concurrentTasks), dataSize, parseInt(numOfClients)]; + const product: [number, number, number][] = concurrentTasks .flatMap((concurrentTasks: string) => clientCount.map((clientCount) => lambda(clientCount, concurrentTasks) ) ) .filter( - ([concurrent_tasks, , clientCount]) => - clientCount <= concurrent_tasks + ([concurrentTasks, , clientCount]) => + clientCount <= concurrentTasks ); - for (const [concurrent_tasks, data_size, clientCount] of product) { + for (const [concurrentTasks, dataSize, clientCount] of product) { const iterations = receivedOptions.minimal ? 1000 - : number_of_iterations(concurrent_tasks); + : numberOfIterations(concurrentTasks); await main( iterations, - concurrent_tasks, - data_size, - clients_to_run, + concurrentTasks, + dataSize, + clientsToRun, receivedOptions.host, clientCount, receivedOptions.tls, @@ -349,7 +346,7 @@ Promise.resolve() // just added to clean the indentation of the rest of the call ); } - print_results(receivedOptions.resultsFile); + printResults(receivedOptions.resultsFile); }) .then(() => { process.exit(0); diff --git a/benchmarks/utilities/fill_db.ts b/benchmarks/utilities/fill_db.ts index aa6a9c41c6..c43b582386 100644 --- a/benchmarks/utilities/fill_db.ts +++ b/benchmarks/utilities/fill_db.ts @@ -5,7 +5,7 @@ import { SIZE_SET_KEYSPACE, createRedisClient, - generate_value, + generateValue, receivedOptions, } from "./utils"; @@ -17,7 +17,7 @@ async function fill_database( port: number ) { const client = await createRedisClient(host, isCluster, tls, port); - const data = generate_value(data_size); + const data = generateValue(data_size); await client.connect(); const CONCURRENT_SETS = 1000; diff --git a/benchmarks/utilities/utils.ts b/benchmarks/utilities/utils.ts index e6b9a86edd..f4fcc617cb 100644 --- a/benchmarks/utilities/utils.ts +++ b/benchmarks/utilities/utils.ts @@ -65,15 +65,15 @@ const optionDefinitions = [ export const receivedOptions = commandLineArgs(optionDefinitions); -export function generate_value(size: number): string { +export function generateValue(size: number): string { return "0".repeat(size); } -export function generate_key_set(): string { +export function generateKeySet(): string { return (Math.floor(Math.random() * SIZE_SET_KEYSPACE) + 1).toString(); } -export function generate_key_get(): string { +export function generateKeyGet(): string { const range = SIZE_GET_KEYSPACE - SIZE_SET_KEYSPACE; return Math.floor(Math.random() * range + SIZE_SET_KEYSPACE + 1).toString(); } From 7b4c2f09e30ae399f63e248baebaaca1acd3fcd7 Mon Sep 17 00:00:00 2001 From: Shachar Langbeheim Date: Tue, 13 Feb 2024 17:31:17 +0200 Subject: [PATCH 07/22] Fix editor config's indentation. (#950) The `Prettier` formatter takes its config from this file, and since there's no default indentation, it resets its indentation to 2 spaces. This change fixes the issue by adding default indentation to all languages. --- .editorconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index 824238f9c1..abcc626724 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,6 +5,8 @@ root = true trim_trailing_whitespace = true insert_final_newline = true indent_style = space +indent_size = 4 +tab_width = 4 # Xml files [*.xml] @@ -16,8 +18,6 @@ indent_size = 2 #### Core EditorConfig Options #### # Indentation and spacing -indent_size = 4 -tab_width = 4 # New line preferences end_of_line = lf From 7ff2e8ee8614a9734830bbe427723b5c7c555d88 Mon Sep 17 00:00:00 2001 From: Adan Wattad Date: Wed, 14 Feb 2024 11:38:41 +0200 Subject: [PATCH 08/22] Added zscore command in node. (#889) * Added zscore command in node. * added zscore and zcard commands to CHANGELOG.md --- CHANGELOG.md | 4 ++-- node/src/BaseClient.ts | 15 +++++++++++++++ node/src/Commands.ts | 7 +++++++ node/src/Transaction.ts | 16 ++++++++++++++++ node/tests/SharedTests.ts | 24 ++++++++++++++++++++++++ node/tests/TestUtilities.ts | 4 +++- 6 files changed, 67 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0881bb1013..17e8e382bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ ## 0.2.0 (2024-02-11) #### Changes -* Python, Node: Added ZCARD command ([#877](https://github.com/aws/glide-for-redis/pull/885), [#885](https://github.com/aws/glide-for-redis/pull/885)) +* Python, Node: Added ZCARD command ([#871](https://github.com/aws/glide-for-redis/pull/871), [#885](https://github.com/aws/glide-for-redis/pull/885)) * Python, Node: Added ZADD and ZADDINCR commands ([#814](https://github.com/aws/glide-for-redis/pull/814), [#830](https://github.com/aws/glide-for-redis/pull/830)) * Python, Node: Added ZREM command ([#834](https://github.com/aws/glide-for-redis/pull/834), [#831](https://github.com/aws/glide-for-redis/pull/831)) -* Python, Node: Added ZSCORE command ([#885](https://github.com/aws/glide-for-redis/pull/885), [#871](https://github.com/aws/glide-for-redis/pull/871)) +* Python, Node: Added ZSCORE command ([#877](https://github.com/aws/glide-for-redis/pull/877), [#889](https://github.com/aws/glide-for-redis/pull/889)) * Use jemalloc as default allocator. ([#847](https://github.com/aws/glide-for-redis/pull/847)) * Python, Node: Added RPOPCOUNT and LPOPCOUNT to transaction ([#874](https://github.com/aws/glide-for-redis/pull/874)) * Standalone client: Improve connection errors. ([#854](https://github.com/aws/glide-for-redis/pull/854)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 0f7276a3b3..e6cc107983 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -54,6 +54,7 @@ import { createZadd, createZcard, createZrem, + createZscore, } from "./Commands"; import { ClosingError, @@ -1062,6 +1063,20 @@ export class BaseClient { return this.createWritePromise(createZcard(key)); } + /** Returns the score of `member` in the sorted set stored at `key`. + * See https://redis.io/commands/zscore/ for more details. + * + * @param key - The key of the sorted set. + * @param member - The member whose score is to be retrieved. + * @returns The score of the member. + * If `member` does not exist in the sorted set, null is returned. + * If `key` does not exist, null is returned. + * If `key` holds a value that is not a sorted set, an error is returned. + */ + public zscore(key: string, member: string): Promise { + return this.createWritePromise(createZscore(key, member)); + } + private readonly MAP_READ_FROM_STRATEGY: Record< ReadFrom, connection_request.ReadFrom diff --git a/node/src/Commands.ts b/node/src/Commands.ts index ff2a0b8ab5..3363d65d52 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -760,3 +760,10 @@ export function createZrem( export function createZcard(key: string): redis_request.Command { return createCommand(RequestType.Zcard, [key]); } + +/** + * @internal + */ +export function createZscore(key: string, member: string): redis_request.Command { + return createCommand(RequestType.ZScore, [key, member]); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 7a12f04670..7e5a434c5b 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -57,6 +57,7 @@ import { createZadd, createZcard, createZrem, + createZscore, } from "./Commands"; import { redis_request } from "./ProtobufMessage"; @@ -821,6 +822,21 @@ export class BaseTransaction> { return this.addAndReturn(createZcard(key)); } + /** Returns the score of `member` in the sorted set stored at `key`. + * See https://redis.io/commands/zscore/ for more details. + * + * @param key - The key of the sorted set. + * @param member - The member whose score is to be retrieved. + * + * Command Response - The score of the member. + * If `member` does not exist in the sorted set, null is returned. + * If `key` does not exist, null is returned. + * If `key` holds a value that is not a sorted set, an error is returned. + */ + public zscore(key: string, member: string) { + this.commands.push(createZscore(key, member)); + } + /** Executes a single command, without checking inputs. Every part of the command, including subcommands, * should be added as a separate value in args. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 025e70ff2c..d8da1c397e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1389,6 +1389,30 @@ export function runBaseTests(config: { }, config.timeout ); + + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zscore test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const membersScores = { one: 1, two: 2, three: 3 }; + expect(await client.zadd(key1, membersScores)).toEqual(3); + expect(await client.zscore(key1, "one")).toEqual(1.0); + expect(await client.zscore(key1, "nonExistingMember")).toEqual( + null + ); + expect( + await client.zscore("nonExistingKey", "nonExistingMember") + ).toEqual(null); + + expect(await client.set(key2, "foo")).toEqual("OK"); + await expect(client.zscore(key2, "foo")).rejects.toThrow(); + }, protocol); + }, + config.timeout + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index e25dede769..6ce9330180 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -95,7 +95,8 @@ export function transactionTest( .zadd(key8, { member1: 1, member2: 2 }) .zaddIncr(key8, "member2", 1) .zrem(key8, ["member1"]) - .zcard(key8); + .zcard(key8) + .zscore(key8, "member2"); return [ "OK", null, @@ -127,6 +128,7 @@ export function transactionTest( 3, 1, 1, + 3.0, ]; } From 25e8565677ac3cc270dcaa7a57e68fc1d6cb2d26 Mon Sep 17 00:00:00 2001 From: Guriy Samarin Date: Wed, 14 Feb 2024 11:26:29 +0000 Subject: [PATCH 09/22] C#: enable net8.0 tests (#952) enable net8.0 tests Co-authored-by: Guriy Samarin --- .github/workflows/csharp.yml | 11 +++++++---- benchmarks/install_and_test.sh | 5 +++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index 3c838c38c5..dafb45bf25 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -28,6 +28,9 @@ jobs: redis: - 6.2.14 - 7.2.3 + dotnet: + - 6.0 + - 8.0 steps: - uses: actions/checkout@v4 @@ -44,10 +47,10 @@ jobs: with: version: "25.1" - - name: Set up dotnet + - name: Set up dotnet ${{ matrix.dotnet }} uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x + dotnet-version: ${{ matrix.dotnet }} - name: Start redis server run: redis-server & @@ -56,9 +59,9 @@ jobs: working-directory: ./csharp run: dotnet format --verify-no-changes --verbosity diagnostic - - name: Test + - name: Test dotnet ${{ matrix.dotnet }} working-directory: ./csharp - run: dotnet test --framework net6.0 /warnaserror + run: dotnet test --framework net${{ matrix.dotnet }} /warnaserror - uses: ./.github/workflows/test-benchmark with: diff --git a/benchmarks/install_and_test.sh b/benchmarks/install_and_test.sh index 2e0a80957e..b45f8ba23f 100755 --- a/benchmarks/install_and_test.sh +++ b/benchmarks/install_and_test.sh @@ -69,6 +69,7 @@ function runCSharpBenchmark(){ dotnet clean dotnet build --configuration Release /warnaserror dotnet run --framework net6.0 --configuration Release --resultsFile=../$1 --dataSize $2 --concurrentTasks $concurrentTasks --clients $chosenClients --host $host --clientCount $clientCount $tlsFlag $portFlag $minimalFlag + dotnet run --framework net8.0 --configuration Release --resultsFile=../$1 --dataSize $2 --concurrentTasks $concurrentTasks --clients $chosenClients --host $host --clientCount $clientCount $tlsFlag $portFlag $minimalFlag } function runJavaBenchmark(){ @@ -230,7 +231,7 @@ do ;; -minimal) minimalFlag="--minimal" - ;; + ;; esac shift done @@ -241,7 +242,7 @@ do then echo "Minimal run, not filling database" flushDB - else + else fillDB $currentDataSize fi From b83247641ade70b7dccc2e7d8c53cdd94bd8e38c Mon Sep 17 00:00:00 2001 From: barshaul Date: Wed, 14 Feb 2024 09:03:22 +0000 Subject: [PATCH 10/22] Fixed Python example to properly work with python cli --- python/README.md | 59 +++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/python/README.md b/python/README.md index b26d650564..30353a7c31 100644 --- a/python/README.md +++ b/python/README.md @@ -44,39 +44,42 @@ To install GLIDE for Redis using `pip`, follow these steps: #### Cluster Redis: ```python: ->>> from glide import ( -... NodeAddress, -... ClusterClientConfiguration, -... RedisClusterClient, -... ) ->>> addresses = [NodeAddress("redis.example.com", 6379)] ->>> config = ClusterClientConfiguration( -... addresses=addresses -... ) ->>> client = await RedisClusterClient.create(config) ->>> await client.set("foo", "bar") -'OK' ->>> await client.get("foo") -'bar' +>>> import asyncio +>>> from glide import ClusterClientConfiguration, NodeAddress, RedisClusterClient +>>> async def test_cluster_client(): +... addresses = [NodeAddress("redis.example.com", 6379)] +... config = ClusterClientConfiguration(addresses) +... client = await RedisClusterClient.create(config) +... set_result = await client.set("foo", "bar") +... print(f"Set response is {set_result}") +... get_result = await client.get("foo") +... print(f"Get response is {get_result}") +... +>>> asyncio.run(test_cluster_client()) +Set response is OK +Get response is bar ``` #### Standalone Redis: ```python: ->>> from glide import ( -... NodeAddress, -... RedisClientConfiguration, -... RedisClient, -... ) ->>> addresses = [NodeAddress("redis_primary.example.com", 6379), NodeAddress("redis_replica.example.com", 6379)] ->>> config = RedisClientConfiguration( -... addresses=addresses -... ) ->>> client = await RedisClient.create(config) ->>> await client.set("foo", "bar") -'OK' ->>> await client.get("foo") -'bar' +>>> import asyncio +>>> from glide import RedisClientConfiguration, NodeAddress, RedisClient +>>> async def test_standalone_client(): +... addresses = [ +... NodeAddress("redis_primary.example.com", 6379), +... NodeAddress("redis_replica.example.com", 6379) +... ] +... config = RedisClientConfiguration(addresses) +... client = await RedisClient.create(config) +... set_result = await client.set("foo", "bar") +... print(f"Set response is {set_result}") +... get_result = await client.get("foo") +... print(f"Get response is {get_result}") +... +>>> asyncio.run(test_standalone_client()) +Set response is OK +Get response is bar ``` ## Documentation From 811bcc6e0be89aa66cc7d63574c4fd6e50fed425 Mon Sep 17 00:00:00 2001 From: ort-bot Date: Wed, 14 Feb 2024 00:18:53 +0000 Subject: [PATCH 11/22] Updated attribution files --- glide-core/THIRD_PARTY_LICENSES_RUST | 4 ++-- node/THIRD_PARTY_LICENSES_NODE | 4 ++-- python/THIRD_PARTY_LICENSES_PYTHON | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index d50e7daa9d..45e744ff2a 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -12482,7 +12482,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: hermit-abi:0.3.5 +Package: hermit-abi:0.3.6 The following copyrights and licenses were found in the source code of this package: @@ -24081,7 +24081,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pki-types:1.2.0 +Package: rustls-pki-types:1.3.0 The following copyrights and licenses were found in the source code of this package: diff --git a/node/THIRD_PARTY_LICENSES_NODE b/node/THIRD_PARTY_LICENSES_NODE index 698c6030d7..7570527299 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -13048,7 +13048,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: hermit-abi:0.3.5 +Package: hermit-abi:0.3.6 The following copyrights and licenses were found in the source code of this package: @@ -25477,7 +25477,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pki-types:1.2.0 +Package: rustls-pki-types:1.3.0 The following copyrights and licenses were found in the source code of this package: diff --git a/python/THIRD_PARTY_LICENSES_PYTHON b/python/THIRD_PARTY_LICENSES_PYTHON index 286957166e..1ee24d9415 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -12919,7 +12919,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: hermit-abi:0.3.5 +Package: hermit-abi:0.3.6 The following copyrights and licenses were found in the source code of this package: @@ -25896,7 +25896,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pki-types:1.2.0 +Package: rustls-pki-types:1.3.0 The following copyrights and licenses were found in the source code of this package: From 6ea7701870bd389dd397580755c48d12acaf4535 Mon Sep 17 00:00:00 2001 From: Adan Wattad Date: Wed, 14 Feb 2024 14:36:19 +0200 Subject: [PATCH 12/22] Added zcount command in node. (#909) --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 21 ++++++++++++++ node/src/Commands.ts | 49 ++++++++++++++++++++++++++++++++ node/src/Transaction.ts | 22 +++++++++++++-- node/tests/SharedTests.ts | 56 +++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 4 ++- 6 files changed, 150 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17e8e382bc..2b075352f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * Python, Node: When recieving LPOP/RPOP with count, convert result to Array. ([#811](https://github.com/aws/glide-for-redis/pull/811)) * Python: Added TYPE command ([#945](https://github.com/aws/glide-for-redis/pull/945)) * Python: Added HLEN command ([#944](https://github.com/aws/glide-for-redis/pull/944)) +* Node: Added ZCOUNT command ([#909](https://github.com/aws/glide-for-redis/pull/909)) #### Features * Python, Node: Added support in Lua Scripts ([#775](https://github.com/aws/glide-for-redis/pull/775), [#860](https://github.com/aws/glide-for-redis/pull/860)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index e6cc107983..8ebdbcc1ca 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -12,6 +12,7 @@ import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; import { ExpireOptions, + ScoreLimit, SetOptions, ZaddOptions, createDecr, @@ -53,6 +54,7 @@ import { createUnlink, createZadd, createZcard, + createZcount, createZrem, createZscore, } from "./Commands"; @@ -1077,6 +1079,25 @@ export class BaseClient { return this.createWritePromise(createZscore(key, member)); } + /** Returns the number of members in the sorted set stored at `key` with scores between `minScore` and `maxScore`. + * See https://redis.io/commands/zcount/ for more details. + * + * @param key - The key of the sorted set. + * @param minScore - The minimum score to count from. Can be positive/negative infinity, or specific score and inclusivity. + * @param maxScore - The maximum score to count up to. Can be positive/negative infinity, or specific score and inclusivity. + * @returns The number of members in the specified score range. + * If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + * If `minScore` is greater than `maxScore`, 0 is returned. + * If `key` holds a value that is not a sorted set, an error is returned. + */ + public zcount( + key: string, + minScore: ScoreLimit, + maxScore: ScoreLimit + ): Promise { + return this.createWritePromise(createZcount(key, minScore, maxScore)); + } + private readonly MAP_READ_FROM_STRATEGY: Record< ReadFrom, connection_request.ReadFrom diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 3363d65d52..d8ab96f9c4 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -767,3 +767,52 @@ export function createZcard(key: string): redis_request.Command { export function createZscore(key: string, member: string): redis_request.Command { return createCommand(RequestType.ZScore, [key, member]); } + +export type ScoreLimit = + | `positiveInfinity` + | `negativeInfinity` + | { + bound: number; + isInclusive?: boolean; + }; + +const positiveInfinityArg = "+inf"; +const negativeInfinityArg = "-inf"; +const isInclusiveArg = "("; + +/** + * @internal + */ +export function createZcount( + key: string, + minScore: ScoreLimit, + maxScore: ScoreLimit +): redis_request.Command { + const args = [key]; + + if (minScore == "positiveInfinity") { + args.push(positiveInfinityArg); + } else if (minScore == "negativeInfinity") { + args.push(negativeInfinityArg); + } else { + const value = + minScore.isInclusive == false + ? isInclusiveArg + minScore.bound.toString() + : minScore.bound.toString(); + args.push(value); + } + + if (maxScore == "positiveInfinity") { + args.push(positiveInfinityArg); + } else if (maxScore == "negativeInfinity") { + args.push(negativeInfinityArg); + } else { + const value = + maxScore.isInclusive == false + ? isInclusiveArg + maxScore.bound.toString() + : maxScore.bound.toString(); + args.push(value); + } + + return createCommand(RequestType.Zcount, args); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 7e5a434c5b..fee9dd583a 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -5,6 +5,7 @@ import { ExpireOptions, InfoOptions, + ScoreLimit, SetOptions, ZaddOptions, createClientGetName, @@ -56,6 +57,7 @@ import { createUnlink, createZadd, createZcard, + createZcount, createZrem, createZscore, } from "./Commands"; @@ -833,8 +835,24 @@ export class BaseTransaction> { * If `key` does not exist, null is returned. * If `key` holds a value that is not a sorted set, an error is returned. */ - public zscore(key: string, member: string) { - this.commands.push(createZscore(key, member)); + public zscore(key: string, member: string): T { + return this.addAndReturn(createZscore(key, member)); + } + + /** Returns the number of members in the sorted set stored at `key` with scores between `minScore` and `maxScore`. + * See https://redis.io/commands/zcount/ for more details. + * + * @param key - The key of the sorted set. + * @param minScore - The minimum score to count from. Can be positive/negative infinity, or specific score and inclusivity. + * @param maxScore - The maximum score to count up to. Can be positive/negative infinity, or specific score and inclusivity. + * + * Command Response - The number of members in the specified score range. + * If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + * If `minScore` is greater than `maxScore`, 0 is returned. + * If `key` holds a value that is not a sorted set, an error is returned. + */ + public zcount(key: string, minScore: ScoreLimit, maxScore: ScoreLimit): T { + return this.addAndReturn(createZcount(key, minScore, maxScore)); } /** Executes a single command, without checking inputs. Every part of the command, including subcommands, diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index d8da1c397e..3aa60e9101 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1413,6 +1413,62 @@ export function runBaseTests(config: { }, config.timeout ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zcount test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const membersScores = { one: 1, two: 2, three: 3 }; + expect(await client.zadd(key1, membersScores)).toEqual(3); + expect( + await client.zcount( + key1, + "negativeInfinity", + "positiveInfinity" + ) + ).toEqual(3); + expect( + await client.zcount( + key1, + { bound: 1, isInclusive: false }, + { bound: 3, isInclusive: false } + ) + ).toEqual(1); + expect( + await client.zcount( + key1, + { bound: 1, isInclusive: false }, + { bound: 3 } + ) + ).toEqual(2); + expect( + await client.zcount(key1, "negativeInfinity", { + bound: 3, + }) + ).toEqual(3); + expect( + await client.zcount(key1, "positiveInfinity", { + bound: 3, + }) + ).toEqual(0); + expect( + await client.zcount( + "nonExistingKey", + "negativeInfinity", + "positiveInfinity" + ) + ).toEqual(0); + + expect(await client.set(key2, "foo")).toEqual("OK"); + await expect( + client.zcount(key2, "negativeInfinity", "positiveInfinity") + ).rejects.toThrow(); + }, protocol); + }, + config.timeout + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 6ce9330180..92f91bd17d 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -96,7 +96,8 @@ export function transactionTest( .zaddIncr(key8, "member2", 1) .zrem(key8, ["member1"]) .zcard(key8) - .zscore(key8, "member2"); + .zscore(key8, "member2") + .zcount(key8, { bound: 2 }, "positiveInfinity"); return [ "OK", null, @@ -129,6 +130,7 @@ export function transactionTest( 1, 1, 3.0, + 1, ]; } From 84667483ffed6170f29535cbf39638a10f1d01c4 Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Wed, 14 Feb 2024 19:10:38 +0200 Subject: [PATCH 13/22] fix commands doc in python (#964) --- .../glide/async_commands/cluster_commands.py | 44 +-- python/python/glide/async_commands/core.py | 192 +++++++------ .../async_commands/standalone_commands.py | 42 +-- .../glide/async_commands/transaction.py | 252 ++++++++++-------- 4 files changed, 297 insertions(+), 233 deletions(-) diff --git a/python/python/glide/async_commands/cluster_commands.py b/python/python/glide/async_commands/cluster_commands.py index 00bb1376d4..5aee73045c 100644 --- a/python/python/glide/async_commands/cluster_commands.py +++ b/python/python/glide/async_commands/cluster_commands.py @@ -15,7 +15,8 @@ class ClusterCommands(CoreCommands): async def custom_command( self, command_args: List[str], route: Optional[Route] = None ) -> TResult: - """Executes a single command, without checking inputs. + """ + Executes a single command, without checking inputs. @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 - Return a list of all pub/sub clients from all nodes: @@ -38,13 +39,14 @@ async def info( sections: Optional[List[InfoSection]] = None, route: Optional[Route] = None, ) -> TClusterResponse[str]: - """Get information and statistics about the Redis server. + """ + Get information and statistics about the Redis server. See https://redis.io/commands/info/ for details. Args: sections (Optional[List[InfoSection]]): A list of InfoSection values specifying which sections of information to retrieve. When no parameter is provided, the default option is assumed. - route (Optional[Route]): The command will be routed to all primeries, unless `route` is provided, in which + route (Optional[Route]): The command will be routed to all primaries, unless `route` is provided, in which case the client will route the command to the nodes defined by `route`. Defaults to None. Returns: @@ -64,7 +66,8 @@ async def exec( transaction: BaseTransaction | ClusterTransaction, route: Optional[TSingleNodeRoute] = None, ) -> Optional[List[TResult]]: - """Execute a transaction by processing the queued commands. + """ + Execute a transaction by processing the queued commands. See https://redis.io/topics/Transactions/ for details on Redis Transactions. Args: @@ -86,11 +89,14 @@ async def config_resetstat( self, route: Optional[Route] = None, ) -> TOK: - """Reset the statistics reported by Redis. + """ + Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. See https://redis.io/commands/config-resetstat/ for details. + Args: route (Optional[Route]): The command will be routed automatically to all nodes, unless `route` is provided, in which case the client will route the command to the nodes defined by `route`. Defaults to None. + Returns: OK: Returns "OK" to confirm that the statistics were successfully reset. """ @@ -102,13 +108,16 @@ async def config_rewrite( self, route: Optional[Route] = None, ) -> TOK: - """Rewrite the configuration file with the current configuration. + """ + Rewrite the configuration file with the current configuration. See https://redis.io/commands/config-rewrite/ for details. + Args: route (Optional[TRoute]): The command will be routed automatically to all nodes, unless `route` is provided, in which case the client will route the command to the nodes defined by `route`. Defaults to None. + Returns: - OK: OK is returned when the configuration was rewritten properly. Otherwise an error is returned. + OK: OK is returned when the configuration was rewritten properly. Otherwise an error is raised. """ return cast( TOK, await self._execute_command(RequestType.ConfigRewrite, [], route) @@ -118,8 +127,10 @@ async def client_id( self, route: Optional[Route] = None, ) -> TClusterResponse[int]: - """Returns the current connection id. + """ + Returns the current connection id. See https://redis.io/commands/client-id/ for more information. + Args: route (Optional[Route]): The command will be sent to a random node, unless `route` is provided, in which case the client will route the command to the nodes defined by `route`. @@ -138,17 +149,19 @@ async def client_id( async def ping( self, message: Optional[str] = None, route: Optional[Route] = None ) -> str: - """Ping the Redis server. + """ + Ping the Redis server. See https://redis.io/commands/ping/ for more details. + Args: - message (Optional[str]): An optional message to include in the PING command. If not provided, - the server will respond with "PONG". If provided, the server will respond with a copy of the message. + message (Optional[str]): An optional message to include in the PING command. If not provided, + the server will respond with "PONG". If provided, the server will respond with a copy of the message route (Optional[Route]): The command will be sent to all primaries, unless `route` is provided, in which case the client will route the command to the nodes defined by `route` Returns: - str: "PONG" if 'message' is not provided, otherwise return a copy of 'message'. + str: "PONG" if `message` is not provided, otherwise return a copy of `message`. Examples: >>> await client.ping() @@ -162,7 +175,8 @@ async def ping( async def config_get( self, parameters: List[str], route: Optional[Route] = None ) -> TClusterResponse[Dict[str, str]]: - """Get the values of configuration parameters. + """ + Get the values of configuration parameters. See https://redis.io/commands/config-get/ for details. Args: @@ -191,7 +205,8 @@ async def config_get( async def config_set( self, parameters_map: Mapping[str, str], route: Optional[Route] = None ) -> TOK: - """Set configuration parameters to the specified values. + """ + Set configuration parameters to the specified values. See https://redis.io/commands/config-set/ for details. Args: @@ -238,7 +253,6 @@ async def client_getname( >>> await client.client_getname(AllNodes()) {'addr': 'Connection Name'', 'addr2': 'Connection Name', 'addr3': 'Connection Name'} """ - return cast( TClusterResponse[Optional[str]], await self._execute_command(RequestType.ClientGetName, [], route), diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 752bb5f17f..99df98c014 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -229,12 +229,14 @@ async def set( expiry: Optional[ExpirySet] = None, return_old_value: bool = False, ) -> Optional[str]: - """Set the given key with the given value. Return value is dependent on the passed options. + """ + Set the given key with the given value. Return value is dependent on the passed options. See https://redis.io/commands/set/ for details. @example - Set "foo" to "bar" only if "foo" already exists, and set the key expiration to 5 seconds: connection.set("foo", "bar", conditional_set=ConditionalChange.ONLY_IF_EXISTS, expiry=Expiry(ExpiryType.SEC, 5)) + Args: key (str): the key to store. value (str): the value to store with the given key. @@ -245,6 +247,7 @@ async def set( return_old_value (bool, optional): Return the old string stored at key, or None 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. Defaults to False. + Returns: Optional[str]: If the value is successfully set, return OK. @@ -263,11 +266,12 @@ async def set( ) async def get(self, key: str) -> Optional[str]: - """Get the value associated with the given key, or null if no such value exists. - See https://redis.io/commands/get/ for details. + """ + Get the value associated with the given key, or null if no such value exists. + See https://redis.io/commands/get/ for details. Args: - key (str): the key to retrieve from the database + key (str): The key to retrieve from the database. Returns: Optional[str]: If the key exists, returns the value of the key as a string. Otherwise, return None. @@ -277,7 +281,8 @@ async def get(self, key: str) -> Optional[str]: ) async def delete(self, keys: List[str]) -> int: - """Delete one or more keys from the database. A key is ignored if it does not exist. + """ + Delete one or more keys from the database. A key is ignored if it does not exist. See https://redis.io/commands/del/ for details. Args: @@ -289,48 +294,48 @@ async def delete(self, keys: List[str]) -> int: return cast(int, await self._execute_command(RequestType.Del, keys)) async def incr(self, key: str) -> int: - """Increments the number stored at `key` by one. If the key does not exist, it is set to 0 before performing the + """ + Increments the number stored at `key` by one. If the key does not exist, it is set to 0 before performing the operation. See https://redis.io/commands/incr/ for more details. Args: key (str): The key to increment it's value. - Returns: - int: The value of `key` after the increment. An error is returned if the key contains a value - of the wrong type or contains a string that can not be represented as integer. + Returns: + int: The value of `key` after the increment. """ return cast(int, await self._execute_command(RequestType.Incr, [key])) async def incrby(self, key: str, amount: int) -> int: - """Increments the number stored at `key` by `amount`. If the key does not exist, it is set to 0 before performing + """ + Increments the number stored at `key` by `amount`. If the key does not exist, it is set to 0 before performing the operation. See https://redis.io/commands/incrby/ for more details. Args: key (str): The key to increment it's value. amount (int) : The amount to increment. - Returns: - int: The value of key after the increment. An error is returned if the key contains a value - of the wrong type or contains a string that can not be represented as integer. + Returns: + int: The value of key after the increment. """ return cast( int, await self._execute_command(RequestType.IncrBy, [key, str(amount)]) ) async def incrbyfloat(self, key: str, amount: float) -> float: - """Increment the string representing a floating point number stored at `key` by `amount`. - By using a negative increment value, the value stored at the `key` is decremented. - If the key does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/incrbyfloat/ for more details. + """ + Increment the string representing a floating point number stored at `key` by `amount`. + By using a negative increment value, the value stored at the `key` is decremented. + If the key does not exist, it is set to 0 before performing the operation. + See https://redis.io/commands/incrbyfloat/ for more details. Args: key (str): The key to increment it's value. amount (float) : The amount to increment. - Returns: - float: The value of key after the increment. An error is returned if the key contains a value - of the wrong type. + Returns: + float: The value of key after the increment. """ return cast( float, @@ -338,7 +343,8 @@ async def incrbyfloat(self, key: str, amount: float) -> float: ) async def mset(self, key_value_map: Mapping[str, str]) -> TOK: - """Set multiple keys to multiple values in a single operation. + """ + Set multiple keys to multiple values in a single atomic operation. See https://redis.io/commands/mset/ for more details. Args: @@ -353,7 +359,8 @@ async def mset(self, key_value_map: Mapping[str, str]) -> TOK: return cast(TOK, await self._execute_command(RequestType.MSet, parameters)) async def mget(self, keys: List[str]) -> List[str]: - """Retrieve the values of multiple keys. + """ + Retrieve the values of multiple keys. See https://redis.io/commands/mget/ for more details. Args: @@ -366,21 +373,22 @@ async def mget(self, keys: List[str]) -> List[str]: return cast(List[str], await self._execute_command(RequestType.MGet, keys)) async def decr(self, key: str) -> int: - """Decrement the number stored at `key` by one. If the key does not exist, it is set to 0 before performing the + """ + Decrement the number stored at `key` by one. If the key does not exist, it is set to 0 before performing the operation. See https://redis.io/commands/decr/ for more details. Args: key (str): The key to increment it's value. - Returns: - int: The value of key after the decrement. An error is returned if the key contains a value - of the wrong type or contains a string that can not be represented as integer. + Returns: + int: The value of key after the decrement. """ return cast(int, await self._execute_command(RequestType.Decr, [key])) async def decrby(self, key: str, amount: int) -> int: - """Decrements the number stored at `key` by `amount`. If the key does not exist, it is set to 0 before performing + """ + Decrements the number stored at `key` by `amount`. If the key does not exist, it is set to 0 before performing the operation. See https://redis.io/commands/decrby/ for more details. @@ -388,16 +396,16 @@ async def decrby(self, key: str, amount: int) -> int: key (str): The key to decrement it's value. amount (int) : The amount to decrement. - Returns: - int: The value of key after the decrement. An error is returned if the key contains a value - of the wrong type or contains a string that can not be represented as integer. + Returns: + int: The value of key after the decrement. """ return cast( int, await self._execute_command(RequestType.DecrBy, [key, str(amount)]) ) async def hset(self, key: str, field_value_map: Mapping[str, str]) -> int: - """Sets the specified fields to their respective values in the hash stored at `key`. + """ + Sets the specified fields to their respective values in the hash stored at `key`. See https://redis.io/commands/hset/ for more details. Args: @@ -406,7 +414,7 @@ async def hset(self, key: str, field_value_map: Mapping[str, str]) -> int: to be set in the hash stored at the specified key. Returns: - int: The number of fields that were added or modified in the hash. + int: The number of fields that were added to the hash. Example: >>> hset("my_hash", {"field": "value", "field2": "value2"}) @@ -421,7 +429,8 @@ async def hset(self, key: str, field_value_map: Mapping[str, str]) -> int: ) async def hget(self, key: str, field: str) -> Optional[str]: - """Retrieves the value associated with field in the hash stored at `key`. + """ + Retrieves the value associated with `field` in the hash stored at `key`. See https://redis.io/commands/hget/ for more details. Args: @@ -429,8 +438,8 @@ async def hget(self, key: str, field: str) -> Optional[str]: field (str): The field whose value should be retrieved. Returns: - Optional[str]: The value associated with the specified field in the hash. - Returns None if the field or key does not exist. + Optional[str]: The value associated `field` in the hash. + Returns None if `field` is not presented in the hash or `key` does not exist. Examples: >>> hget("my_hash", "field") @@ -444,7 +453,8 @@ async def hget(self, key: str, field: str) -> Optional[str]: ) async def hincrby(self, key: str, field: str, amount: int) -> int: - """Increment or decrement the value of a `field` in the hash stored at `key` by the specified amount. + """ + Increment or decrement the value of a `field` in the hash stored at `key` by the specified amount. By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. If `field` or `key` does not exist, it is set to 0 before performing the operation. See https://redis.io/commands/hincrby/ for more details. @@ -457,13 +467,10 @@ async def hincrby(self, key: str, field: str, amount: int) -> int: Returns: int: The value of the specified field in the hash stored at `key` after the increment or decrement. - An error will be returned if `key` holds a value of an incorrect type (not a string) or if it contains a string - that cannot be represented as an integer. Examples: >>> await client.hincrby("my_hash", "field1", 5) 5 - """ return cast( int, @@ -473,7 +480,8 @@ async def hincrby(self, key: str, field: str, amount: int) -> int: ) async def hincrbyfloat(self, key: str, field: str, amount: float) -> float: - """Increment or decrement the floating-point value stored at `field` in the hash stored at `key` by the specified + """ + Increment or decrement the floating-point value stored at `field` in the hash stored at `key` by the specified amount. By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. If `field` or `key` does not exist, it is set to 0 before performing the operation. @@ -487,8 +495,6 @@ async def hincrbyfloat(self, key: str, field: str, amount: float) -> float: Returns: float: The value of the specified field in the hash stored at `key` after the increment as a string. - An error is returned if `key` contains a value of the wrong type or the current field content is not - parsable as a double precision floating point number. Examples: >>> await client.hincrbyfloat("my_hash", "field1", 2.5) @@ -502,7 +508,8 @@ async def hincrbyfloat(self, key: str, field: str, amount: float) -> float: ) async def hexists(self, key: str, field: str) -> bool: - """Check if a field exists in the hash stored at `key`. + """ + Check if a field exists in the hash stored at `key`. See https://redis.io/commands/hexists/ for more details. Args: @@ -512,20 +519,20 @@ async def hexists(self, key: str, field: str) -> bool: Returns: bool: Returns 'True' if the hash contains the specified field. If the hash does not contain the field, or if the key does not exist, it returns 'False'. - If `key` holds a value that is not a hash, an error is returned. Examples: >>> await client.hexists("my_hash", "field1") - 1 + True >>> await client.hexists("my_hash", "nonexistent_field") - 0 + False """ return cast( bool, await self._execute_command(RequestType.HashExists, [key, field]) ) async def hgetall(self, key: str) -> Dict[str, str]: - """Returns all fields and values of the hash stored at `key`. + """ + Returns all fields and values of the hash stored at `key`. See https://redis.io/commands/hgetall/ for details. Args: @@ -533,8 +540,8 @@ async def hgetall(self, key: str) -> Dict[str, str]: Returns: Dict[str, str]: A dictionary of fields and their values stored in the hash. Every field name in the list is followed by - its value. If `key` does not exist, it returns an empty dictionary. - If `key` holds a value that is not a hash, an error is returned. + its value. + If `key` does not exist, it returns an empty dictionary. Examples: >>> await client.hgetall("my_hash") @@ -545,7 +552,8 @@ async def hgetall(self, key: str) -> Dict[str, str]: ) async def hmget(self, key: str, fields: List[str]) -> List[Optional[str]]: - """Retrieve the values associated with specified fields in the hash stored at `key`. + """ + Retrieve the values associated with specified fields in the hash stored at `key`. See https://redis.io/commands/hmget/ for details. Args: @@ -555,8 +563,7 @@ async def hmget(self, key: str, fields: List[str]) -> List[Optional[str]]: Returns: List[Optional[str]]: A list of values associated with the given fields, in the same order as they are requested. For every field that does not exist in the hash, a null value is returned. - If the key does not exist, it is treated as an empty hash, and the function returns a list of null values. - If `key` holds a value that is not a hash, an error is returned. + If `key` does not exist, it is treated as an empty hash, and the function returns a list of null values. Examples: >>> await client.hmget("my_hash", ["field1", "field2"]) @@ -568,7 +575,8 @@ async def hmget(self, key: str, fields: List[str]) -> List[Optional[str]]: ) async def hdel(self, key: str, fields: List[str]) -> int: - """Remove specified fields from the hash stored at `key`. + """ + Remove specified fields from the hash stored at `key`. See https://redis.io/commands/hdel/ for more details. Args: @@ -577,9 +585,7 @@ async def hdel(self, key: str, fields: List[str]) -> int: Returns: int: The number of fields that were removed from the hash, excluding specified but non-existing fields. - If the key does not exist, it is treated as an empty hash, and the function returns 0. - If `key` holds a value that is not a hash, an error is returned. - + If `key` does not exist, it is treated as an empty hash, and the function returns 0. Examples: >>> await client.hdel("my_hash", ["field1", "field2"]) @@ -611,7 +617,8 @@ async def hlen(self, key: str) -> int: return cast(int, await self._execute_command(RequestType.HLen, [key])) async def lpush(self, key: str, elements: List[str]) -> int: - """Insert all the specified values at the head of the list stored at `key`. + """ + Insert all the specified values at the head of the list stored at `key`. `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. If `key` does not exist, it is created as empty list before performing the push operations. See https://redis.io/commands/lpush/ for more details. @@ -622,7 +629,6 @@ async def lpush(self, key: str, elements: List[str]) -> int: Returns: int: The length of the list after the push operations. - If `key` holds a value that is not a list, an error is returned. Examples: >>> await client.lpush("my_list", ["value1", "value2"]) @@ -635,7 +641,8 @@ async def lpush(self, key: str, elements: List[str]) -> int: ) async def lpop(self, key: str) -> Optional[str]: - """Remove and return the first elements of the list stored at `key`. + """ + Remove and return the first elements of the list stored at `key`. The command pops a single element from the beginning of the list. See https://redis.io/commands/lpop/ for details. @@ -645,7 +652,6 @@ async def lpop(self, key: str) -> Optional[str]: Returns: Optional[str]: The value of the first element. If `key` does not exist, None will be returned. - If `key` holds a value that is not a list, an error is returned. Examples: >>> await client.lpop("my_list") @@ -653,14 +659,14 @@ async def lpop(self, key: str) -> Optional[str]: >>> await client.lpop("non_exiting_key") None """ - return cast( Optional[str], await self._execute_command(RequestType.LPop, [key]), ) async def lpop_count(self, key: str, count: int) -> Optional[List[str]]: - """Remove and return up to `count` elements from the list stored at `key`, depending on the list's length. + """ + Remove and return up to `count` elements from the list stored at `key`, depending on the list's length. See https://redis.io/commands/lpop/ for details. Args: @@ -670,23 +676,21 @@ async def lpop_count(self, key: str, count: int) -> Optional[List[str]]: Returns: Optional[List[str]]: A a list of popped elements will be returned depending on the list's length. If `key` does not exist, None will be returned. - If `key` holds a value that is not a list, an error is returned. Examples: - >>> await client.lpop("my_list", 2) ["value1", "value2"] >>> await client.lpop("non_exiting_key" , 3) None """ - return cast( Optional[List[str]], await self._execute_command(RequestType.LPop, [key, str(count)]), ) async def lrange(self, key: str, start: int, end: int) -> List[str]: - """Retrieve the specified elements of the list stored at `key` within the given range. + """ + Retrieve the specified elements of the list stored at `key` within the given range. The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next element and so on. These offsets can also be negative numbers indicating offsets starting at the end of the list, with -1 being the last element of the list, -2 being the penultimate, and so on. @@ -702,7 +706,6 @@ async def lrange(self, key: str, start: int, end: int) -> List[str]: If `start` exceeds the `end` of the list, or if `start` is greater than `end`, an empty list will be returned. If `end` exceeds the actual end of the list, the range will stop at the actual end of the list. If `key` does not exist an empty list will be returned. - If `key` holds a value that is not a list, an error is returned. Examples: >>> await client.lrange("my_list", 0, 2) @@ -712,7 +715,6 @@ async def lrange(self, key: str, start: int, end: int) -> List[str]: >>> await client.lrange("non_exiting_key", 0, 2) [] """ - return cast( List[str], await self._execute_command( @@ -721,7 +723,8 @@ async def lrange(self, key: str, start: int, end: int) -> List[str]: ) async def rpush(self, key: str, elements: List[str]) -> int: - """Inserts all the specified values at the tail of the list stored at `key`. + """ + Inserts all the specified values at the tail of the list stored at `key`. `elements` are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. If `key` does not exist, it is created as empty list before performing the push operations. See https://redis.io/commands/rpush/ for more details. @@ -732,7 +735,6 @@ async def rpush(self, key: str, elements: List[str]) -> int: Returns: int: The length of the list after the push operations. - If `key` holds a value that is not a list, an error is returned. Examples: >>> await client.rpush("my_list", ["value1", "value2"]) @@ -745,7 +747,8 @@ async def rpush(self, key: str, elements: List[str]) -> int: ) async def rpop(self, key: str, count: Optional[int] = None) -> Optional[str]: - """Removes and returns the last elements of the list stored at `key`. + """ + Removes and returns the last elements of the list stored at `key`. The command pops a single element from the end of the list. See https://redis.io/commands/rpop/ for details. @@ -755,7 +758,6 @@ async def rpop(self, key: str, count: Optional[int] = None) -> Optional[str]: Returns: Optional[str]: The value of the last element. If `key` does not exist, None will be returned. - If `key` holds a value that is not a list, an error is returned. Examples: >>> await client.rpop("my_list") @@ -763,14 +765,14 @@ async def rpop(self, key: str, count: Optional[int] = None) -> Optional[str]: >>> await client.rpop("non_exiting_key") None """ - return cast( Optional[str], await self._execute_command(RequestType.RPop, [key]), ) async def rpop_count(self, key: str, count: int) -> Optional[List[str]]: - """Removes and returns up to `count` elements from the list stored at `key`, depending on the list's length. + """ + Removes and returns up to `count` elements from the list stored at `key`, depending on the list's length. See https://redis.io/commands/rpop/ for details. Args: @@ -780,7 +782,6 @@ async def rpop_count(self, key: str, count: int) -> Optional[List[str]]: Returns: Optional[List[str]: A list of popped elements will be returned depending on the list's length. If `key` does not exist, None will be returned. - If `key` holds a value that is not a list, an error is returned. Examples: >>> await client.rpop("my_list", 2) @@ -788,14 +789,14 @@ async def rpop_count(self, key: str, count: int) -> Optional[List[str]]: >>> await client.rpop("non_exiting_key" , 7) None """ - return cast( Optional[List[str]], await self._execute_command(RequestType.RPop, [key, str(count)]), ) async def sadd(self, key: str, members: List[str]) -> int: - """Add specified members to the set stored at `key`. + """ + Add specified members to the set stored at `key`. Specified members that are already a member of this set are ignored. If `key` does not exist, a new set is created before adding `members`. See https://redis.io/commands/sadd/ for more details. @@ -806,7 +807,6 @@ async def sadd(self, key: str, members: List[str]) -> int: Returns: int: The number of members that were added to the set, excluding members already present. - If `key` holds a value that is not a set, an error is returned. Examples: >>> await client.sadd("my_set", ["member1", "member2"]) @@ -815,7 +815,8 @@ async def sadd(self, key: str, members: List[str]) -> int: return cast(int, await self._execute_command(RequestType.SAdd, [key] + members)) async def srem(self, key: str, members: List[str]) -> int: - """Remove specified members from the set stored at `key`. + """ + Remove specified members from the set stored at `key`. Specified members that are not a member of this set are ignored. See https://redis.io/commands/srem/ for details. @@ -826,7 +827,6 @@ async def srem(self, key: str, members: List[str]) -> int: Returns: int: The number of members that were removed from the set, excluding non-existing members. If `key` does not exist, it is treated as an empty set and this command returns 0. - If `key` holds a value that is not a set, an error is returned. Examples: >>> await client.srem("my_set", ["member1", "member2"]) @@ -835,7 +835,8 @@ async def srem(self, key: str, members: List[str]) -> int: return cast(int, await self._execute_command(RequestType.SRem, [key] + members)) async def smembers(self, key: str) -> Set[str]: - """Retrieve all the members of the set value stored at `key`. + """ + Retrieve all the members of the set value stored at `key`. See https://redis.io/commands/smembers/ for details. Args: @@ -844,7 +845,6 @@ async def smembers(self, key: str) -> Set[str]: Returns: Set[str]: A set of all members of the set. If `key` does not exist an empty list will be returned. - If `key` holds a value that is not a set, an error is returned. Examples: >>> await client.smembers("my_set") @@ -853,7 +853,8 @@ async def smembers(self, key: str) -> Set[str]: return cast(Set[str], await self._execute_command(RequestType.SMembers, [key])) async def scard(self, key: str) -> int: - """Retrieve the set cardinality (number of elements) of the set stored at `key`. + """ + Retrieve the set cardinality (number of elements) of the set stored at `key`. See https://redis.io/commands/scard/ for details. Args: @@ -861,7 +862,6 @@ async def scard(self, key: str) -> int: Returns: int: The cardinality (number of elements) of the set, or 0 if the key does not exist. - If `key` holds a value that is not a set, an error is returned. Examples: >>> await client.scard("my_set") @@ -870,7 +870,8 @@ async def scard(self, key: str) -> int: return cast(int, await self._execute_command(RequestType.SCard, [key])) async def ltrim(self, key: str, start: int, end: int) -> TOK: - """Trim an existing list so that it will contain only the specified range of elements specified. + """ + Trim an existing list so that it will contain only the specified range of elements specified. The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next element and so on. These offsets can also be negative numbers indicating offsets starting at the end of the list, with -1 being the last @@ -888,7 +889,6 @@ async def ltrim(self, key: str, start: int, end: int) -> TOK: (which causes `key` to be removed). If `end` exceeds the actual end of the list, it will be treated like the last element of the list. If `key` does not exist, "OK" will be returned without changes to the database. - If `key` holds a value that is not a list, an error is returned. Examples: >>> await client.ltrim("my_list", 0, 1) @@ -900,7 +900,8 @@ async def ltrim(self, key: str, start: int, end: int) -> TOK: ) async def lrem(self, key: str, count: int, element: str) -> int: - """Removes the first `count` occurrences of elements equal to `element` from the list stored at `key`. + """ + Removes the first `count` occurrences of elements equal to `element` from the list stored at `key`. If `count` is positive, it removes elements equal to `element` moving from head to tail. If `count` is negative, it removes elements equal to `element` moving from tail to head. If `count` is 0 or greater than the occurrences of elements equal to `element`, it removes all elements @@ -915,7 +916,6 @@ async def lrem(self, key: str, count: int, element: str) -> int: Returns: int: The number of removed elements. If `key` does not exist, 0 is returned. - If `key` holds a value that is not a list, an error is returned. Examples: >>> await client.lrem("my_list", 2, "value") @@ -927,7 +927,8 @@ async def lrem(self, key: str, count: int, element: str) -> int: ) async def llen(self, key: str) -> int: - """Get the length of the list stored at `key`. + """ + Get the length of the list stored at `key`. See https://redis.io/commands/llen/ for details. Args: @@ -936,7 +937,6 @@ async def llen(self, key: str) -> int: Returns: int: The length of the list at the specified key. If `key` does not exist, it is interpreted as an empty list and 0 is returned. - If `key` holds a value that is not a list, an error is returned. Examples: >>> await client.llen("my_list") @@ -945,7 +945,8 @@ async def llen(self, key: str) -> int: return cast(int, await self._execute_command(RequestType.LLen, [key])) async def exists(self, keys: List[str]) -> int: - """Returns the number of keys in `keys` that exist in the database. + """ + Returns the number of keys in `keys` that exist in the database. See https://redis.io/commands/exists/ for more details. Args: @@ -962,7 +963,8 @@ async def exists(self, keys: List[str]) -> int: return cast(int, await self._execute_command(RequestType.Exists, keys)) async def unlink(self, keys: List[str]) -> int: - """Unlink (delete) multiple keys from the database. + """ + Unlink (delete) multiple keys from the database. A key is ignored if it does not exist. This command, similar to DEL, removes specified keys and ignores non-existent ones. However, this command does not block the server, while [DEL](https://redis.io/commands/del) does. @@ -1169,7 +1171,6 @@ async def zadd( Returns: int: The number of elements added to the sorted set. If `changed` is set, returns the number of elements updated in the sorted set. - If `key` holds a value that is not a sorted set, an error is returned. Examples: >>> await zadd("my_sorted_set", {"member1": 10.5, "member2": 8.2}) @@ -1233,7 +1234,6 @@ async def zadd_incr( Returns: Optional[float]: The score of the member. If there was a conflict with choosing the XX/NX/LT/GT options, the operation aborts and None is returned. - If `key` holds a value that is not a sorted set, an error is returned. Examples: >>> await zaddIncr("my_sorted_set", member , 5.0) @@ -1275,7 +1275,6 @@ async def zcard(self, key: str) -> int: Returns: int: The number of elements in the sorted set. If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. - If `key` holds a value that is not a sorted set, an error is returned. Examples: >>> await zcard("my_sorted_set") @@ -1309,7 +1308,6 @@ async def zcount( int: The number of members in the specified score range. If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. If `max_score` < `min_score`, 0 is returned. - If `key` holds a value that is not a sorted set, an error is returned. Examples: >>> await zcount("my_sorted_set", ScoreLimit(5.0 , is_inclusive=true) , InfBound.POS_INF) @@ -1342,7 +1340,6 @@ async def zrem( Returns: int: The number of members that were removed from the sorted set, not including non-existing members. If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. - If `key` holds a value that is not a sorted set, an error is returned. Examples: >>> await zrem("my_sorted_set", ["member1", "member2"]) @@ -1369,7 +1366,6 @@ async def zscore(self, key: str, member: str) -> Optional[float]: Optional[float]: The score of the member. If `member` does not exist in the sorted set, None is returned. If `key` does not exist, None is returned. - If `key` holds a value that is not a sorted set, an error is returned. Examples: >>> await zscore("my_sorted_set", "member") diff --git a/python/python/glide/async_commands/standalone_commands.py b/python/python/glide/async_commands/standalone_commands.py index 77c731e1cc..eea355406f 100644 --- a/python/python/glide/async_commands/standalone_commands.py +++ b/python/python/glide/async_commands/standalone_commands.py @@ -12,7 +12,8 @@ class StandaloneCommands(CoreCommands): async def custom_command(self, command_args: List[str]) -> TResult: - """Executes a single command, without checking inputs. + """ + Executes a single command, without checking inputs. @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 - Return a list of all pub/sub clients: @@ -30,7 +31,8 @@ async def info( self, sections: Optional[List[InfoSection]] = None, ) -> str: - """Get information and statistics about the Redis server. + """ + Get information and statistics about the Redis server. See https://redis.io/commands/info/ for details. Args: @@ -48,7 +50,8 @@ async def exec( self, transaction: BaseTransaction | Transaction, ) -> Optional[List[TResult]]: - """Execute a transaction by processing the queued commands. + """ + Execute a transaction by processing the queued commands. See https://redis.io/topics/Transactions/ for details on Redis Transactions. Args: @@ -64,7 +67,8 @@ async def exec( return await self._execute_transaction(commands) async def select(self, index: int) -> TOK: - """Change the currently selected Redis database. + """ + Change the currently selected Redis database. See https://redis.io/commands/select/ for details. Args: @@ -76,26 +80,30 @@ async def select(self, index: int) -> TOK: return cast(TOK, await self._execute_command(RequestType.Select, [str(index)])) async def config_resetstat(self) -> TOK: - """Reset the statistics reported by Redis. + """ + Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. See https://redis.io/commands/config-resetstat/ for details. + Returns: OK: Returns "OK" to confirm that the statistics were successfully reset. """ return cast(TOK, await self._execute_command(RequestType.ConfigResetStat, [])) async def config_rewrite(self) -> TOK: - """Rewrite the configuration file with the current configuration. + """ + Rewrite the configuration file with the current configuration. See https://redis.io/commands/config-rewrite/ for details. Returns: - OK: OK is returned when the configuration was rewritten properly. Otherwise an error is returned. + OK: OK is returned when the configuration was rewritten properly. Otherwise, an error is raised. """ return cast(TOK, await self._execute_command(RequestType.ConfigRewrite, [])) async def client_id( self, ) -> int: - """Returns the current connection id. + """ + Returns the current connection id. See https://redis.io/commands/client-id/ for more information. Returns: @@ -104,26 +112,29 @@ async def client_id( return cast(int, await self._execute_command(RequestType.ClientId, [])) async def ping(self, message: Optional[str] = None) -> str: - """Ping the Redis server. + """ + Ping the Redis server. See https://redis.io/commands/ping/ for more details. + Args: message (Optional[str]): An optional message to include in the PING command. If not provided, the server will respond with "PONG". If provided, the server will respond with a copy of the message. Returns: - str: "PONG" if 'message' is not provided, otherwise return a copy of 'message'. + str: "PONG" if `message` is not provided, otherwise return a copy of `message`. Examples: - >>> ping() + >>> await client.ping() "PONG" - >>> ping("Hello") + >>> await client.ping("Hello") "Hello" """ argument = [] if message is None else [message] return cast(str, await self._execute_command(RequestType.Ping, argument)) async def config_get(self, parameters: List[str]) -> Dict[str, str]: - """Get the values of configuration parameters. + """ + Get the values of configuration parameters. See https://redis.io/commands/config-get/ for details. Args: @@ -145,7 +156,8 @@ async def config_get(self, parameters: List[str]) -> Dict[str, str]: ) async def config_set(self, parameters_map: Mapping[str, str]) -> TOK: - """Set configuration parameters to the specified values. + """ + Set configuration parameters to the specified values. See https://redis.io/commands/config-set/ for details. Args: @@ -174,7 +186,7 @@ async def client_getname(self) -> Optional[str]: or None if no name is assigned. Examples: - >>> client_getname() + >>> await client.client_getname() 'Connection Name' """ return cast( diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index d7b9b5262d..9a4f5b101e 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -27,7 +27,7 @@ class BaseTransaction: transaction = BaseTransaction() >>> transaction.set("key", "value") >>> transaction.get("key") - >>> client.exec(transaction) + >>> await client.exec(transaction) [OK , "value"] """ @@ -47,6 +47,16 @@ def clear(self): self.commands.clear() def get(self, key: str): + """ + Get the value associated with the given key, or null if no such value exists. + See https://redis.io/commands/get/ for details. + + Args: + key (str): The key to retrieve from the database. + + Command response: + Optional[str]: If the key exists, returns the value of the key as a string. Otherwise, return None. + """ self.append_command(RequestType.GetString, [key]) def set( @@ -57,6 +67,31 @@ def set( expiry: Union[ExpirySet, None] = None, return_old_value: bool = False, ): + """ + Set the given key with the given value. Return value is dependent on the passed options. + See https://redis.io/commands/set/ for details. + + @example - Set "foo" to "bar" only if "foo" already exists, and set the key expiration to 5 seconds: + + connection.set("foo", "bar", conditional_set=ConditionalChange.ONLY_IF_EXISTS, expiry=Expiry(ExpiryType.SEC, 5)) + + Args: + key (str): the key to store. + value (str): the value to store with the given key. + conditional_set (Optional[ConditionalChange], optional): set the key only if the given condition is met. + Equivalent to [`XX` | `NX`] in the Redis API. Defaults to None. + expiry (Optional[ExpirySet], optional): set expiriation to the given key. + Equivalent to [`EX` | `PX` | `EXAT` | `PXAT` | `KEEPTTL`] in the Redis API. Defaults to None. + return_old_value (bool, optional): Return the old string stored at key, or None 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. Defaults to False. + + Command response: + Optional[str]: + If the value is successfully set, return OK. + If value isn't set because of only_if_exists or only_if_does_not_exist conditions, return None. + If return_old_value is set, return the old value as a string. + """ args = [key, value] if conditional_set: if conditional_set == ConditionalChange.ONLY_IF_EXISTS: @@ -70,17 +105,19 @@ def set( self.append_command(RequestType.SetString, args) def custom_command(self, command_args: List[str]): - """Executes a single command, without checking inputs. + """ + Executes a single command, without checking inputs. @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 - Append a command to list of all pub/sub clients: transaction.customCommand(["CLIENT", "LIST","TYPE", "PUBSUB"]) + Args: command_args (List[str]): List of strings of the command's arguments. Every part of the command, including the command name and subcommands, should be added as a separate value in args. Command response: - TResult: The returning value depends on the executed command + TResult: The returning value depends on the executed command. """ self.append_command(RequestType.CustomCommand, command_args) @@ -88,11 +125,14 @@ def info( self, sections: Optional[List[InfoSection]] = None, ): - """Get information and statistics about the Redis server. + """ + Get information and statistics about the Redis server. See https://redis.io/commands/info/ for details. + Args: sections (Optional[List[InfoSection]]): A list of InfoSection values specifying which sections of information to retrieve. When no parameter is provided, the default option is assumed. + Command response: str: Returns a string containing the information for the sections requested. """ @@ -100,7 +140,8 @@ def info( self.append_command(RequestType.Info, args) def delete(self, keys: List[str]): - """Delete one or more keys from the database. A key is ignored if it does not exist. + """ + Delete one or more keys from the database. A key is ignored if it does not exist. See https://redis.io/commands/del/ for details. Args: @@ -112,7 +153,8 @@ def delete(self, keys: List[str]): self.append_command(RequestType.Del, keys) def config_get(self, parameters: List[str]): - """Get the values of configuration parameters. + """ + Get the values of configuration parameters. See https://redis.io/commands/config-get/ for details. Args: @@ -124,14 +166,16 @@ def config_get(self, parameters: List[str]): self.append_command(RequestType.ConfigGet, parameters) def config_set(self, parameters_map: Mapping[str, str]): - """Set configuration parameters to the specified values. + """ + Set configuration parameters to the specified values. See https://redis.io/commands/config-set/ for details. Args: parameters_map (Mapping[str, str]): A map consisting of configuration parameters and their respective values to set. + Command response: - OK: Returns OK if all configurations have been successfully set. Otherwise, raises an error. + OK: Returns OK if all configurations have been successfully set. Otherwise, the transaction fails with an error. """ parameters: List[str] = [] for pair in parameters_map.items(): @@ -139,15 +183,18 @@ def config_set(self, parameters_map: Mapping[str, str]): self.append_command(RequestType.ConfigSet, parameters) def config_resetstat(self): - """Reset the statistics reported by Redis. + """ + Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. See https://redis.io/commands/config-resetstat/ for details. + Command response: - OK: Returns "OK" to confirm that the statistics were successfully reset. + OK: a simple OK response. """ self.append_command(RequestType.ConfigResetStat, []) def mset(self, key_value_map: Mapping[str, str]): - """Set multiple keys to multiple values in a single operation. + """ + Set multiple keys to multiple values in a single atomic operation. See https://redis.io/commands/mset/ for more details. Args: @@ -162,7 +209,8 @@ def mset(self, key_value_map: Mapping[str, str]): self.append_command(RequestType.MSet, parameters) def mget(self, keys: List[str]): - """Retrieve the values of multiple keys. + """ + Retrieve the values of multiple keys. See https://redis.io/commands/mget/ for more details. Args: @@ -175,26 +223,29 @@ def mget(self, keys: List[str]): self.append_command(RequestType.MGet, keys) def config_rewrite(self): - """Rewrite the configuration file with the current configuration. + """ + Rewrite the configuration file with the current configuration. See https://redis.io/commands/config-rewrite/ for details. Command response: - OK: OK is returned when the configuration was rewritten properly. Otherwise an error is returned. + OK: OK is returned when the configuration was rewritten properly. Otherwise, the transaction fails with an error. """ self.append_command(RequestType.ConfigRewrite, []) def client_id(self): - """Returns the current connection id. + """ + Returns the current connection id. See https://redis.io/commands/client-id/ for more information. - Command response: int: the id of the client. """ self.append_command(RequestType.ClientId, []) def incr(self, key: str): - """Increments the number stored at `key` by one. If the key does not exist, it is set to 0 before performing the + """ + Increments the number stored at `key` by one. + If `key` does not exist, it is set to 0 before performing the operation. See https://redis.io/commands/incr/ for more details. @@ -202,13 +253,13 @@ def incr(self, key: str): key (str): The key to increment it's value. Command response: - int: the value of `key` after the increment. An error is returned if the key contains a value - of the wrong type or contains a string that can not be represented as integer. + int: the value of `key` after the increment. """ self.append_command(RequestType.Incr, [key]) def incrby(self, key: str, amount: int): - """Increments the number stored at `key` by `amount`. If the key does not exist, it is set to 0 before performing + """ + Increments the number stored at `key` by `amount`. If the key does not exist, it is set to 0 before performing the operation. See https://redis.io/commands/incrby/ for more details. @@ -217,42 +268,44 @@ def incrby(self, key: str, amount: int): amount (int) : The amount to increment. Command response: - int: The value of `key` after the increment. An error is returned if the key contains a value - of the wrong type or contains a string that can not be represented as integer. + int: The value of `key` after the increment. """ self.append_command(RequestType.IncrBy, [key, str(amount)]) def incrbyfloat(self, key: str, amount: float): - """Increment the string representing a floating point number stored at `key` by `amount`. - By using a negative increment value, the value stored at the `key` is decremented. - If the key does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/incrbyfloat/ for more details. + """ + Increment the string representing a floating point number stored at `key` by `amount`. + By using a negative increment value, the value stored at the `key` is decremented. + If the key does not exist, it is set to 0 before performing the operation. + See https://redis.io/commands/incrbyfloat/ for more details. Args: key (str): The key to increment it's value. amount (float) : The amount to increment. Command response: - float: The value of key after the increment. The transaction fails if the key contains a value - of the wrong type. + float: The value of key after the increment. """ self.append_command(RequestType.IncrByFloat, [key, str(amount)]) def ping(self, message: Optional[str] = None): - """Ping the Redis server. + """ + Ping the Redis server. See https://redis.io/commands/ping/ for more details. + Args: message (Optional[str]): An optional message to include in the PING command. If not provided, the server will respond with "PONG". If provided, the server will respond with a copy of the message. Command response: - str: "PONG" if 'message' is not provided, otherwise return a copy of 'message'. + str: "PONG" if `message` is not provided, otherwise return a copy of `message`. """ argument = [] if message is None else [message] self.append_command(RequestType.Ping, argument) def decr(self, key: str): - """Decrements the number stored at `key` by one. If the key does not exist, it is set to 0 before performing the + """ + Decrements the number stored at `key` by one. If the key does not exist, it is set to 0 before performing the operation. See https://redis.io/commands/decr/ for more details. @@ -260,13 +313,13 @@ def decr(self, key: str): key (str): The key to decrement it's value. Command response: - int: the value of `key` after the decrement. An error is returned if the key contains a value - of the wrong type or contains a string that can not be represented as integer. + int: the value of `key` after the decrement. """ self.append_command(RequestType.Decr, [key]) def decrby(self, key: str, amount: int): - """Decrements the number stored at `key` by `amount`. If the key does not exist, it is set to 0 before performing + """ + Decrements the number stored at `key` by `amount`. If the key does not exist, it is set to 0 before performing the operation. See https://redis.io/commands/decrby/ for more details. @@ -275,13 +328,13 @@ def decrby(self, key: str, amount: int): amount (int) : The amount to decrement. Command response: - int: The value of `key` after the decrement. An error is returned if the key contains a value - of the wrong type or contains a string that can not be represented as integer. + int: The value of `key` after the decrement. """ self.append_command(RequestType.DecrBy, [key, str(amount)]) def hset(self, key: str, field_value_map: Mapping[str, str]): - """Sets the specified fields to their respective values in the hash stored at `key`. + """ + Sets the specified fields to their respective values in the hash stored at `key`. See https://redis.io/commands/hset/ for more details. Args: @@ -290,7 +343,7 @@ def hset(self, key: str, field_value_map: Mapping[str, str]): to be set in the hash stored at the specified key. Command response: - int: The number of fields that were added or modified in the hash. + int: The number of fields that were added to the hash. """ field_value_list: List[str] = [key] for pair in field_value_map.items(): @@ -298,7 +351,8 @@ def hset(self, key: str, field_value_map: Mapping[str, str]): self.append_command(RequestType.HashSet, field_value_list) def hget(self, key: str, field: str): - """Retrieves the value associated with field in the hash stored at `key`. + """ + Retrieves the value associated with `field` in the hash stored at `key`. See https://redis.io/commands/hget/ for more details. Args: @@ -306,13 +360,14 @@ def hget(self, key: str, field: str): field (str): The field whose value should be retrieved. Command response: - Optional[str]: The value associated with the specified field in the hash. - Returns None if the field or key does not exist. + Optional[str]: The value associated `field` in the hash. + Returns None if `field` is not presented in the hash or `key` does not exist. """ self.append_command(RequestType.HashGet, [key, field]) def hincrby(self, key: str, field: str, amount: int): - """Increment or decrement the value of a `field` in the hash stored at `key` by `amount`. + """ + Increment or decrement the value of a `field` in the hash stored at `key` by the specified amount. By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. If `field` or `key` does not exist, it is set to 0 before performing the operation. See https://redis.io/commands/hincrby/ for more details. @@ -325,14 +380,12 @@ def hincrby(self, key: str, field: str, amount: int): Command response: int: The value of the specified field in the hash stored at `key` after the increment or decrement. - The transaction fails if `key` holds a value of an incorrect type (not a string) or if it contains a string - that cannot be represented as an integer. - """ self.append_command(RequestType.HashIncrBy, [key, field, str(amount)]) def hincrbyfloat(self, key: str, field: str, amount: float): - """Increment or decrement the floating-point value stored at `field` in the hash stored at `key` by the specified + """ + Increment or decrement the floating-point value stored at `field` in the hash stored at `key` by the specified amount. By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. If `field` or `key` does not exist, it is set to 0 before performing the operation. @@ -344,15 +397,14 @@ def hincrbyfloat(self, key: str, field: str, amount: float): amount (float): The amount by which to increment or decrement the field's value. Use a negative value to decrement. - Returns: + Command response: float: The value of the specified field in the hash stored at `key` after the increment as a string. - The transaction fails if `key` contains a value of the wrong type or the current field content is not - parsable as a double precision floating point number. """ self.append_command(RequestType.HashIncrByFloat, [key, field, str(amount)]) def hexists(self, key: str, field: str): - """Check if a field exists in the hash stored at `key`. + """ + Check if a field exists in the hash stored at `key`. See https://redis.io/commands/hexists/ for more details. Args: @@ -362,7 +414,6 @@ def hexists(self, key: str, field: str): Command response: bool: Returns 'True' if the hash contains the specified field. If the hash does not contain the field, or if the key does not exist, it returns 'False'. - If `key` holds a value that is not a hash, the transaction fails with an error. """ self.append_command(RequestType.HashExists, [key, field]) @@ -393,7 +444,8 @@ def client_getname(self): self.append_command(RequestType.ClientGetName, []) def hgetall(self, key: str): - """Returns all fields and values of the hash stored at `key`. + """ + Returns all fields and values of the hash stored at `key`. See https://redis.io/commands/hgetall/ for details. Args: @@ -401,44 +453,45 @@ def hgetall(self, key: str): Command response: Dict[str, str]: A dictionary of fields and their values stored in the hash. Every field name in the list is followed by - its value. If `key` does not exist, it returns an empty dictionary. - If `key` holds a value that is not a hash, the transaction fails with an error. + its value. + If `key` does not exist, it returns an empty dictionary. """ self.append_command(RequestType.HashGetAll, [key]), def hmget(self, key: str, fields: List[str]): - """Retrieve the values associated with specified fields in the hash stored at `key`. + """ + Retrieve the values associated with specified fields in the hash stored at `key`. See https://redis.io/commands/hmget/ for details. Args: key (str): The key of the hash. fields (List[str]): The list of fields in the hash stored at `key` to retrieve from the database. - Command response: + Returns: List[Optional[str]]: A list of values associated with the given fields, in the same order as they are requested. For every field that does not exist in the hash, a null value is returned. - If the key does not exist, it is treated as an empty hash, and the function returns a list of null values. - If `key` holds a value that is not a hash, the transaction fails. + If `key` does not exist, it is treated as an empty hash, and the function returns a list of null values. """ self.append_command(RequestType.HashMGet, [key] + fields) def hdel(self, key: str, fields: List[str]): - """Remove specified fields from the hash stored at `key`. + """ + Remove specified fields from the hash stored at `key`. See https://redis.io/commands/hdel/ for more details. Args: key (str): The key of the hash. fields (List[str]): The list of fields to remove from the hash stored at `key`. - Command response: + Returns: int: The number of fields that were removed from the hash, excluding specified but non-existing fields. - If the key does not exist, it is treated as an empty hash, returns 0. - If `key` holds a value that is not a hash , the transaction fails. + If `key` does not exist, it is treated as an empty hash, and the function returns 0. """ self.append_command(RequestType.HashDel, [key] + fields) def lpush(self, key: str, elements: List[str]): - """Insert all the specified values at the head of the list stored at `key`. + """ + Insert all the specified values at the head of the list stored at `key`. `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. If `key` does not exist, it is created as empty list before performing the push operations. See https://redis.io/commands/lpush/ for more details. @@ -449,12 +502,12 @@ def lpush(self, key: str, elements: List[str]): Command response: int: The length of the list after the push operations. - If `key` holds a value that is not a list, the transaction fails. """ self.append_command(RequestType.LPush, [key] + elements) def lpop(self, key: str): - """Remove and return the first elements of the list stored at `key`. + """ + Remove and return the first elements of the list stored at `key`. The command pops a single element from the beginning of the list. See https://redis.io/commands/lpop/ for details. @@ -464,13 +517,12 @@ def lpop(self, key: str): Command response: Optional[str]: The value of the first element. If `key` does not exist, None will be returned. - If `key` holds a value that is not a list, the transaction fails with an error """ - self.append_command(RequestType.LPop, [key]) def lpop_count(self, key: str, count: int): - """Remove and return up to `count` elements from the list stored at `key`, depending on the list's length. + """ + Remove and return up to `count` elements from the list stored at `key`, depending on the list's length. See https://redis.io/commands/lpop/ for details. Args: @@ -480,13 +532,12 @@ def lpop_count(self, key: str, count: int): Command response: Optional[List[str]]: A a list of popped elements will be returned depending on the list's length. If `key` does not exist, None will be returned. - If `key` holds a value that is not a list, the transaction fails with an error """ - self.append_command(RequestType.LPop, [key, str(count)]) def lrange(self, key: str, start: int, end: int): - """Retrieve the specified elements of the list stored at `key` within the given range. + """ + Retrieve the specified elements of the list stored at `key` within the given range. The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next element and so on. These offsets can also be negative numbers indicating offsets starting at the end of the list, with -1 being the last element of the list, -2 being the penultimate, and so on. @@ -502,9 +553,7 @@ def lrange(self, key: str, start: int, end: int): If `start` exceeds the `end` of the list, or if `start` is greater than `end`, an empty list will be returned. If `end` exceeds the actual end of the list, the range will stop at the actual end of the list. If `key` does not exist an empty list will be returned. - If `key` holds a value that is not a list, the transaction fails. """ - self.append_command(RequestType.LRange, [key, str(start), str(end)]) def rpush(self, key: str, elements: List[str]): @@ -524,7 +573,8 @@ def rpush(self, key: str, elements: List[str]): self.append_command(RequestType.RPush, [key] + elements) def rpop(self, key: str, count: Optional[int] = None): - """Removes and returns the last elements of the list stored at `key`. + """ + Removes and returns the last elements of the list stored at `key`. The command pops a single element from the end of the list. See https://redis.io/commands/rpop/ for details. @@ -534,13 +584,12 @@ def rpop(self, key: str, count: Optional[int] = None): Commands response: Optional[str]: The value of the last element. If `key` does not exist, None will be returned. - If `key` holds a value that is not a list, the transaction fails with an error. """ - self.append_command(RequestType.RPop, [key]) def rpop_count(self, key: str, count: int): - """Removes and returns up to `count` elements from the list stored at `key`, depending on the list's length. + """ + Removes and returns up to `count` elements from the list stored at `key`, depending on the list's length. See https://redis.io/commands/rpop/ for details. Args: @@ -550,13 +599,12 @@ def rpop_count(self, key: str, count: int): Commands response: Optional[List[str]: A list of popped elements will be returned depending on the list's length. If `key` does not exist, None will be returned. - If `key` holds a value that is not a list, the transaction fails with an error. """ - self.append_command(RequestType.RPop, [key, str(count)]) def sadd(self, key: str, members: List[str]): - """Add specified members to the set stored at `key`. + """ + Add specified members to the set stored at `key`. Specified members that are already a member of this set are ignored. If `key` does not exist, a new set is created before adding `members`. See https://redis.io/commands/sadd/ for more details. @@ -567,12 +615,12 @@ def sadd(self, key: str, members: List[str]): Command response: int: The number of members that were added to the set, excluding members already present. - If `key` holds a value that is not a set, the transaction fails. """ self.append_command(RequestType.SAdd, [key] + members) def srem(self, key: str, members: List[str]): - """Remove specified members from the set stored at `key`. + """ + Remove specified members from the set stored at `key`. Specified members that are not a member of this set are ignored. See https://redis.io/commands/srem/ for details. @@ -583,12 +631,12 @@ def srem(self, key: str, members: List[str]): Commands response: int: The number of members that were removed from the set, excluding non-existing members. If `key` does not exist, it is treated as an empty set and this command returns 0. - If `key` holds a value that is not a set, the transaction fails. """ self.append_command(RequestType.SRem, [key] + members) def smembers(self, key: str): - """Retrieve all the members of the set value stored at `key`. + """ + Retrieve all the members of the set value stored at `key`. See https://redis.io/commands/smembers/ for details. Args: @@ -597,12 +645,12 @@ def smembers(self, key: str): Commands response: Set[str]: A set of all members of the set. If `key` does not exist an empty list will be returned. - If `key` holds a value that is not a set, the transaction fails. """ self.append_command(RequestType.SMembers, [key]) def scard(self, key: str): - """Retrieve the set cardinality (number of elements) of the set stored at `key`. + """ + Retrieve the set cardinality (number of elements) of the set stored at `key`. See https://redis.io/commands/scard/ for details. Args: @@ -610,12 +658,12 @@ def scard(self, key: str): Commands response: int: The cardinality (number of elements) of the set, or 0 if the key does not exist. - If `key` holds a value that is not a set, the transaction fails. """ self.append_command(RequestType.SCard, [key]) def ltrim(self, key: str, start: int, end: int): - """Trim an existing list so that it will contain only the specified range of elements specified. + """ + Trim an existing list so that it will contain only the specified range of elements specified. The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next element and so on. These offsets can also be negative numbers indicating offsets starting at the end of the list, with -1 being the last @@ -633,12 +681,12 @@ def ltrim(self, key: str, start: int, end: int): (which causes `key` to be removed). If `end` exceeds the actual end of the list, it will be treated like the last element of the list. f `key` does not exist, the response will be "OK" without changes to the database. - If `key` holds a value that is not a list, the transaction fails. """ self.append_command(RequestType.LTrim, [key, str(start), str(end)]) def lrem(self, key: str, count: int, element: str): - """Removes the first `count` occurrences of elements equal to `element` from the list stored at `key`. + """ + Removes the first `count` occurrences of elements equal to `element` from the list stored at `key`. If `count` is positive, it removes elements equal to `element` moving from head to tail. If `count` is negative, it removes elements equal to `element` moving from tail to head. If `count` is 0 or greater than the occurrences of elements equal to `element`, it removes all elements @@ -653,12 +701,12 @@ def lrem(self, key: str, count: int, element: str): Commands response: int: The number of removed elements. If `key` does not exist, 0 is returned. - If `key` holds a value that is not a list, the transaction fails with an error. """ self.append_command(RequestType.LRem, [key, str(count), element]) def llen(self, key: str): - """Get the length of the list stored at `key`. + """ + Get the length of the list stored at `key`. See https://redis.io/commands/llen/ for details. Args: @@ -667,12 +715,12 @@ def llen(self, key: str): Commands response: int: The length of the list at the specified key. If `key` does not exist, it is interpreted as an empty list and 0 is returned. - If `key` holds a value that is not a list, the transaction fails with an error. """ self.append_command(RequestType.LLen, [key]) def exists(self, keys: List[str]): - """Returns the number of keys in `keys` that exist in the database. + """ + Returns the number of keys in `keys` that exist in the database. See https://redis.io/commands/exists/ for more details. Args: @@ -685,7 +733,8 @@ def exists(self, keys: List[str]): self.append_command(RequestType.Exists, keys) def unlink(self, keys: List[str]): - """Unlink (delete) multiple keys from the database. + """ + Unlink (delete) multiple keys from the database. A key is ignored if it does not exist. This command, similar to DEL, removes specified keys and ignores non-existent ones. However, this command does not block the server, while [DEL](https://redis.io/commands/del) does. @@ -715,7 +764,6 @@ def expire(self, key: str, seconds: int, option: Optional[ExpireOptions] = None) Commands response: bool: 'True' if the timeout was set, 'False' if the timeout was not set (e.g., the key doesn't exist or the operation is skipped due to the provided arguments). - """ args: List[str] = ( [key, str(seconds)] if option is None else [key, str(seconds), option.value] @@ -768,7 +816,6 @@ def pexpire( Commands response: bool: 'True' if the timeout was set, 'False' if the timeout was not set (e.g., the key doesn't exist or the operation is skipped due to the provided arguments). - """ args = ( [key, str(milliseconds)] @@ -861,7 +908,6 @@ def zadd( Commands response: int: The number of elements added to the sorted set. If `changed` is set, returns the number of elements updated in the sorted set. - If `key` holds a value that is not a sorted set, the transaction fails with an error. """ args = [key] if existing_options: @@ -916,7 +962,6 @@ def zadd_incr( Commands response: Optional[float]: The score of the member. If there was a conflict with choosing the XX/NX/LT/GT options, the operation aborts and None is returned. - If `key` holds a value that is not a sorted set, the transaction fails with an error. """ args = [key] if existing_options: @@ -949,7 +994,6 @@ def zcard(self, key: str): Commands response: int: The number of elements in the sorted set. If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. - If `key` holds a value that is not a sorted set, the transaction fails with an error. """ self.append_command(RequestType.Zcard, [key]) @@ -977,7 +1021,6 @@ def zcount( int: The number of members in the specified score range. If key does not exist, 0 is returned. If `max_score` < `min_score`, 0 is returned. - If `key` holds a value that is not a sorted set, an error is returned. """ self.append_command(RequestType.Zcount, [key, min_score.value, max_score.value]) @@ -999,7 +1042,6 @@ def zrem( Commands response: int: The number of members that were removed from the sorted set, not including non-existing members. If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. - If `key` holds a value that is not a sorted set, the transaction fails with an error. """ self.append_command(RequestType.Zrem, [key] + members) @@ -1017,7 +1059,6 @@ def zscore(self, key: str, member: str): Optional[float]: The score of the member. If `member` does not exist in the sorted set, None is returned. If `key` does not exist, None is returned. - If `key` holds a value that is not a sorted set, the transaction fails with an error. """ self.append_command(RequestType.ZScore, [key, member]) @@ -1035,14 +1076,15 @@ class Transaction(BaseTransaction): >>> transaction.set("key", "value") >>> transaction.select(1) # Standalone command >>> transaction.get("key") - >>> client.exec(transaction) + >>> await client.exec(transaction) [OK , OK , None] """ # TODO: add MOVE, SLAVEOF and all SENTINEL commands def select(self, index: int): - """Change the currently selected Redis database. + """ + Change the currently selected Redis database. See https://redis.io/commands/select/ for details. Args: From ace07863eacdd361fc3840719bba18a5d2134d4b Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Wed, 14 Feb 2024 09:23:27 -0800 Subject: [PATCH 14/22] Go: Add CI (#946) * Add CI for Go * Add command to install wget to amazon linux workflow * Remove sudo from the amazon linux tar command * Add command to install tar on amazon linux * Change command so that the Go path is updated for all amazon linux steps * Remove command to list Go version * Fix missing steps, go mod command * Remove redundant step * Add Go bin to path, remove patch version from go mod command * Update amazon linux test name * Increase timeout * Specify minimum go version as 1.18, remove dependency requiring Go 1.21, fix yml formatting, fix setup-go cache --- .github/workflows/go.yml | 178 ++++++++++++++++++++++ go/.cargo/config.toml | 3 + go/.gitignore | 4 + go/Cargo.toml | 19 +++ go/Makefile | 29 ++++ go/glide/glide.go | 1 + go/go.mod | 21 +++ go/go.sum | 318 +++++++++++++++++++++++++++++++++++++++ go/src/lib.rs | 9 ++ go/tests/glide_test.go | 13 ++ go/tools.go | 10 ++ 11 files changed, 605 insertions(+) create mode 100644 .github/workflows/go.yml create mode 100644 go/.cargo/config.toml create mode 100644 go/.gitignore create mode 100644 go/Cargo.toml create mode 100644 go/Makefile create mode 100644 go/glide/glide.go create mode 100644 go/go.mod create mode 100644 go/go.sum create mode 100644 go/src/lib.rs create mode 100644 go/tests/glide_test.go create mode 100644 go/tools.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000000..c50de313cb --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,178 @@ +name: Go CI + +on: + workflow_dispatch: + push: + branches: [ "main" ] + paths: + - glide-core/** + - submodules/** + - go/** + - .github/workflows/go.yml + pull_request: + paths: + - glide-core/** + - submodules/** + - go/** + - .github/workflows/go.yml + +# Run only the latest job on a branch and cancel previous ones +concurrency: + group: ${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + build-and-test-go-client: + timeout-minutes: 20 + strategy: + # Run all jobs + fail-fast: false + matrix: + go: + - '1.18' + - '1.21' + redis: + - 6.2.14 + - 7.2.3 + os: + - ubuntu-latest + - macos-latest + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Go ${{ matrix.go }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + cache-dependency-path: go/go.sum + + - name: Install shared software dependencies + uses: ./.github/workflows/install-shared-dependencies + with: + os: ${{ matrix.os }} + target: ${{ matrix.os == 'ubuntu-latest' && 'x86_64-unknown-linux-gnu' || 'x86_64-apple-darwin' }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install redis + # TODO: make this step macos compatible: https://github.com/aws/glide-for-redis/issues/781 + if: ${{ matrix.os == 'ubuntu-latest' }} + uses: ./.github/workflows/install-redis + with: + redis-version: ${{ matrix.redis }} + + - name: Install client dependencies + working-directory: ./go + run: make install-tools + + - name: Build client + working-directory: ./go + run: make build + + - name: Run linters + working-directory: ./go + run: make lint + + - name: Run unit tests + working-directory: ./go + run: make unit-test-report + + - name: Upload test reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: test-reports-go-${{ matrix.go }}-redis-${{ matrix.redis }}-${{ matrix.os }} + path: | + go/reports/unit-test-report.html + + build-amazonlinux-latest: + if: github.repository_owner == 'aws' + strategy: + # Run all jobs + fail-fast: false + matrix: + go: + - 1.18.10 + - 1.21.6 + runs-on: ubuntu-latest + container: amazonlinux:latest + timeout-minutes: 15 + steps: + - name: Install git + run: | + yum -y remove git + yum -y remove git-* + yum -y install https://packages.endpointdev.com/rhel/7/os/x86_64/endpoint-repo.x86_64.rpm + yum update + yum install -y git + git --version + + - uses: actions/checkout@v4 + + - name: Checkout submodules + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + git submodule update --init --recursive + + - name: Install shared software dependencies + uses: ./.github/workflows/install-shared-dependencies + with: + os: "amazon-linux" + target: "x86_64-unknown-linux-gnu" + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Create a symbolic Link for redis6 binaries + run: | + ln -s /usr/bin/redis6-server /usr/bin/redis-server + ln -s /usr/bin/redis6-cli /usr/bin/redis-cli + + - name: Install Go + run: | + yum -y install wget + yum -y install tar + wget https://go.dev/dl/go${{ matrix.go }}.linux-amd64.tar.gz + tar -C /usr/local -xzf go${{ matrix.go }}.linux-amd64.tar.gz + echo "/usr/local/go/bin" >> $GITHUB_PATH + echo "$HOME/go/bin" >> $GITHUB_PATH + + - name: Install client dependencies + working-directory: ./go + run: make install-tools + + - name: Build client + working-directory: ./go + run: make build + + - name: Run linters + working-directory: ./go + run: make lint + + - name: Run unit tests + working-directory: ./go + run: make unit-test-report + + - name: Upload test reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: test-reports-go-${{ matrix.go }}-amazon-linux-latest + path: go/reports/unit-test-report.html + + lint-rust: + timeout-minutes: 15 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: ./.github/workflows/lint-rust + with: + cargo-toml-folder: ./go + name: lint go rust diff --git a/go/.cargo/config.toml b/go/.cargo/config.toml new file mode 100644 index 0000000000..f510628c9e --- /dev/null +++ b/go/.cargo/config.toml @@ -0,0 +1,3 @@ +[env] +GLIDE_NAME = { value = "GlideGo", force = true } +GLIDE_VERSION = "0.1.0" diff --git a/go/.gitignore b/go/.gitignore new file mode 100644 index 0000000000..5b5c1537a7 --- /dev/null +++ b/go/.gitignore @@ -0,0 +1,4 @@ +# Go compilation files +*.pb.go + +reports diff --git a/go/Cargo.toml b/go/Cargo.toml new file mode 100644 index 0000000000..8bbc3b5b12 --- /dev/null +++ b/go/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "glide-rs" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +authors = ["Amazon Web Services"] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +redis = { path = "../submodules/redis-rs/redis", features = ["aio", "tokio-comp", "tls", "tokio-native-tls-comp", "tls-rustls-insecure"] } +glide-core = { path = "../glide-core" } +tokio = { version = "^1", features = ["rt", "macros", "rt-multi-thread", "time"] } +protobuf = { version = "3.3.0", features = [] } + +[profile.release] +lto = true +debug = true diff --git a/go/Makefile b/go/Makefile new file mode 100644 index 0000000000..bd3e101b7b --- /dev/null +++ b/go/Makefile @@ -0,0 +1,29 @@ +install-tools: + @cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI % go install % + +build: build-glide-core build-glide-client generate-protobuf + go build ./... + +build-glide-core: + cd ../glide-core; cargo build --release + +build-glide-client: + cargo build --release + +generate-protobuf: + mkdir -p protobuf + protoc --proto_path=../glide-core/src/protobuf \ + --go_opt=Mconnection_request.proto=github.com/aws/glide-for-redis/go/protobuf \ + --go_opt=Mredis_request.proto=github.com/aws/glide-for-redis/go/protobuf \ + --go_opt=Mresponse.proto=github.com/aws/glide-for-redis/go/protobuf \ + --go_out=./protobuf \ + --go_opt=paths=source_relative \ + ../glide-core/src/protobuf/*.proto + +lint: + go vet ./... + staticcheck ./... + +unit-test-report: + mkdir -p reports + go test -race ./... -json | go-test-report -o reports/unit-test-report.html diff --git a/go/glide/glide.go b/go/glide/glide.go new file mode 100644 index 0000000000..767864d035 --- /dev/null +++ b/go/glide/glide.go @@ -0,0 +1 @@ +package glide diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000000..e5c99d1988 --- /dev/null +++ b/go/go.mod @@ -0,0 +1,21 @@ +module github.com/aws/glide-for-redis/go/glide + +go 1.18 + +require ( + github.com/vakenbolt/go-test-report v0.9.3 + google.golang.org/protobuf v1.32.0 + honnef.co/go/tools v0.3.3 +) + +require ( + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/tools v0.12.1-0.20230825192346-2191a27a6dc5 // indirect +) diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000000..4ccef17d5a --- /dev/null +++ b/go/go.sum @@ -0,0 +1,318 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/vakenbolt/go-test-report v0.9.3 h1:KPJIZJhr3CKdk82+6KD/LnLF89lvW8aklyRqOjlPJRQ= +github.com/vakenbolt/go-test-report v0.9.3/go.mod h1:sSBCeKCZsuw8Ph983JpYkuEe4fWteYI3YdAtZr9FNds= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE= +golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.12.1-0.20230825192346-2191a27a6dc5 h1:Vk4mysSz+GqQK2eqgWbo4zEO89wkeAjJiFIr9bpqa8k= +golang.org/x/tools v0.12.1-0.20230825192346-2191a27a6dc5/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.3.3 h1:oDx7VAwstgpYpb3wv0oxiZlxY+foCpRAwY7Vk6XpAgA= +honnef.co/go/tools v0.3.3/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/go/src/lib.rs b/go/src/lib.rs new file mode 100644 index 0000000000..26b1dacc35 --- /dev/null +++ b/go/src/lib.rs @@ -0,0 +1,9 @@ +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ +use std::ffi::c_void; + +#[no_mangle] +pub extern "C" fn create_connection() -> *const c_void { + todo!() +} diff --git a/go/tests/glide_test.go b/go/tests/glide_test.go new file mode 100644 index 0000000000..32103c6fc5 --- /dev/null +++ b/go/tests/glide_test.go @@ -0,0 +1,13 @@ +package tests + +import ( + "testing" +) + +// TODO: Replace this test with real tests when glide client implementation is started +func TestArbitraryLogic(t *testing.T) { + someVar := true + if !someVar { + t.Fatalf("Expected someVar to be true, but was false.") + } +} diff --git a/go/tools.go b/go/tools.go new file mode 100644 index 0000000000..b6b28536d0 --- /dev/null +++ b/go/tools.go @@ -0,0 +1,10 @@ +//go:build tools +// +build tools + +package main + +import ( + _ "github.com/vakenbolt/go-test-report" + _ "google.golang.org/protobuf/cmd/protoc-gen-go" + _ "honnef.co/go/tools/cmd/staticcheck" +) From b17a77f06c70de040bf1ddeabfe5d6764e33c3cc Mon Sep 17 00:00:00 2001 From: ort-bot Date: Thu, 15 Feb 2024 00:18:36 +0000 Subject: [PATCH 15/22] Updated attribution files --- python/THIRD_PARTY_LICENSES_PYTHON | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/THIRD_PARTY_LICENSES_PYTHON b/python/THIRD_PARTY_LICENSES_PYTHON index 1ee24d9415..59937eb594 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -41454,7 +41454,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: google-api-core:2.17.0 +Package: google-api-core:2.17.1 The following copyrights and licenses were found in the source code of this package: From 2c5561153c22dbc04a238e0a22024a6e431b459a Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Thu, 15 Feb 2024 08:52:19 -0800 Subject: [PATCH 16/22] Java: Add @NonNull annotation to constructors and generic functions (#928) * Add @NonNull annotation to constructors and generic functions Signed-off-by: Andrew Carbonetto --------- Signed-off-by: Andrew Carbonetto Signed-off-by: Yury-Fridlyand Co-authored-by: Yury-Fridlyand --- java/client/src/main/java/glide/api/RedisClient.java | 3 ++- .../client/src/main/java/glide/api/RedisClusterClient.java | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index 744d582d8a..c6dd4e10d4 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -30,7 +30,8 @@ protected RedisClient(ConnectionManager connectionManager, CommandManager comman * @param config Redis client Configuration * @return A Future to connect and return a RedisClient */ - public static CompletableFuture CreateClient(RedisClientConfiguration config) { + public static CompletableFuture CreateClient( + @NonNull RedisClientConfiguration config) { return CreateClient(config, RedisClient::new); } diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index ddbb5d6e25..c802979931 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -41,12 +41,12 @@ protected RedisClusterClient(ConnectionManager connectionManager, CommandManager * @return A Future to connect and return a RedisClusterClient */ public static CompletableFuture CreateClient( - RedisClusterClientConfiguration config) { + @NonNull RedisClusterClientConfiguration config) { return CreateClient(config, RedisClusterClient::new); } @Override - public CompletableFuture> customCommand(String[] args) { + public CompletableFuture> customCommand(@NonNull String[] args) { // TODO if a command returns a map as a single value, ClusterValue misleads user return commandManager.submitNewCommand( CustomCommand, args, response -> ClusterValue.of(handleObjectOrNullResponse(response))); @@ -54,7 +54,8 @@ public CompletableFuture> customCommand(String[] args) { @Override @SuppressWarnings("unchecked") - public CompletableFuture> customCommand(String[] args, Route route) { + public CompletableFuture> customCommand( + @NonNull String[] args, @NonNull Route route) { return commandManager.submitNewCommand( CustomCommand, args, From 7292960446f1f20dc32b78d8db45cd2519c5b646 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Thu, 15 Feb 2024 08:53:46 -0800 Subject: [PATCH 17/22] Java: Add ExamplesApp and Update benchmarkingApp to both have Java/Glide-for-redis client (#896) * Java: Add ExamplesApp and Update benchmarkingApp to both have Java/Glide-for-redis client (#896) Signed-off-by: Andrew Carbonetto --------- Signed-off-by: Andrew Carbonetto Signed-off-by: Yury-Fridlyand Co-authored-by: Yury-Fridlyand --- .github/workflows/java-benchmark.yml | 9 +- benchmarks/install_and_test.sh | 2 +- java/README.md | 26 +++--- java/benchmarks/build.gradle | 6 +- .../glide/benchmarks/BenchmarkingApp.java | 6 +- .../clients/glide/GlideAsyncClient.java | 83 +++++++++++++++++++ .../clients/lettuce/LettuceAsyncClient.java | 2 +- java/examples/build.gradle | 25 ++++++ .../main/java/glide/examples/ExamplesApp.java | 41 +++++++++ java/settings.gradle | 1 + 10 files changed, 177 insertions(+), 24 deletions(-) create mode 100644 java/benchmarks/src/main/java/glide/benchmarks/clients/glide/GlideAsyncClient.java create mode 100644 java/examples/build.gradle create mode 100644 java/examples/src/main/java/glide/examples/ExamplesApp.java diff --git a/.github/workflows/java-benchmark.yml b/.github/workflows/java-benchmark.yml index eceaf6cc0f..21d7d7fea1 100644 --- a/.github/workflows/java-benchmark.yml +++ b/.github/workflows/java-benchmark.yml @@ -30,7 +30,7 @@ jobs: submodules: recursive - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "temurin" java-version: ${{ matrix.java }} @@ -42,16 +42,15 @@ jobs: - name: benchmark uses: ./.github/workflows/test-benchmark - # TODO - enable once benchmark works - if: ${{ false }} with: language-flag: -java - name: Upload test reports if: always() continue-on-error: true - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: test-reports-${{ matrix.java }} + name: test-reports-java-${{ matrix.java }}-redis-${{ matrix.redis }} path: | java/benchmarks/build/reports/** + benchmarks/results/** diff --git a/benchmarks/install_and_test.sh b/benchmarks/install_and_test.sh index b45f8ba23f..72b56fb2cb 100755 --- a/benchmarks/install_and_test.sh +++ b/benchmarks/install_and_test.sh @@ -74,7 +74,7 @@ function runCSharpBenchmark(){ function runJavaBenchmark(){ cd ${BENCH_FOLDER}/../java - ./gradlew run --args="-resultsFile \"${BENCH_FOLDER}/$1\" -dataSize \"$2\" -concurrentTasks \"$concurrentTasks\" -clients \"$chosenClients\" -host $host $javaPortFlag -clientCount \"$clientCount\" $javaTlsFlag $javaClusterFlag" + ./gradlew :benchmarks:run --args="-resultsFile \"${BENCH_FOLDER}/$1\" -dataSize \"$2\" -concurrentTasks \"$concurrentTasks\" -clients \"$chosenClients\" -host $host $javaPortFlag -clientCount \"$clientCount\" $javaTlsFlag $javaClusterFlag" } function runRustBenchmark(){ diff --git a/java/README.md b/java/README.md index a9379f32a1..fbedc1b499 100644 --- a/java/README.md +++ b/java/README.md @@ -9,8 +9,10 @@ to develop this Java wrapper. The Java client contains the following parts: -1. A Java client (lib folder): wrapper to rust client. -2. A benchmark app: A dedicated benchmarking tool designed to evaluate and compare the performance of GLIDE for Redis and other Java clients. +1. `client`: A Java-wrapper around the rust-core client. +2. `examples`: An examples app to test the client against a Redis localhost +3. `benchmark`: A dedicated benchmarking tool designed to evaluate and compare the performance of GLIDE for Redis and other Java clients. +4. `integTest`: An integration test sub-project for API and E2E testing ## Installation and Setup @@ -82,8 +84,10 @@ $ ./gradlew :client:test Other useful gradle developer commands: * `./gradlew :client:test` to run client unit tests +* `./gradlew :integTest:test` to run client examples * `./gradlew spotlessCheck` to check for codestyle issues * `./gradlew spotlessApply` to apply codestyle recommendations +* `./gradlew :examples:run` to run client examples * `./gradlew :benchmarks:run` to run performance benchmarks ## Basic Examples @@ -91,19 +95,15 @@ Other useful gradle developer commands: ### Standalone Redis: ```java -import glide.Client; -import glide.Client.SingleResponse; +import glide.api.RedisClient; -Client client = new Client(); +RedisClient client = RedisClient.CreateClient().get(); -SingleResponse connect = client.asyncConnectToRedis("localhost", 6379); -connect.await().isSuccess(); +CompletableFuture setResponse = client.set("key", "foobar"); +assert setResponse.get() == "OK" : "Failed on client.set("key", "foobar") request"; -SingleResponse set = client.asyncSet("key", "foobar"); -set.await().isSuccess(); - -SingleResponse get = client.asyncGet("key"); -get.await().getValue() == "foobar"; +CompletableFuture getResponse = client.get("key"); +assert getResponse.get() == "foobar" : "Failed on client.get("key") request"; ``` ### Benchmarks @@ -115,7 +115,7 @@ You can run benchmarks using `./gradlew run`. You can set arguments using the ar ./gradlew run --args="-resultsFile=output -dataSize \"100 1000\" -concurrentTasks \"10 100\" -clients all -host localhost -port 6279 -clientCount \"1 5\" -tls" ``` -The following arguments are accepted: +The following arguments are accepted: * `resultsFile`: the results output file * `concurrentTasks`: Number of concurrent tasks * `clients`: one of: all|jedis|lettuce|glide diff --git a/java/benchmarks/build.gradle b/java/benchmarks/build.gradle index 18ca4e4f13..2aaad7c27e 100644 --- a/java/benchmarks/build.gradle +++ b/java/benchmarks/build.gradle @@ -9,6 +9,8 @@ repositories { } dependencies { + implementation project(':client') + // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:32.1.1-jre' implementation 'redis.clients:jedis:4.4.3' @@ -17,10 +19,12 @@ dependencies { implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.13.0' implementation group: 'org.apache.commons', name: 'commons-math3', version: '3.5' implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1' - } +run.dependsOn ':client:buildRustRelease' + application { // Define the main class for the application. mainClass = 'glide.benchmarks.BenchmarkingApp' + applicationDefaultJvmArgs = ['-Djava.library.path=../target/release'] } diff --git a/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java b/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java index e246ec7345..9c5d196699 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java @@ -3,6 +3,7 @@ import static glide.benchmarks.utils.Benchmarking.testClientSetGet; +import glide.benchmarks.clients.glide.GlideAsyncClient; import glide.benchmarks.clients.jedis.JedisClient; import glide.benchmarks.clients.lettuce.LettuceAsyncClient; import java.util.Arrays; @@ -46,17 +47,16 @@ public static void main(String[] args) { for (ClientName client : runConfiguration.clients) { switch (client) { case JEDIS: - // run testClientSetGet on JEDIS sync client System.out.println("Run JEDIS sync client"); testClientSetGet(JedisClient::new, runConfiguration, false); break; case LETTUCE: - // run testClientSetGet on LETTUCE async client System.out.println("Run LETTUCE async client"); testClientSetGet(LettuceAsyncClient::new, runConfiguration, true); break; case GLIDE: - System.out.println("GLIDE for Redis async not yet configured"); + System.out.println("GLIDE for Redis async client"); + testClientSetGet(GlideAsyncClient::new, runConfiguration, true); break; } } diff --git a/java/benchmarks/src/main/java/glide/benchmarks/clients/glide/GlideAsyncClient.java b/java/benchmarks/src/main/java/glide/benchmarks/clients/glide/GlideAsyncClient.java new file mode 100644 index 0000000000..456ba7a50f --- /dev/null +++ b/java/benchmarks/src/main/java/glide/benchmarks/clients/glide/GlideAsyncClient.java @@ -0,0 +1,83 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.benchmarks.clients.glide; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import glide.api.BaseClient; +import glide.api.RedisClient; +import glide.api.RedisClusterClient; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.RedisClientConfiguration; +import glide.api.models.configuration.RedisClusterClientConfiguration; +import glide.benchmarks.clients.AsyncClient; +import glide.benchmarks.utils.ConnectionSettings; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +/** A Glide client with async capabilities */ +public class GlideAsyncClient implements AsyncClient { + private BaseClient redisClient; + + @Override + public void connectToRedis(ConnectionSettings connectionSettings) { + + if (connectionSettings.clusterMode) { + RedisClusterClientConfiguration config = + RedisClusterClientConfiguration.builder() + .address( + NodeAddress.builder() + .host(connectionSettings.host) + .port(connectionSettings.port) + .build()) + .useTLS(connectionSettings.useSsl) + .build(); + try { + redisClient = RedisClusterClient.CreateClient(config).get(10, SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + + } else { + RedisClientConfiguration config = + RedisClientConfiguration.builder() + .address( + NodeAddress.builder() + .host(connectionSettings.host) + .port(connectionSettings.port) + .build()) + .useTLS(connectionSettings.useSsl) + .build(); + + try { + redisClient = RedisClient.CreateClient(config).get(10, SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public CompletableFuture asyncSet(String key, String value) { + return redisClient.set(key, value); + } + + @Override + public CompletableFuture asyncGet(String key) { + return redisClient.get(key); + } + + @Override + public void closeConnection() { + try { + redisClient.close(); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + @Override + public String getName() { + return "Glide Async"; + } +} diff --git a/java/benchmarks/src/main/java/glide/benchmarks/clients/lettuce/LettuceAsyncClient.java b/java/benchmarks/src/main/java/glide/benchmarks/clients/lettuce/LettuceAsyncClient.java index 3c24578ec9..d880b9b71a 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/clients/lettuce/LettuceAsyncClient.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/clients/lettuce/LettuceAsyncClient.java @@ -30,7 +30,7 @@ public void connectToRedis(ConnectionSettings connectionSettings) { .withPort(connectionSettings.port) .withSsl(connectionSettings.useSsl) .build(); - if (connectionSettings.clusterMode) { + if (!connectionSettings.clusterMode) { client = RedisClient.create(uri); connection = ((RedisClient) client).connect(); asyncCommands = ((StatefulRedisConnection) connection).async(); diff --git a/java/examples/build.gradle b/java/examples/build.gradle new file mode 100644 index 0000000000..0e526d95e4 --- /dev/null +++ b/java/examples/build.gradle @@ -0,0 +1,25 @@ +plugins { + // Apply the application plugin to add support for building a CLI application in Java. + id 'application' +} + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + implementation project(':client') + + implementation 'redis.clients:jedis:4.4.3' + implementation 'io.lettuce:lettuce-core:6.2.6.RELEASE' + implementation 'commons-cli:commons-cli:1.5.0' +} + +run.dependsOn ':client:buildRustRelease' + +application { + // Define the main class for the application. + mainClass = 'glide.examples.ExamplesApp' + applicationDefaultJvmArgs = ['-Djava.library.path=../target/release'] +} diff --git a/java/examples/src/main/java/glide/examples/ExamplesApp.java b/java/examples/src/main/java/glide/examples/ExamplesApp.java new file mode 100644 index 0000000000..ea816f9632 --- /dev/null +++ b/java/examples/src/main/java/glide/examples/ExamplesApp.java @@ -0,0 +1,41 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.examples; + +import glide.api.RedisClient; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.RedisClientConfiguration; +import java.util.concurrent.ExecutionException; + +public class ExamplesApp { + + // main application entrypoint + public static void main(String[] args) { + runGlideExamples(); + } + + private static void runGlideExamples() { + String host = "localhost"; + Integer port = 6379; + boolean useSsl = false; + + RedisClientConfiguration config = + RedisClientConfiguration.builder() + .address(NodeAddress.builder().host(host).port(port).build()) + .useTLS(useSsl) + .build(); + + try { + RedisClient client = RedisClient.CreateClient(config).get(); + + System.out.println("PING: " + client.ping().get()); + System.out.println("PING(found you): " + client.ping("found you").get()); + + System.out.println("SET(apples, oranges): " + client.set("apples", "oranges").get()); + System.out.println("GET(apples): " + client.get("apples").get()); + + } catch (ExecutionException | InterruptedException e) { + System.out.println("Glide example failed with an exception: "); + e.printStackTrace(); + } + } +} diff --git a/java/settings.gradle b/java/settings.gradle index 6d5e31d8a0..d93b818e15 100644 --- a/java/settings.gradle +++ b/java/settings.gradle @@ -2,4 +2,5 @@ rootProject.name = 'glide' include 'client' include 'integTest' +include 'examples' include 'benchmarks' From 98b1ed7278f84acb319c7177f0083bb476852173 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 15 Feb 2024 09:05:56 -0800 Subject: [PATCH 18/22] Fix java IT for linter (#966) * Fix a file. Signed-off-by: Yury-Fridlyand * Add CI check. Signed-off-by: Yury-Fridlyand --------- Signed-off-by: Yury-Fridlyand --- .github/workflows/java.yml | 4 + .../test/java/glide/cluster/CommandTests.java | 91 ++++++++++++++----- 2 files changed, 70 insertions(+), 25 deletions(-) diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index e1f23f92d0..b657a880f5 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -68,6 +68,10 @@ jobs: working-directory: java run: ./gradlew --continue build + - name: Ensure no skipped files by linter + working-directory: java + run: ./gradlew spotlessDiagnose | grep 'All formatters are well behaved for all files' + - name: Upload test reports if: always() continue-on-error: true diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 195aa10252..0e4e584877 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -3,9 +3,6 @@ import static glide.TestConfiguration.CLUSTER_PORTS; import static glide.TestConfiguration.REDIS_VERSION; -import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_NODES; -import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_PRIMARIES; -import static glide.TestConfiguration.CLUSTER_PORTS; import static glide.api.models.commands.InfoOptions.Section.CLIENTS; import static glide.api.models.commands.InfoOptions.Section.CLUSTER; import static glide.api.models.commands.InfoOptions.Section.COMMANDSTATS; @@ -13,9 +10,10 @@ import static glide.api.models.commands.InfoOptions.Section.EVERYTHING; import static glide.api.models.commands.InfoOptions.Section.MEMORY; import static glide.api.models.commands.InfoOptions.Section.REPLICATION; +import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_NODES; +import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.RANDOM; import static glide.api.models.configuration.RequestRoutingConfiguration.SlotType.PRIMARY; -import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -41,12 +39,49 @@ public class CommandTests { private static final String INITIAL_VALUE = "VALUE"; - public static final List - DEFAULT_INFO_SECTIONS = List.of("Server", "Clients", "Memory", "Persistence", "Stats", "Replication", "CPU", "Modules", "Errorstats", "Cluster", "Keyspace"); - public static final List EVERYTHING_INFO_SECTIONS = REDIS_VERSION.feature() >= 7 - // Latencystats was added in redis 7 - ? List.of("Server", "Clients", "Memory", "Persistence", "Stats", "Replication", "CPU", "Modules", "Commandstats", "Errorstats", "Latencystats", "Cluster", "Keyspace") - : List.of("Server", "Clients", "Memory", "Persistence", "Stats", "Replication", "CPU", "Modules", "Commandstats", "Errorstats", "Cluster", "Keyspace"); + public static final List DEFAULT_INFO_SECTIONS = + List.of( + "Server", + "Clients", + "Memory", + "Persistence", + "Stats", + "Replication", + "CPU", + "Modules", + "Errorstats", + "Cluster", + "Keyspace"); + public static final List EVERYTHING_INFO_SECTIONS = + REDIS_VERSION.feature() >= 7 + // Latencystats was added in redis 7 + ? List.of( + "Server", + "Clients", + "Memory", + "Persistence", + "Stats", + "Replication", + "CPU", + "Modules", + "Commandstats", + "Errorstats", + "Latencystats", + "Cluster", + "Keyspace") + : List.of( + "Server", + "Clients", + "Memory", + "Persistence", + "Stats", + "Replication", + "CPU", + "Modules", + "Commandstats", + "Errorstats", + "Cluster", + "Keyspace"); @BeforeAll @SneakyThrows @@ -79,7 +114,8 @@ public void custom_command_info() { @Test @SneakyThrows public void custom_command_ping() { - ClusterValue data = clusterClient.customCommand(new String[] {"ping"}).get(10, TimeUnit.SECONDS); + ClusterValue data = + clusterClient.customCommand(new String[] {"ping"}).get(10, TimeUnit.SECONDS); assertEquals("PONG", data.getSingleValue()); } @@ -141,8 +177,10 @@ public void info_with_multiple_options() { InfoOptions options = builder.build(); ClusterValue data = clusterClient.info(options).get(); for (String info : data.getMultiValue().values()) { - for (String section : options.toArgs()) { - assertTrue(info.toLowerCase().contains("# " + section.toLowerCase()), "Section " + section + " is missing"); + for (String section : options.toArgs()) { + assertTrue( + info.toLowerCase().contains("# " + section.toLowerCase()), + "Section " + section + " is missing"); } } } @@ -163,18 +201,19 @@ public void info_with_everything_option() { @Test @SneakyThrows public void info_with_routing_and_options() { - ClusterValue slotData = clusterClient.customCommand(new String[] {"cluster", "slots"}).get(); - /* - Nested Object arrays like - 1) 1) (integer) 0 - 2) (integer) 5460 - 3) 1) "127.0.0.1" - 2) (integer) 7000 - 3) "92d73b6eb847604b63c7f7cbbf39b148acdd1318" - 4) (empty array) - */ + ClusterValue slotData = + clusterClient.customCommand(new String[] {"cluster", "slots"}).get(); + + // Nested Object arrays like + // 1) 1) (integer) 0 + // 2) (integer) 5460 + // 3) 1) "127.0.0.1" + // 2) (integer) 7000 + // 3) "92d73b6eb847604b63c7f7cbbf39b148acdd1318" + // 4) (empty array) // Extracting first slot key - var slotKey = (String)((Object[])((Object[])((Object[])slotData.getSingleValue())[0])[2])[2]; + var slotKey = + (String) ((Object[]) ((Object[]) ((Object[]) slotData.getSingleValue())[0])[2])[2]; InfoOptions.InfoOptionsBuilder builder = InfoOptions.builder().section(CLIENTS); if (REDIS_VERSION.feature() >= 7) { @@ -185,7 +224,9 @@ public void info_with_routing_and_options() { ClusterValue data = clusterClient.info(options, routing).get(); for (String section : options.toArgs()) { - assertTrue(data.getSingleValue().toLowerCase().contains("# " + section.toLowerCase()), "Section " + section + " is missing"); + assertTrue( + data.getSingleValue().toLowerCase().contains("# " + section.toLowerCase()), + "Section " + section + " is missing"); } } } From 18bb7c35882f3881e9702042ea41cabdb7e27fea Mon Sep 17 00:00:00 2001 From: aaron-congo Date: Tue, 13 Feb 2024 15:32:06 -0800 Subject: [PATCH 19/22] Java: Add SADD, SREM, SMEMBERS, and SCARD commands (Set Commands) --- .../src/main/java/glide/api/BaseClient.java | 38 ++++++- .../java/glide/api/commands/SetCommands.java | 89 +++++++++++++++ .../glide/api/models/BaseTransaction.java | 85 +++++++++++++++ .../test/java/glide/api/RedisClientTest.java | 102 ++++++++++++++++++ .../api/models/ClusterTransactionTests.java | 16 +++ .../glide/api/models/TransactionTests.java | 16 +++ .../test/java/glide/SharedCommandTests.java | 66 ++++++++++++ .../src/test/java/glide/TestUtilities.java | 9 +- 8 files changed, 419 insertions(+), 2 deletions(-) create mode 100644 java/client/src/main/java/glide/api/commands/SetCommands.java diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 3d513c512c..2e6a1a9241 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -4,9 +4,14 @@ 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.SAdd; +import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SMembers; +import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SetString; import glide.api.commands.ConnectionManagementCommands; +import glide.api.commands.SetCommands; import glide.api.commands.StringCommands; import glide.api.models.commands.SetOptions; import glide.api.models.configuration.BaseClientConfiguration; @@ -20,6 +25,7 @@ import glide.managers.BaseCommandResponseResolver; import glide.managers.CommandManager; import glide.managers.ConnectionManager; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.BiFunction; @@ -32,7 +38,7 @@ /** Base Client class for Redis */ @AllArgsConstructor public abstract class BaseClient - implements AutoCloseable, ConnectionManagementCommands, StringCommands { + implements AutoCloseable, ConnectionManagementCommands, StringCommands, SetCommands { /** Redis simple string response with "OK" */ public static final String OK = ConstantResponse.OK.toString(); @@ -149,10 +155,18 @@ protected String handleStringOrNullResponse(Response response) throws RedisExcep return handleRedisResponse(String.class, true, response); } + protected Long handleLongResponse(Response response) throws RedisException { + return handleRedisResponse(Long.class, false, response); + } + protected Object[] handleArrayResponse(Response response) { return handleRedisResponse(Object[].class, true, response); } + protected Set handleSetResponse(Response response) { + return handleRedisResponse(Set.class, false, response); + } + @Override public CompletableFuture ping() { return commandManager.submitNewCommand(Ping, new String[0], this::handleStringResponse); @@ -181,4 +195,26 @@ public CompletableFuture set( String[] arguments = ArrayUtils.addAll(new String[] {key, value}, options.toArgs()); return commandManager.submitNewCommand(SetString, arguments, this::handleStringOrNullResponse); } + + @Override + public CompletableFuture sadd(String key, String[] members) { + String[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand(SAdd, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture srem(String key, String[] members) { + String[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand(SRem, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture> smembers(String key) { + return commandManager.submitNewCommand(SMembers, new String[] {key}, this::handleSetResponse); + } + + @Override + public CompletableFuture scard(String key) { + return commandManager.submitNewCommand(SCard, new String[] {key}, this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/SetCommands.java b/java/client/src/main/java/glide/api/commands/SetCommands.java new file mode 100644 index 0000000000..2a7644db5a --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/SetCommands.java @@ -0,0 +1,89 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +/** + * Set Commands interface. + * + * @see Set Commands + */ +public interface SetCommands { + /** + * Add specified members to the set stored at key. Specified members that are already + * a member of this set are ignored. + * + * @see redis.io for details. + * @param key The key where members will be added to its set. + * @param members A list of members to add to the set stored at key. + * @return The number of members that were added to the set, excluding members already present. + * @remarks + *
    + *
  • If key does not exist, a new set is created before adding members + * . + *
  • If key holds a value that is not a set, an error is returned. + *
+ * + * @example + *

+ * int result = client.sadd("my_set", new String[]{"member1", "member2"}).get(); + * + */ + CompletableFuture sadd(String key, String[] members); + + /** + * Remove specified members from the set stored at key. Specified members that are + * not a member of this set are ignored. + * + * @see redis.io for details. + * @param key The key from which members will be removed. + * @param members A list of members to remove from the set stored at key. + * @return The number of members that were removed from the set, excluding non-existing members. + * @remarks + *

    + *
  • If key does not exist, it is treated as an empty set and this command + * returns 0. + *
  • If key holds a value that is not a set, an error is returned. + *
+ * + * @example + *

+ * int result = client.srem("my_set", new String[]{"member1", "member2"}).get(); + * + */ + CompletableFuture srem(String key, String[] members); + + /** + * Retrieve all the members of the set value stored at key. + * + * @see redis.io for details. + * @param key The key from which to retrieve the set members. + * @return A Set of all members of the set. + * @remarks + *

    + *
  • If key does not exist an empty set will be returned. + *
  • If key holds a value that is not a set, an error is returned. + *
+ * + * @example + *

+ * {@literal Set} result = client.smembers("my_set").get(); + * + */ + CompletableFuture> smembers(String key); + + /** + * Retrieve the set cardinality (number of elements) of the set stored at key. + * + * @see redis.io for details. + * @param key The key from which to retrieve the number of set members. + * @return The cardinality (number of elements) of the set, or 0 if the key does not exist. + * @remarks If key holds a value that is not a set, an error is returned. + * @example + *

+ * int result = client.scard("my_set").get(); + * + */ + CompletableFuture scard(String key); +} diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 92248e5047..ca7c7f8c96 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -5,6 +5,10 @@ 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.SAdd; +import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SMembers; +import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SetString; import glide.api.models.commands.InfoOptions; @@ -165,6 +169,87 @@ public T set(String key, String value, SetOptions options) { return getThis(); } + /** + * Add specified members to the set stored at key. Specified members that are already + * a member of this set are ignored. + * + * @see redis.io for details. + * @param key The key where members will be added to its set. + * @param members A list of members to add to the set stored at key. + * @return Command Response - The number of members that were added to the set, excluding members + * already present. + * @remarks + *

    + *
  • If key does not exist, a new set is created before adding members + * . + *
  • If key holds a value that is not a set, the transaction fails. + *
+ */ + public T sadd(String key, String[] members) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); + + protobufTransaction.addCommands(buildCommand(SAdd, commandArgs)); + return getThis(); + } + + /** + * Remove specified members from the set stored at key. Specified members that are + * not a member of this set are ignored. + * + * @see redis.io for details. + * @param key The key from which members will be removed. + * @param members A list of members to remove from the set stored at key. + * @return Command Response - The number of members that were removed from the set, excluding + * non-existing members. + * @remarks + *
    + *
  • If key does not exist, it is treated as an empty set and this command + * returns 0. + *
  • If key holds a value that is not a set, the transaction fails. + *
+ */ + public T srem(String key, String[] members) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); + + protobufTransaction.addCommands(buildCommand(SRem, commandArgs)); + return getThis(); + } + + /** + * Retrieve all the members of the set value stored at key. + * + * @see redis.io for details. + * @param key The key from which to retrieve the set members. + * @return Command Response - A Set of all members of the set. + * @remarks + *
    + *
  • If key does not exist an empty set will be returned. + *
  • If key holds a value that is not a set, the transaction fails. + *
+ */ + public T smembers(String key) { + ArgsArray commandArgs = buildArgs(key); + + protobufTransaction.addCommands(buildCommand(SMembers, commandArgs)); + return getThis(); + } + + /** + * Retrieve the set cardinality (number of elements) of the set stored at key. + * + * @see redis.io for details. + * @param key The key from which to retrieve the number of set members. + * @return Command Response - The cardinality (number of elements) of the set, or 0 if the key + * does not exist. + * @remarks If key holds a value that is not a set, the transaction fails. + */ + public T scard(String key) { + ArgsArray commandArgs = buildArgs(key); + + protobufTransaction.addCommands(buildCommand(SCard, commandArgs)); + return getThis(); + } + /** Build protobuf {@link Command} object for given command and arguments. */ protected Command buildCommand(RequestType requestType) { return buildCommand(requestType, buildArgs()); diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index a77111b082..29405685dd 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -15,6 +15,10 @@ 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.SAdd; +import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SMembers; +import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SetString; import glide.api.models.commands.InfoOptions; @@ -22,8 +26,10 @@ import glide.api.models.commands.SetOptions.Expiry; import glide.managers.CommandManager; import glide.managers.ConnectionManager; +import java.util.Set; import java.util.concurrent.CompletableFuture; import lombok.SneakyThrows; +import org.apache.commons.lang3.ArrayUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -267,4 +273,100 @@ public void info_with_empty_InfoOptions_returns_success() { assertEquals(testResponse, response); assertEquals(testPayload, payload); } + + @SneakyThrows + @Test + public void sadd_returns_success() { + // setup + String key = "testKey"; + String[] members = new String[] {"testMember1", "testMember2"}; + String[] arguments = ArrayUtils.addFirst(members, key); + Long value = 2L; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sadd(key, members); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void srem_returns_success() { + // setup + String key = "testKey"; + String[] members = new String[] {"testMember1", "testMember2"}; + String[] arguments = ArrayUtils.addFirst(members, key); + Long value = 2L; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SRem), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.srem(key, members); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void smembers_returns_success() { + // setup + String key = "testKey"; + Set value = Set.of("testMember"); + + CompletableFuture> testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SMembers), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.smembers(key); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scard_returns_success() { + // setup + String key = "testKey"; + Long value = 2L; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SCard), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scard(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } } diff --git a/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java index cbdcd4632c..745f5b124a 100644 --- a/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java +++ b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java @@ -6,6 +6,10 @@ 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.SAdd; +import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SMembers; +import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SetString; import glide.api.models.commands.InfoOptions; @@ -57,6 +61,18 @@ public void transaction_builds_protobuf_request() { Info, ArgsArray.newBuilder().addArgs(InfoOptions.Section.EVERYTHING.toString()).build())); + transaction.sadd("key", new String[] {"value"}); + results.add(Pair.of(SAdd, ArgsArray.newBuilder().addArgs("key").addArgs("value").build())); + + transaction.srem("key", new String[] {"value"}); + results.add(Pair.of(SRem, ArgsArray.newBuilder().addArgs("key").addArgs("value").build())); + + transaction.smembers("key"); + results.add(Pair.of(SMembers, ArgsArray.newBuilder().addArgs("key").build())); + + transaction.scard("key"); + results.add(Pair.of(SCard, ArgsArray.newBuilder().addArgs("key").build())); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java index 5cd4c52df4..591fcbd855 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -6,6 +6,10 @@ 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.SAdd; +import static redis_request.RedisRequestOuterClass.RequestType.SCard; +import static redis_request.RedisRequestOuterClass.RequestType.SMembers; +import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SetString; import glide.api.models.commands.InfoOptions; @@ -56,6 +60,18 @@ public void transaction_builds_protobuf_request() { Info, ArgsArray.newBuilder().addArgs(InfoOptions.Section.EVERYTHING.toString()).build())); + transaction.sadd("key", new String[] {"value"}); + results.add(Pair.of(SAdd, ArgsArray.newBuilder().addArgs("key").addArgs("value").build())); + + transaction.srem("key", new String[] {"value"}); + results.add(Pair.of(SRem, ArgsArray.newBuilder().addArgs("key").addArgs("value").build())); + + transaction.smembers("key"); + results.add(Pair.of(SMembers, ArgsArray.newBuilder().addArgs("key").build())); + + transaction.scard("key"); + results.add(Pair.of(SCard, ArgsArray.newBuilder().addArgs("key").build())); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 4f33ca36db..0bd737e7d3 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import glide.api.BaseClient; import glide.api.RedisClient; @@ -18,7 +19,11 @@ import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClientConfiguration; import glide.api.models.configuration.RedisClusterClientConfiguration; +import glide.api.models.exceptions.RequestException; import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; import lombok.Getter; import lombok.SneakyThrows; import org.junit.jupiter.api.AfterAll; @@ -247,4 +252,65 @@ public void set_missing_value_and_returnOldValue_is_null(BaseClient client) { String data = client.set("another", ANOTHER_VALUE, options).get(); assertNull(data); } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void sadd_srem_scard_smembers_existing_set(BaseClient client) { + String key = UUID.randomUUID().toString(); + assertEquals( + 4, client.sadd(key, new String[] {"member1", "member2", "member3", "member4"}).get()); + assertEquals(1, client.srem(key, new String[] {"member3", "nonExistingMember"}).get()); + + Set expectedMembers = Set.of("member1", "member2", "member4"); + assertEquals(expectedMembers, client.smembers(key).get()); + assertEquals(1, client.srem(key, new String[] {"member1"}).get()); + assertEquals(2, client.scard(key).get()); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void srem_scard_smembers_non_existing_key(BaseClient client) { + assertEquals(0, client.srem("nonExistingKey", new String[] {"member"}).get()); + assertEquals(0, client.scard("nonExistingKey").get()); + assertEquals(Set.of(), client.smembers("nonExistingKey").get()); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void sadd_srem_scard_smembers_key_with_non_set_value(BaseClient client) { + String key = UUID.randomUUID().toString(); + assertEquals(OK, client.set(key, "foo").get()); + + Exception e = + assertThrows(ExecutionException.class, () -> client.sadd(key, new String[] {"baz"}).get()); + assertTrue(e.getCause() instanceof RequestException); + assertTrue( + e.getCause() + .getMessage() + .contains("Operation against a key holding the wrong kind of value")); + + e = assertThrows(ExecutionException.class, () -> client.srem(key, new String[] {"baz"}).get()); + assertTrue(e.getCause() instanceof RequestException); + assertTrue( + e.getCause() + .getMessage() + .contains("Operation against a key holding the wrong kind of value")); + + e = assertThrows(ExecutionException.class, () -> client.scard(key).get()); + assertTrue(e.getCause() instanceof RequestException); + assertTrue( + e.getCause() + .getMessage() + .contains("Operation against a key holding the wrong kind of value")); + + e = assertThrows(ExecutionException.class, () -> client.smembers(key).get()); + assertTrue(e.getCause() instanceof RequestException); + assertTrue( + e.getCause() + .getMessage() + .contains("Operation against a key holding the wrong kind of value")); + } } diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java index 2b01067cc2..f891dc757b 100644 --- a/java/integTest/src/test/java/glide/TestUtilities.java +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -3,6 +3,7 @@ import glide.api.models.BaseTransaction; import glide.api.models.commands.SetOptions; +import java.util.Set; import java.util.UUID; public class TestUtilities { @@ -10,15 +11,21 @@ public class TestUtilities { public static BaseTransaction transactionTest(BaseTransaction baseTransaction) { String key1 = "{key}" + UUID.randomUUID(); String key2 = "{key}" + UUID.randomUUID(); + String key3 = "{key}" + UUID.randomUUID(); baseTransaction.set(key1, "bar"); baseTransaction.set(key2, "baz", SetOptions.builder().returnOldValue(true).build()); baseTransaction.customCommand("MGET", key1, key2); + baseTransaction.sadd(key3, new String[] {"baz", "foo"}); + baseTransaction.srem(key3, new String[] {"foo"}); + baseTransaction.scard(key3); + baseTransaction.smembers(key3); + return baseTransaction; } public static Object[] transactionTestResult() { - return new Object[] {"OK", null, new String[] {"bar", "baz"}}; + return new Object[] {"OK", null, new String[] {"bar", "baz"}, 2L, 1L, 1L, Set.of("baz")}; } } From 032c1b01bf4b012932c3a9e3f62b731355bd9af1 Mon Sep 17 00:00:00 2001 From: aaron-congo Date: Thu, 15 Feb 2024 12:07:03 -0800 Subject: [PATCH 20/22] PR suggestions --- .../java/glide/api/commands/SetCommands.java | 26 ++++++------------- .../glide/api/models/BaseTransaction.java | 21 +++------------ .../test/java/glide/SharedCommandTests.java | 16 ------------ 3 files changed, 11 insertions(+), 52 deletions(-) diff --git a/java/client/src/main/java/glide/api/commands/SetCommands.java b/java/client/src/main/java/glide/api/commands/SetCommands.java index 2a7644db5a..9f936969e9 100644 --- a/java/client/src/main/java/glide/api/commands/SetCommands.java +++ b/java/client/src/main/java/glide/api/commands/SetCommands.java @@ -18,16 +18,12 @@ public interface SetCommands { * @param key The key where members will be added to its set. * @param members A list of members to add to the set stored at key. * @return The number of members that were added to the set, excluding members already present. - * @remarks - *
    - *
  • If key does not exist, a new set is created before adding members - * . - *
  • If key holds a value that is not a set, an error is returned. - *
+ * @remarks If key does not exist, a new set is created before adding members. * * @example *

* int result = client.sadd("my_set", new String[]{"member1", "member2"}).get(); + * // result: 2 * */ CompletableFuture sadd(String key, String[] members); @@ -40,16 +36,12 @@ public interface SetCommands { * @param key The key from which members will be removed. * @param members A list of members to remove from the set stored at key. * @return The number of members that were removed from the set, excluding non-existing members. - * @remarks - *

    - *
  • If key does not exist, it is treated as an empty set and this command - * returns 0. - *
  • If key holds a value that is not a set, an error is returned. - *
+ * @remarks If key does not exist, it is treated as an empty set and this command returns 0. * * @example *

* int result = client.srem("my_set", new String[]{"member1", "member2"}).get(); + * // result: 2 * */ CompletableFuture srem(String key, String[] members); @@ -60,15 +52,12 @@ public interface SetCommands { * @see redis.io for details. * @param key The key from which to retrieve the set members. * @return A Set of all members of the set. - * @remarks - *

    - *
  • If key does not exist an empty set will be returned. - *
  • If key holds a value that is not a set, an error is returned. - *
+ * @remarks If key does not exist an empty set will be returned. * * @example *

* {@literal Set} result = client.smembers("my_set").get(); + * // result: {"member1", "member2", "member3"} * */ CompletableFuture> smembers(String key); @@ -79,10 +68,11 @@ public interface SetCommands { * @see redis.io for details. * @param key The key from which to retrieve the number of set members. * @return The cardinality (number of elements) of the set, or 0 if the key does not exist. - * @remarks If key holds a value that is not a set, an error is returned. + * * @example *

* int result = client.scard("my_set").get(); + * // result: 3 * */ CompletableFuture scard(String key); diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index ca7c7f8c96..4019ccc6a7 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -178,12 +178,7 @@ public T set(String key, String value, SetOptions options) { * @param members A list of members to add to the set stored at key. * @return Command Response - The number of members that were added to the set, excluding members * already present. - * @remarks - *

    - *
  • If key does not exist, a new set is created before adding members - * . - *
  • If key holds a value that is not a set, the transaction fails. - *
+ * @remarks If key does not exist, a new set is created before adding members. */ public T sadd(String key, String[] members) { ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); @@ -201,12 +196,7 @@ public T sadd(String key, String[] members) { * @param members A list of members to remove from the set stored at key. * @return Command Response - The number of members that were removed from the set, excluding * non-existing members. - * @remarks - *
    - *
  • If key does not exist, it is treated as an empty set and this command - * returns 0. - *
  • If key holds a value that is not a set, the transaction fails. - *
+ * @remarks If key does not exist, it is treated as an empty set and this command returns 0. */ public T srem(String key, String[] members) { ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); @@ -221,11 +211,7 @@ public T srem(String key, String[] members) { * @see redis.io for details. * @param key The key from which to retrieve the set members. * @return Command Response - A Set of all members of the set. - * @remarks - *
    - *
  • If key does not exist an empty set will be returned. - *
  • If key holds a value that is not a set, the transaction fails. - *
+ * @remarks If key does not exist an empty set will be returned. */ public T smembers(String key) { ArgsArray commandArgs = buildArgs(key); @@ -241,7 +227,6 @@ public T smembers(String key) { * @param key The key from which to retrieve the number of set members. * @return Command Response - The cardinality (number of elements) of the set, or 0 if the key * does not exist. - * @remarks If key holds a value that is not a set, the transaction fails. */ public T scard(String key) { ArgsArray commandArgs = buildArgs(key); diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 0bd737e7d3..4bddc118e1 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -287,30 +287,14 @@ public void sadd_srem_scard_smembers_key_with_non_set_value(BaseClient client) { Exception e = assertThrows(ExecutionException.class, () -> client.sadd(key, new String[] {"baz"}).get()); assertTrue(e.getCause() instanceof RequestException); - assertTrue( - e.getCause() - .getMessage() - .contains("Operation against a key holding the wrong kind of value")); e = assertThrows(ExecutionException.class, () -> client.srem(key, new String[] {"baz"}).get()); assertTrue(e.getCause() instanceof RequestException); - assertTrue( - e.getCause() - .getMessage() - .contains("Operation against a key holding the wrong kind of value")); e = assertThrows(ExecutionException.class, () -> client.scard(key).get()); assertTrue(e.getCause() instanceof RequestException); - assertTrue( - e.getCause() - .getMessage() - .contains("Operation against a key holding the wrong kind of value")); e = assertThrows(ExecutionException.class, () -> client.smembers(key).get()); assertTrue(e.getCause() instanceof RequestException); - assertTrue( - e.getCause() - .getMessage() - .contains("Operation against a key holding the wrong kind of value")); } } From a54b67d3770e2e0c1de387965ca8880e81163800 Mon Sep 17 00:00:00 2001 From: aaron-congo Date: Thu, 15 Feb 2024 12:12:54 -0800 Subject: [PATCH 21/22] Fix spotless --- .../src/main/java/glide/api/commands/SetCommands.java | 10 ++++------ .../main/java/glide/api/models/BaseTransaction.java | 6 ++++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/java/client/src/main/java/glide/api/commands/SetCommands.java b/java/client/src/main/java/glide/api/commands/SetCommands.java index 9f936969e9..f2098c9b08 100644 --- a/java/client/src/main/java/glide/api/commands/SetCommands.java +++ b/java/client/src/main/java/glide/api/commands/SetCommands.java @@ -18,8 +18,8 @@ public interface SetCommands { * @param key The key where members will be added to its set. * @param members A list of members to add to the set stored at key. * @return The number of members that were added to the set, excluding members already present. - * @remarks If key does not exist, a new set is created before adding members. - * + * @remarks If key does not exist, a new set is created before adding members + * . * @example *

* int result = client.sadd("my_set", new String[]{"member1", "member2"}).get(); @@ -36,8 +36,8 @@ public interface SetCommands { * @param key The key from which members will be removed. * @param members A list of members to remove from the set stored at key. * @return The number of members that were removed from the set, excluding non-existing members. - * @remarks If key does not exist, it is treated as an empty set and this command returns 0. - * + * @remarks If key does not exist, it is treated as an empty set and this command + * returns 0. * @example *

* int result = client.srem("my_set", new String[]{"member1", "member2"}).get(); @@ -53,7 +53,6 @@ public interface SetCommands { * @param key The key from which to retrieve the set members. * @return A Set of all members of the set. * @remarks If key does not exist an empty set will be returned. - * * @example *

* {@literal Set} result = client.smembers("my_set").get(); @@ -68,7 +67,6 @@ public interface SetCommands { * @see redis.io for details. * @param key The key from which to retrieve the number of set members. * @return The cardinality (number of elements) of the set, or 0 if the key does not exist. - * * @example *

* int result = client.scard("my_set").get(); diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 4019ccc6a7..6d40cc5ce2 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -178,7 +178,8 @@ public T set(String key, String value, SetOptions options) { * @param members A list of members to add to the set stored at key. * @return Command Response - The number of members that were added to the set, excluding members * already present. - * @remarks If key does not exist, a new set is created before adding members. + * @remarks If key does not exist, a new set is created before adding members + * . */ public T sadd(String key, String[] members) { ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); @@ -196,7 +197,8 @@ public T sadd(String key, String[] members) { * @param members A list of members to remove from the set stored at key. * @return Command Response - The number of members that were removed from the set, excluding * non-existing members. - * @remarks If key does not exist, it is treated as an empty set and this command returns 0. + * @remarks If key does not exist, it is treated as an empty set and this command + * returns 0. */ public T srem(String key, String[] members) { ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); From 26e7a0b74272ee34a7c219c0cd12c597abd6ec2b Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Fri, 16 Feb 2024 00:18:44 +0200 Subject: [PATCH 22/22] Improve readability of transaction test (#973) --- node/tests/TestUtilities.ts | 140 ++++++++++++------------ python/python/tests/test_transaction.py | 98 ++++++++--------- 2 files changed, 121 insertions(+), 117 deletions(-) diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 92f91bd17d..82fa33976f 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -63,75 +63,79 @@ export function transactionTest( const key8 = "{key}" + uuidv4(); const field = uuidv4(); const value = uuidv4(); - baseTransaction - .set(key1, "bar") - .set(key2, "baz", { - returnOldValue: true, - }) - .customCommand(["MGET", key1, key2]) - .mset({ [key3]: value }) - .mget([key1, key2]) - .del([key1]) - .hset(key4, { [field]: value }) - .hget(key4, field) - .hgetall(key4) - .hdel(key4, [field]) - .hmget(key4, [field]) - .hexists(key4, field) - .lpush(key5, [field + "1", field + "2", field + "3", field + "4"]) - .lpop(key5) - .llen(key5) - .lrem(key5, 1, field + "1") - .ltrim(key5, 0, 1) - .lrange(key5, 0, -1) - .lpopCount(key5, 2) - .rpush(key6, [field + "1", field + "2", field + "3"]) - .rpop(key6) - .rpopCount(key6, 2) - .sadd(key7, ["bar", "foo"]) - .srem(key7, ["foo"]) - .scard(key7) - .smembers(key7) - .zadd(key8, { member1: 1, member2: 2 }) - .zaddIncr(key8, "member2", 1) - .zrem(key8, ["member1"]) - .zcard(key8) - .zscore(key8, "member2") - .zcount(key8, { bound: 2 }, "positiveInfinity"); - return [ - "OK", - null, - ["bar", "baz"], - "OK", - ["bar", "baz"], - 1, - 1, - value, - { [field]: value }, - 1, - [null], - false, - 4, - field + "4", - 3, - 1, - "OK", - [field + "3", field + "2"], - [field + "3", field + "2"], - 3, + const args: ReturnType[] = []; + baseTransaction.set(key1, "bar"); + args.push("OK"); + baseTransaction.set(key2, "baz", { + returnOldValue: true, + }); + args.push(null); + baseTransaction.customCommand(["MGET", key1, key2]); + args.push(["bar", "baz"]); + baseTransaction.mset({ [key3]: value }); + args.push("OK"); + baseTransaction.mget([key1, key2]); + args.push(["bar", "baz"]); + baseTransaction.del([key1]); + args.push(1); + baseTransaction.hset(key4, { [field]: value }); + args.push(1); + baseTransaction.hget(key4, field); + args.push(value); + baseTransaction.hgetall(key4); + args.push({ [field]: value }); + baseTransaction.hdel(key4, [field]); + args.push(1); + baseTransaction.hmget(key4, [field]); + args.push([null]); + baseTransaction.hexists(key4, field); + args.push(false); + baseTransaction.lpush(key5, [ + field + "1", + field + "2", field + "3", - [field + "2", field + "1"], - 2, - 1, - 1, - ["bar"], - 2, - 3, - 1, - 1, - 3.0, - 1, - ]; + field + "4", + ]); + args.push(4); + baseTransaction.lpop(key5); + args.push(field + "4"); + baseTransaction.llen(key5); + args.push(3); + baseTransaction.lrem(key5, 1, field + "1"); + args.push(1); + baseTransaction.ltrim(key5, 0, 1); + args.push("OK"); + baseTransaction.lrange(key5, 0, -1); + args.push([field + "3", field + "2"]); + baseTransaction.lpopCount(key5, 2); + args.push([field + "3", field + "2"]); + baseTransaction.rpush(key6, [field + "1", field + "2", field + "3"]); + args.push(3); + baseTransaction.rpop(key6); + args.push(field + "3"); + baseTransaction.rpopCount(key6, 2); + args.push([field + "2", field + "1"]); + baseTransaction.sadd(key7, ["bar", "foo"]); + args.push(2); + baseTransaction.srem(key7, ["foo"]); + args.push(1); + baseTransaction.scard(key7); + args.push(1); + baseTransaction.smembers(key7); + args.push(["bar"]); + baseTransaction.zadd(key8, { member1: 1, member2: 2 }); + args.push(2); + baseTransaction.zaddIncr(key8, "member2", 1); + args.push(3); + baseTransaction.zrem(key8, ["member1"]); + args.push(1); + baseTransaction.zcard(key8); + args.push(1); + baseTransaction.zscore(key8, "member2"); + args.push(3.0); + baseTransaction.zcount(key8, { bound: 2 }, "positiveInfinity"); + args.push(1); + return args; } export class RedisCluster { diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 35bd6b68c2..2a73b145d1 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -32,120 +32,120 @@ def transaction_test( value = datetime.now(timezone.utc).strftime("%m/%d/%Y, %H:%M:%S") value2 = get_random_string(5) + args: List[TResult] = [] transaction.set(key, value) + args.append(OK) transaction.get(key) + args.append(value) transaction.type(key) + args.append("string") transaction.exists([key]) + args.append(1) transaction.delete([key]) + args.append(1) transaction.get(key) + args.append(None) transaction.mset({key: value, key2: value2}) + args.append(OK) transaction.mget([key, key2]) + args.append([value, value2]) transaction.incr(key3) + args.append(1) transaction.incrby(key3, 2) + args.append(3) transaction.decr(key3) + args.append(2) transaction.decrby(key3, 2) + args.append(0) transaction.incrbyfloat(key3, 0.5) + args.append(0.5) transaction.unlink([key3]) + args.append(1) transaction.ping() + args.append("PONG") transaction.config_set({"timeout": "1000"}) + args.append(OK) transaction.config_get(["timeout"]) + args.append({"timeout": "1000"}) transaction.hset(key4, {key: value, key2: value2}) + args.append(2) transaction.hget(key4, key2) + args.append(value2) transaction.hlen(key4) + args.append(2) transaction.hincrby(key4, key3, 5) + args.append(5) transaction.hincrbyfloat(key4, key3, 5.5) + args.append(10.5) transaction.hexists(key4, key) + args.append(True) transaction.hmget(key4, [key, "nonExistingField", key2]) + args.append([value, None, value2]) transaction.hgetall(key4) + args.append({key: value, key2: value2, key3: "10.5"}) transaction.hdel(key4, [key, key2]) + args.append(2) transaction.client_getname() + args.append(None) transaction.lpush(key5, [value, value, value2, value2]) + args.append(4) transaction.llen(key5) + args.append(4) transaction.lpop(key5) + args.append(value2) transaction.lrem(key5, 1, value) + args.append(1) transaction.ltrim(key5, 0, 1) + args.append(OK) transaction.lrange(key5, 0, -1) + args.append([value2, value]) transaction.lpop_count(key5, 2) + args.append([value2, value]) transaction.rpush(key6, [value, value2, value2]) + args.append(3) transaction.rpop(key6) + args.append(value2) transaction.rpop_count(key6, 2) + args.append([value2, value]) transaction.sadd(key7, ["foo", "bar"]) + args.append(2) transaction.srem(key7, ["foo"]) + args.append(1) transaction.smembers(key7) + args.append({"bar"}) transaction.scard(key7) + args.append(1) transaction.zadd(key8, {"one": 1, "two": 2, "three": 3}) + args.append(3) transaction.zadd_incr(key8, "one", 3) + args.append(4) transaction.zrem(key8, ["one"]) + args.append(1) transaction.zcard(key8) + args.append(2) transaction.zcount(key8, ScoreLimit(2, True), InfBound.POS_INF) + args.append(2) transaction.zscore(key8, "two") - return [ - OK, - value, - "string", - 1, - 1, - None, - OK, - [value, value2], - 1, - 3, - 2, - 0, - 0.5, - 1, - "PONG", - OK, - {"timeout": "1000"}, - 2, - value2, - 2, - 5, - 10.5, - True, - [value, None, value2], - {key: value, key2: value2, key3: "10.5"}, - 2, - None, - 4, - 4, - value2, - 1, - OK, - [value2, value], - [value2, value], - 3, - value2, - [value2, value], - 2, - 1, - {"bar"}, - 1, - 3, - 4, - 1, - 2, - 2, - 2.0, - ] + args.append(2.0) + return args @pytest.mark.asyncio