From 8f35f5a3baf8095beb58f69307e96f11b32ac9fc Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Fri, 9 Feb 2024 12:01:47 -0800 Subject: [PATCH] Java: Add GET & SET commands (#919) * Update commandmanager to remove optional argument Signed-off-by: Andrew Carbonetto * Java: Add GET & SET commands Signed-off-by: Andrew Carbonetto * Move IT tests to SharedCommandTests Signed-off-by: Andrew Carbonetto * Spotless Signed-off-by: Andrew Carbonetto * Add nonnull check to API Signed-off-by: Andrew Carbonetto * Fix from merge Signed-off-by: Andrew Carbonetto * Update handleRedisResponse Signed-off-by: Andrew Carbonetto * Use @Timeout annotation Signed-off-by: Andrew Carbonetto * Remove extra @nonnull annotations Signed-off-by: Andrew Carbonetto * Merge main Signed-off-by: Andrew Carbonetto * Add NonNull Signed-off-by: Andrew Carbonetto * Add back IT tests Signed-off-by: Andrew Carbonetto * Remove extra tests Signed-off-by: Andrew Carbonetto * Add DELME tests Signed-off-by: Andrew Carbonetto * Minor: clean tests; throw exception Signed-off-by: Andrew Carbonetto --------- Signed-off-by: Andrew Carbonetto --- .../src/main/java/glide/api/BaseClient.java | 39 +++- .../glide/api/commands/StringCommands.java | 51 +++++ .../glide/api/models/commands/SetOptions.java | 171 +++++++++++++++++ .../managers/BaseCommandResponseResolver.java | 5 +- .../test/java/glide/api/RedisClientTest.java | 105 +++++++++++ .../test/java/glide/SharedCommandTests.java | 175 ++++++++++++++++++ .../test/java/glide/cluster/CommandTests.java | 14 ++ .../java/glide/standalone/CommandTests.java | 16 ++ 8 files changed, 571 insertions(+), 5 deletions(-) create mode 100644 java/client/src/main/java/glide/api/commands/StringCommands.java create mode 100644 java/client/src/main/java/glide/api/models/commands/SetOptions.java diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 6acc5da67b..b73a231bc3 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -2,9 +2,13 @@ package glide.api; import static glide.ffi.resolvers.SocketListenerResolver.getSocket; +import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.Ping; +import static redis_request.RedisRequestOuterClass.RequestType.SetString; import glide.api.commands.ConnectionManagementCommands; +import glide.api.commands.StringCommands; +import glide.api.models.commands.SetOptions; import glide.api.models.configuration.BaseClientConfiguration; import glide.api.models.exceptions.RedisException; import glide.connectors.handlers.CallbackDispatcher; @@ -21,11 +25,17 @@ import java.util.function.BiFunction; import lombok.AllArgsConstructor; import lombok.NonNull; +import org.apache.commons.lang3.ArrayUtils; +import response.ResponseOuterClass.ConstantResponse; import response.ResponseOuterClass.Response; /** Base Client class for Redis */ @AllArgsConstructor -public abstract class BaseClient implements AutoCloseable, ConnectionManagementCommands { +public abstract class BaseClient + implements AutoCloseable, ConnectionManagementCommands, StringCommands { + /** Redis simple string response with "OK" */ + public static final String OK = ConstantResponse.OK.toString(); + protected final ConnectionManager connectionManager; protected final CommandManager commandManager; @@ -123,14 +133,18 @@ private T handleRedisResponse(Class classType, boolean isNullable, Respon + classType.getSimpleName()); } - protected Object handleObjectOrNullResponse(Response response) { + protected Object handleObjectOrNullResponse(Response response) throws RedisException { return handleRedisResponse(Object.class, true, response); } - protected String handleStringResponse(Response response) { + protected String handleStringResponse(Response response) throws RedisException { return handleRedisResponse(String.class, false, response); } + protected String handleStringOrNullResponse(Response response) throws RedisException { + return handleRedisResponse(String.class, true, response); + } + @Override public CompletableFuture ping() { return commandManager.submitNewCommand(Ping, new String[0], this::handleStringResponse); @@ -140,4 +154,23 @@ public CompletableFuture ping() { public CompletableFuture ping(@NonNull String str) { return commandManager.submitNewCommand(Ping, new String[] {str}, this::handleStringResponse); } + + @Override + public CompletableFuture get(@NonNull String key) { + return commandManager.submitNewCommand( + GetString, new String[] {key}, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture set(@NonNull String key, @NonNull String value) { + return commandManager.submitNewCommand( + SetString, new String[] {key, value}, this::handleStringResponse); + } + + @Override + public CompletableFuture set( + @NonNull String key, @NonNull String value, @NonNull SetOptions options) { + String[] arguments = ArrayUtils.addAll(new String[] {key, value}, options.toArgs()); + return commandManager.submitNewCommand(SetString, arguments, this::handleStringOrNullResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/StringCommands.java b/java/client/src/main/java/glide/api/commands/StringCommands.java new file mode 100644 index 0000000000..8037a4682c --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/StringCommands.java @@ -0,0 +1,51 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import glide.api.models.commands.SetOptions; +import glide.api.models.commands.SetOptions.ConditionalSet; +import glide.api.models.commands.SetOptions.SetOptionsBuilder; +import java.util.concurrent.CompletableFuture; + +/** + * String Commands interface to handle single commands. + * + * @see String Commands + */ +public interface StringCommands { + + /** + * Get the value associated with the given key, or null if no such value + * exists. + * + * @see redis.io for details. + * @param key The key to retrieve from the database. + * @return Response from Redis. If key exists, returns the value of + * key as a String. Otherwise, return null. + */ + CompletableFuture get(String key); + + /** + * Set the given key with the given value. + * + * @see redis.io for details. + * @param key The key to store. + * @param value The value to store with the given key. + * @return Response from Redis containing "OK". + */ + CompletableFuture set(String key, String value); + + /** + * Set the given key with the given value. Return value is dependent on the passed options. + * + * @see redis.io for details. + * @param key The key to store. + * @param value The value to store with the given key. + * @param options The Set options. + * @return Response from Redis containing a String or null response. If + * the value is successfully set, return "OK". If value isn't set because of + * {@link ConditionalSet#ONLY_IF_EXISTS} or {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST} + * conditions, return null. If {@link SetOptionsBuilder#returnOldValue(boolean)} + * is set, return the old value as a String. + */ + CompletableFuture set(String key, String value, SetOptions options); +} diff --git a/java/client/src/main/java/glide/api/models/commands/SetOptions.java b/java/client/src/main/java/glide/api/models/commands/SetOptions.java new file mode 100644 index 0000000000..05f4cd5bec --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SetOptions.java @@ -0,0 +1,171 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import static glide.api.models.commands.SetOptions.ExpiryType.KEEP_EXISTING; +import static glide.api.models.commands.SetOptions.ExpiryType.MILLISECONDS; +import static glide.api.models.commands.SetOptions.ExpiryType.SECONDS; +import static glide.api.models.commands.SetOptions.ExpiryType.UNIX_MILLISECONDS; +import static glide.api.models.commands.SetOptions.ExpiryType.UNIX_SECONDS; + +import glide.api.commands.StringCommands; +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import redis_request.RedisRequestOuterClass.Command; + +/** + * Optional arguments for {@link StringCommands#set(String, String, SetOptions)} command. + * + * @see redis.io + */ +@Builder +public final class SetOptions { + + /** + * If conditionalSet is not set the value will be set regardless of prior value + * existence. If value isn't set because of the condition, command will return null. + */ + private final ConditionalSet conditionalSet; + + /** + * Set command to return the old string stored at key, or null if + * key did not exist. An error is returned and SET aborted if the value stored + * at key is not a string. Equivalent to GET in the Redis API. + */ + private final boolean returnOldValue; + + /** If not set, no expiry time will be set for the value. */ + private final Expiry expiry; + + /** Conditions which define whether new value should be set or not. */ + @RequiredArgsConstructor + @Getter + public enum ConditionalSet { + /** + * Only set the key if it does not already exist. Equivalent to XX in the Redis + * API. + */ + ONLY_IF_EXISTS("XX"), + /** Only set the key if it already exists. Equivalent to NX in the Redis API. */ + ONLY_IF_DOES_NOT_EXIST("NX"); + + private final String redisApi; + } + + /** Configuration of value lifetime. */ + public static final class Expiry { + + /** Expiry type for the time to live */ + private final ExpiryType type; + + /** + * The amount of time to live before the key expires. Ignored when {@link + * ExpiryType#KEEP_EXISTING} type is set. + */ + private Long count; + + private Expiry(ExpiryType type) { + this.type = type; + } + + private Expiry(ExpiryType type, Long count) { + this.type = type; + this.count = count; + } + + /** + * Retain the time to live associated with the key. Equivalent to KEEPTTL in the + * Redis API. + */ + public static Expiry KeepExisting() { + return new Expiry(KEEP_EXISTING); + } + + /** + * Set the specified expire time, in seconds. Equivalent to EX in the Redis API. + * + * @param seconds time to expire, in seconds + * @return Expiry + */ + public static Expiry Seconds(Long seconds) { + return new Expiry(SECONDS, seconds); + } + + /** + * Set the specified expire time, in milliseconds. Equivalent to PX in the Redis + * API. + * + * @param milliseconds time to expire, in milliseconds + * @return Expiry + */ + public static Expiry Milliseconds(Long milliseconds) { + return new Expiry(MILLISECONDS, milliseconds); + } + + /** + * Set the specified Unix time at which the key will expire, in seconds. Equivalent to + * EXAT in the Redis API. + * + * @param unixSeconds unix time to expire, in seconds + * @return Expiry + */ + public static Expiry UnixSeconds(Long unixSeconds) { + return new Expiry(UNIX_SECONDS, unixSeconds); + } + + /** + * Set the specified Unix time at which the key will expire, in milliseconds. Equivalent to + * PXAT in the Redis API. + * + * @param unixMilliseconds unix time to expire, in milliseconds + * @return Expiry + */ + public static Expiry UnixMilliseconds(Long unixMilliseconds) { + return new Expiry(UNIX_MILLISECONDS, unixMilliseconds); + } + } + + /** Types of value expiration configuration. */ + @RequiredArgsConstructor + protected enum ExpiryType { + KEEP_EXISTING("KEEPTTL"), + SECONDS("EX"), + MILLISECONDS("PX"), + UNIX_SECONDS("EXAT"), + UNIX_MILLISECONDS("PXAT"); + + private final String redisApi; + } + + /** String representation of {@link #returnOldValue} when set. */ + public static final String RETURN_OLD_VALUE = "GET"; + + /** + * Converts SetOptions into a String[] to add to a {@link Command} arguments. + * + * @return String[] + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(); + if (conditionalSet != null) { + optionArgs.add(conditionalSet.redisApi); + } + + if (returnOldValue) { + optionArgs.add(RETURN_OLD_VALUE); + } + + if (expiry != null) { + optionArgs.add(expiry.type.redisApi); + if (expiry.type != KEEP_EXISTING) { + assert expiry.count != null + : "Set command received expiry type " + expiry.type + ", but count was not set."; + optionArgs.add(expiry.count.toString()); + } + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/managers/BaseCommandResponseResolver.java b/java/client/src/main/java/glide/managers/BaseCommandResponseResolver.java index 695bf05cab..f9b7ed87ab 100644 --- a/java/client/src/main/java/glide/managers/BaseCommandResponseResolver.java +++ b/java/client/src/main/java/glide/managers/BaseCommandResponseResolver.java @@ -1,6 +1,8 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.managers; +import static glide.api.BaseClient.OK; + import glide.api.models.exceptions.RedisException; import lombok.AllArgsConstructor; import response.ResponseOuterClass.Response; @@ -25,8 +27,7 @@ public Object apply(Response response) throws RedisException { assert !response.hasRequestError() : "Unhandled response request error"; if (response.hasConstantResponse()) { - // Return "OK" - return response.getConstantResponse().toString(); + return OK; } if (response.hasRespPointer()) { // Return the shared value - which may be a null value diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 0a1b3c23a8..bf666776e9 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -1,14 +1,23 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; +import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; +import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; +import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.Ping; +import static redis_request.RedisRequestOuterClass.RequestType.SetString; +import glide.api.models.commands.SetOptions; +import glide.api.models.commands.SetOptions.Expiry; import glide.managers.CommandManager; import glide.managers.ConnectionManager; import java.util.concurrent.CompletableFuture; @@ -96,4 +105,100 @@ public void ping_with_message_returns_success() { assertEquals(testResponse, response); assertEquals(message, pong); } + + @SneakyThrows + @Test + public void get_returns_success() { + // setup + String key = "testKey"; + String value = "testValue"; + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + when(commandManager.submitNewCommand(eq(GetString), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.get(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void set_returns_success() { + // setup + String key = "testKey"; + String value = "testValue"; + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(null); + when(commandManager.submitNewCommand(eq(SetString), eq(new String[] {key, value}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.set(key, value); + Object okResponse = response.get(); + + // verify + assertEquals(testResponse, response); + assertNull(okResponse); + } + + @SneakyThrows + @Test + public void set_with_SetOptions_OnlyIfExists_returns_success() { + // setup + String key = "testKey"; + String value = "testValue"; + SetOptions setOptions = + SetOptions.builder() + .conditionalSet(ONLY_IF_EXISTS) + .returnOldValue(false) + .expiry(Expiry.KeepExisting()) + .build(); + String[] arguments = new String[] {key, value, ONLY_IF_EXISTS.getRedisApi(), "KEEPTTL"}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(null); + when(commandManager.submitNewCommand(eq(SetString), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.set(key, value, setOptions); + + // verify + assertEquals(testResponse, response); + assertNull(response.get()); + } + + @SneakyThrows + @Test + public void set_with_SetOptions_OnlyIfDoesNotExist_returns_success() { + // setup + String key = "testKey"; + String value = "testValue"; + SetOptions setOptions = + SetOptions.builder() + .conditionalSet(ONLY_IF_DOES_NOT_EXIST) + .returnOldValue(true) + .expiry(Expiry.UnixSeconds(60L)) + .build(); + String[] arguments = + new String[] { + key, value, ONLY_IF_DOES_NOT_EXIST.getRedisApi(), RETURN_OLD_VALUE, "EXAT", "60" + }; + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + when(commandManager.submitNewCommand(eq(SetString), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.set(key, value, setOptions); + + // verify + assertNotNull(response); + assertEquals(value, response.get()); + } } diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index ed522c9116..4f33ca36db 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -3,11 +3,18 @@ import static glide.TestConfiguration.CLUSTER_PORTS; import static glide.TestConfiguration.STANDALONE_PORTS; +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.Expiry.Milliseconds; 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 glide.api.BaseClient; import glide.api.RedisClient; import glide.api.RedisClusterClient; +import glide.api.models.commands.SetOptions; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClientConfiguration; import glide.api.models.configuration.RedisClusterClientConfiguration; @@ -29,6 +36,10 @@ public class SharedCommandTests { @Getter private static List clients; + private static final String KEY_NAME = "key"; + private static final String INITIAL_VALUE = "VALUE"; + private static final String ANOTHER_VALUE = "VALUE2"; + @BeforeAll @SneakyThrows public static void init() { @@ -72,4 +83,168 @@ public void ping_with_message(BaseClient client) { String data = client.ping("H3LL0").get(); assertEquals("H3LL0", data); } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void set_and_get_without_options(BaseClient client) { + String ok = client.set(KEY_NAME, INITIAL_VALUE).get(); + assertEquals(OK, ok); + + String data = client.get(KEY_NAME).get(); + assertEquals(INITIAL_VALUE, data); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void get_missing_value(BaseClient client) { + String data = client.get("invalid").get(); + assertNull(data); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void set_overwrite_value_and_returnOldValue_returns_string(BaseClient client) { + String ok = client.set(KEY_NAME, INITIAL_VALUE).get(); + assertEquals(OK, ok); + + SetOptions options = SetOptions.builder().returnOldValue(true).build(); + String data = client.set(KEY_NAME, ANOTHER_VALUE, options).get(); + assertEquals(INITIAL_VALUE, data); + } + + @ParameterizedTest + @MethodSource("getClients") + public void set_requires_a_value(BaseClient client) { + assertThrows(NullPointerException.class, () -> client.set("SET", null)); + } + + @ParameterizedTest + @MethodSource("getClients") + public void set_requires_a_key(BaseClient client) { + assertThrows(NullPointerException.class, () -> client.set(null, INITIAL_VALUE)); + } + + @ParameterizedTest + @MethodSource("getClients") + public void get_requires_a_key(BaseClient client) { + assertThrows(NullPointerException.class, () -> client.get(null)); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void set_only_if_exists_overwrite(BaseClient client) { + String key = "set_only_if_exists_overwrite"; + SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_EXISTS).build(); + client.set(key, INITIAL_VALUE).get(); + client.set(key, ANOTHER_VALUE, options).get(); + String data = client.get(key).get(); + assertEquals(ANOTHER_VALUE, data); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void set_only_if_exists_missing_key(BaseClient client) { + String key = "set_only_if_exists_missing_key"; + SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_EXISTS).build(); + client.set(key, ANOTHER_VALUE, options).get(); + String data = client.get(key).get(); + assertNull(data); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void set_only_if_does_not_exists_missing_key(BaseClient client) { + String key = "set_only_if_does_not_exists_missing_key"; + SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_DOES_NOT_EXIST).build(); + client.set(key, ANOTHER_VALUE, options).get(); + String data = client.get(key).get(); + assertEquals(ANOTHER_VALUE, data); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void set_only_if_does_not_exists_existing_key(BaseClient client) { + String key = "set_only_if_does_not_exists_existing_key"; + SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_DOES_NOT_EXIST).build(); + client.set(key, INITIAL_VALUE).get(); + client.set(key, ANOTHER_VALUE, options).get(); + String data = client.get(key).get(); + assertEquals(INITIAL_VALUE, data); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void set_value_with_ttl_and_update_value_with_keeping_ttl(BaseClient client) { + String key = "set_value_with_ttl_and_update_value_with_keeping_ttl"; + SetOptions options = SetOptions.builder().expiry(Milliseconds(2000L)).build(); + client.set(key, INITIAL_VALUE, options).get(); + String data = client.get(key).get(); + assertEquals(INITIAL_VALUE, data); + + options = SetOptions.builder().expiry(SetOptions.Expiry.KeepExisting()).build(); + client.set(key, ANOTHER_VALUE, options).get(); + data = client.get(key).get(); + assertEquals(ANOTHER_VALUE, data); + + Thread.sleep(2222); // sleep a bit more than TTL + + data = client.get(key).get(); + assertNull(data); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void set_value_with_ttl_and_update_value_with_new_ttl(BaseClient client) { + String key = "set_value_with_ttl_and_update_value_with_new_ttl"; + SetOptions options = SetOptions.builder().expiry(Milliseconds(100500L)).build(); + client.set(key, INITIAL_VALUE, options).get(); + String data = client.get(key).get(); + assertEquals(INITIAL_VALUE, data); + + options = SetOptions.builder().expiry(Milliseconds(2000L)).build(); + client.set(key, ANOTHER_VALUE, options).get(); + data = client.get(key).get(); + assertEquals(ANOTHER_VALUE, data); + + Thread.sleep(2222); // sleep a bit more than new TTL + + data = client.get(key).get(); + assertNull(data); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void set_expired_value(BaseClient client) { + String key = "set_expired_value"; + SetOptions options = + SetOptions.builder() + // expiration is in the past + .expiry(SetOptions.Expiry.UnixSeconds(100500L)) + .build(); + client.set(key, INITIAL_VALUE, options).get(); + String data = client.get(key).get(); + assertNull(data); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void set_missing_value_and_returnOldValue_is_null(BaseClient client) { + String ok = client.set(KEY_NAME, INITIAL_VALUE).get(); + assertEquals(OK, ok); + + SetOptions options = SetOptions.builder().returnOldValue(true).build(); + String data = client.set("another", ANOTHER_VALUE, options).get(); + assertNull(data); + } } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index fba4c2a952..be9072a13c 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -5,6 +5,7 @@ import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_PRIMARIES; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import glide.api.RedisClusterClient; @@ -23,6 +24,8 @@ public class CommandTests { private static RedisClusterClient clusterClient = null; + private static final String INITIAL_VALUE = "VALUE"; + @BeforeAll @SneakyThrows public static void init() { @@ -58,6 +61,17 @@ public void custom_command_ping() { assertEquals("PONG", data.getSingleValue()); } + @Test + @SneakyThrows + public void custom_command_del_returns_a_number() { + String key = "custom_command_del_returns_a_number"; + clusterClient.set(key, INITIAL_VALUE).get(); + var del = clusterClient.customCommand(new String[] {"DEL", key}).get(); + assertEquals(1L, del.getSingleValue()); + var data = clusterClient.get(key).get(); + assertNull(data); + } + @Test @SneakyThrows public void ping_with_route() { diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index bc054cbdb5..29568ef9ea 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -2,6 +2,8 @@ package glide.standalone; import static glide.TestConfiguration.STANDALONE_PORTS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import glide.api.RedisClient; @@ -15,6 +17,9 @@ @Timeout(10) public class CommandTests { + + private static final String INITIAL_VALUE = "VALUE"; + private static RedisClient regularClient = null; @BeforeAll @@ -40,4 +45,15 @@ public void custom_command_info() { Object data = regularClient.customCommand(new String[] {"info"}).get(); assertTrue(((String) data).contains("# Stats")); } + + @Test + @SneakyThrows + public void custom_command_del_returns_a_number() { + String key = "custom_command_del_returns_a_number"; + regularClient.set(key, INITIAL_VALUE).get(); + var del = regularClient.customCommand(new String[] {"DEL", key}).get(); + assertEquals(1L, del); + var data = regularClient.get(key).get(); + assertNull(data); + } }