From a1facbffa01150661c4d55680720d2e71c3abae1 Mon Sep 17 00:00:00 2001 From: SanHalacogluImproving <144171266+SanHalacogluImproving@users.noreply.github.com> Date: Tue, 20 Feb 2024 09:20:53 -0800 Subject: [PATCH] Java: Added Mget and Mset commands to BaseClient and BaseTransactions. (String Commands) (#955) * Added Mget and Mset commands. (#78) * Added Mget and Mset commands to BaseClient and BaseTransactions. * Added 2 util functions for code cleanup. Minor documentation refactor. * Added transaction tests. * Added documentation for CommandUtils. * Minor changes based on PR comments * Minor changes based on PR comments * Fix merge conflicts; spotless Signed-off-by: Andrew Carbonetto * Minor changes from PR comments. * Reorder commands Signed-off-by: Andrew Carbonetto * Spotless. --------- Signed-off-by: Andrew Carbonetto Co-authored-by: Andrew Carbonetto --- .../src/main/java/glide/api/BaseClient.java | 22 +++++++- .../src/main/java/glide/api/RedisClient.java | 2 +- .../glide/api/commands/StringCommands.java | 21 ++++++++ .../glide/api/models/BaseTransaction.java | 37 +++++++++++++ .../java/glide/utils/ArrayTransformUtils.java | 37 +++++++++++++ .../test/java/glide/api/RedisClientTest.java | 53 +++++++++++++++++++ .../glide/api/models/TransactionTests.java | 9 ++++ .../test/java/glide/SharedCommandTests.java | 19 +++++++ .../src/test/java/glide/TestUtilities.java | 31 ++++++++--- 9 files changed, 223 insertions(+), 8 deletions(-) create mode 100644 java/client/src/main/java/glide/utils/ArrayTransformUtils.java diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 8ad5a4ba55..20360ab13d 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -2,7 +2,11 @@ package glide.api; import static glide.ffi.resolvers.SocketListenerResolver.getSocket; +import static glide.utils.ArrayTransformUtils.castArray; +import static glide.utils.ArrayTransformUtils.convertMapToArgArray; import static redis_request.RedisRequestOuterClass.RequestType.GetString; +import static redis_request.RedisRequestOuterClass.RequestType.MGet; +import static redis_request.RedisRequestOuterClass.RequestType.MSet; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; import static redis_request.RedisRequestOuterClass.RequestType.SCard; @@ -157,13 +161,17 @@ protected Long handleLongResponse(Response response) throws RedisException { } protected Object[] handleArrayResponse(Response response) throws RedisException { + return handleRedisResponse(Object[].class, false, response); + } + + protected Object[] handleArrayOrNullResponse(Response response) throws RedisException { return handleRedisResponse(Object[].class, true, response); } /** * @param response A Protobuf response * @return A map of String to V - * @param Value type, could be even map too + * @param Value type could be even map too */ @SuppressWarnings("unchecked") // raw Map cast to Map protected Map handleMapResponse(Response response) throws RedisException { @@ -204,6 +212,18 @@ public CompletableFuture set( return commandManager.submitNewCommand(SetString, arguments, this::handleStringOrNullResponse); } + @Override + public CompletableFuture mget(@NonNull String[] keys) { + return commandManager.submitNewCommand( + MGet, keys, response -> castArray(handleArrayOrNullResponse(response), String.class)); + } + + @Override + public CompletableFuture mset(@NonNull Map keyValueMap) { + String[] args = convertMapToArgArray(keyValueMap); + return commandManager.submitNewCommand(MSet, args, this::handleStringResponse); + } + @Override public CompletableFuture sadd(String key, String[] members) { String[] arguments = ArrayUtils.addFirst(members, key); diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index c6dd4e10d4..94af58bd42 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -42,7 +42,7 @@ public CompletableFuture customCommand(@NonNull String[] args) { @Override public CompletableFuture exec(Transaction transaction) { - return commandManager.submitNewCommand(transaction, this::handleArrayResponse); + return commandManager.submitNewCommand(transaction, this::handleArrayOrNullResponse); } @Override diff --git a/java/client/src/main/java/glide/api/commands/StringCommands.java b/java/client/src/main/java/glide/api/commands/StringCommands.java index 8037a4682c..72d8b06104 100644 --- a/java/client/src/main/java/glide/api/commands/StringCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringCommands.java @@ -4,6 +4,7 @@ import glide.api.models.commands.SetOptions; import glide.api.models.commands.SetOptions.ConditionalSet; import glide.api.models.commands.SetOptions.SetOptionsBuilder; +import java.util.Map; import java.util.concurrent.CompletableFuture; /** @@ -48,4 +49,24 @@ public interface StringCommands { * is set, return the old value as a String. */ CompletableFuture set(String key, String value, SetOptions options); + + /** + * Retrieve the values of multiple keys. + * + * @see redis.io for details. + * @param keys A list of keys to retrieve values for. + * @return An array of values corresponding to the provided keys.
+ * If a keyis not found, its corresponding value in the list will be null + * . + */ + CompletableFuture mget(String[] keys); + + /** + * Set multiple keys to multiple values in a single operation. + * + * @see redis.io for details. + * @param keyValueMap A key-value map consisting of keys and their respective values to set. + * @return Always OK. + */ + CompletableFuture mset(Map keyValueMap); } 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 8721331ecc..ba59840859 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -1,9 +1,12 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.utils.ArrayTransformUtils.convertMapToArgArray; 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.MGet; +import static redis_request.RedisRequestOuterClass.RequestType.MSet; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; import static redis_request.RedisRequestOuterClass.RequestType.SCard; @@ -16,7 +19,9 @@ import glide.api.models.commands.SetOptions; import glide.api.models.commands.SetOptions.ConditionalSet; import glide.api.models.commands.SetOptions.SetOptionsBuilder; +import java.util.Map; import lombok.Getter; +import lombok.NonNull; import org.apache.commons.lang3.ArrayUtils; import redis_request.RedisRequestOuterClass.Command; import redis_request.RedisRequestOuterClass.Command.ArgsArray; @@ -168,6 +173,38 @@ public T set(String key, String value, SetOptions options) { return getThis(); } + /** + * Retrieve the values of multiple keys. + * + * @see redis.io for details. + * @param keys A list of keys to retrieve values for. + * @return Command Response - An array of values corresponding to the provided keys. + *
+ * If a keyis not found, its corresponding value in the list will be null + * . + */ + public T mget(@NonNull String[] keys) { + ArgsArray commandArgs = buildArgs(keys); + + protobufTransaction.addCommands(buildCommand(MGet, commandArgs)); + return getThis(); + } + + /** + * Set multiple keys to multiple values in a single operation. + * + * @see redis.io for details. + * @param keyValueMap A key-value map consisting of keys and their respective values to set. + * @return Command Response - Always OK. + */ + public T mset(@NonNull Map keyValueMap) { + String[] args = convertMapToArgArray(keyValueMap); + ArgsArray commandArgs = buildArgs(args); + + protobufTransaction.addCommands(buildCommand(MSet, commandArgs)); + return getThis(); + } + /** * Add specified members to the set stored at key. Specified members that are already * a member of this set are ignored. diff --git a/java/client/src/main/java/glide/utils/ArrayTransformUtils.java b/java/client/src/main/java/glide/utils/ArrayTransformUtils.java new file mode 100644 index 0000000000..091ee13a9b --- /dev/null +++ b/java/client/src/main/java/glide/utils/ArrayTransformUtils.java @@ -0,0 +1,37 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.utils; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Stream; + +public class ArrayTransformUtils { + + /** + * Converts a map to an array of strings with alternating keys and values. + * + * @param args Map of string pairs to convert. + * @return Array of strings [key1, value1, key2, value2, ...]. + */ + public static String[] convertMapToArgArray(Map args) { + return args.entrySet().stream() + .flatMap(entry -> Stream.of(entry.getKey(), entry.getValue())) + .toArray(String[]::new); + } + + /** + * Casts an array of objects to an array of type T. + * + * @param objectArr Array of objects to cast. + * @param clazz The class of the array elements to cast to. + * @return An array of type T, containing the elements from the input array. + * @param The type to which the elements are cast. + */ + @SuppressWarnings("unchecked") + public static U[] castArray(T[] objectArr, Class clazz) { + return Arrays.stream(objectArr) + .map(clazz::cast) + .toArray(size -> (U[]) Array.newInstance(clazz, size)); + } +} diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 29405685dd..77d4168002 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.BaseClient.OK; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; @@ -14,6 +15,8 @@ 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.MGet; +import static redis_request.RedisRequestOuterClass.RequestType.MSet; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; import static redis_request.RedisRequestOuterClass.RequestType.SCard; @@ -26,6 +29,8 @@ import glide.api.models.commands.SetOptions.Expiry; import glide.managers.CommandManager; import glide.managers.ConnectionManager; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import lombok.SneakyThrows; @@ -274,6 +279,54 @@ public void info_with_empty_InfoOptions_returns_success() { assertEquals(testPayload, payload); } + @SneakyThrows + @Test + public void mget_returns_success() { + // setup + String[] keys = {"key1", null, "key2"}; + String[] values = {"value1", null, "value2"}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(values); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(MGet), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.mget(keys); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(values, payload); + } + + @SneakyThrows + @Test + public void mset_returns_success() { + // setup + Map keyValueMap = new LinkedHashMap<>(); + keyValueMap.put("key1", "value1"); + keyValueMap.put("key2", "value2"); + String[] args = {"key1", "value1", "key2", "value2"}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(MSet), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.mset(keyValueMap); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + @SneakyThrows @Test public void sadd_returns_success() { 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 591fcbd855..6f2d070319 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -5,6 +5,8 @@ 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.MGet; +import static redis_request.RedisRequestOuterClass.RequestType.MSet; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; import static redis_request.RedisRequestOuterClass.RequestType.SCard; @@ -16,6 +18,7 @@ import glide.api.models.commands.SetOptions; import java.util.LinkedList; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.Test; import redis_request.RedisRequestOuterClass.Command; @@ -60,6 +63,12 @@ public void transaction_builds_protobuf_request() { Info, ArgsArray.newBuilder().addArgs(InfoOptions.Section.EVERYTHING.toString()).build())); + transaction.mset(Map.of("key", "value")); + results.add(Pair.of(MSet, ArgsArray.newBuilder().addArgs("key").addArgs("value").build())); + + transaction.mget(new String[] {"key"}); + results.add(Pair.of(MGet, ArgsArray.newBuilder().addArgs("key").build())); + transaction.sadd("key", new String[] {"value"}); results.add(Pair.of(SAdd, ArgsArray.newBuilder().addArgs("key").addArgs("value").build())); diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 4bddc118e1..dc7335ed14 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -7,6 +7,7 @@ import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; import static glide.api.models.commands.SetOptions.Expiry.Milliseconds; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -21,6 +22,7 @@ import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.exceptions.RequestException; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -253,6 +255,23 @@ public void set_missing_value_and_returnOldValue_is_null(BaseClient client) { assertNull(data); } + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void mset_mget_existing_non_existing_key(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String key3 = UUID.randomUUID().toString(); + String nonExisting = UUID.randomUUID().toString(); + String value = UUID.randomUUID().toString(); + Map keyValueMap = Map.of(key1, value, key2, value, key3, value); + + assertEquals(OK, client.mset(keyValueMap).get()); + assertArrayEquals( + new String[] {value, value, null, value}, + client.mget(new String[] {key1, key2, nonExisting, key3}).get()); + } + @SneakyThrows @ParameterizedTest @MethodSource("getClients") diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java index f891dc757b..ee4d17472f 100644 --- a/java/integTest/src/test/java/glide/TestUtilities.java +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -3,20 +3,28 @@ import glide.api.models.BaseTransaction; import glide.api.models.commands.SetOptions; +import java.util.Map; import java.util.Set; import java.util.UUID; public class TestUtilities { + private static final String key1 = "{key}" + UUID.randomUUID(); + private static final String key2 = "{key}" + UUID.randomUUID(); + private static final String key3 = "{key}" + UUID.randomUUID(); + private static final String value1 = "{value}" + UUID.randomUUID(); + private static final String value2 = "{value}" + UUID.randomUUID(); 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.set(key1, value1); + baseTransaction.get(key1); + + baseTransaction.set(key2, value2, SetOptions.builder().returnOldValue(true).build()); baseTransaction.customCommand("MGET", key1, key2); + baseTransaction.mset(Map.of(key1, value2, key2, value1)); + baseTransaction.mget(new String[] {key1, key2}); + baseTransaction.sadd(key3, new String[] {"baz", "foo"}); baseTransaction.srem(key3, new String[] {"foo"}); baseTransaction.scard(key3); @@ -26,6 +34,17 @@ public static BaseTransaction transactionTest(BaseTransaction baseTransaction) { } public static Object[] transactionTestResult() { - return new Object[] {"OK", null, new String[] {"bar", "baz"}, 2L, 1L, 1L, Set.of("baz")}; + return new Object[] { + "OK", + value1, + null, + new String[] {value1, value2}, + "OK", + new String[] {value2, value1}, + 2L, + 1L, + 1L, + Set.of("baz"), + }; } }