From 6b34f680728c241cea724c2257906d25336b1d19 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 16 Apr 2024 15:57:08 -0700 Subject: [PATCH 1/2] Add `LASTSAVE` command. (#222) * Add `LASTSAVE` command. Signed-off-by: Yury-Fridlyand * PR comments. Signed-off-by: Yury-Fridlyand * PR comments. Signed-off-by: Yury-Fridlyand * PR comments. Signed-off-by: Yury-Fridlyand --------- Signed-off-by: Yury-Fridlyand --- glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/request_type.rs | 3 ++ .../src/main/java/glide/api/RedisClient.java | 6 +++ .../java/glide/api/RedisClusterClient.java | 18 ++++++++ .../ServerManagementClusterCommands.java | 45 ++++++++++++++++--- .../commands/ServerManagementCommands.java | 20 +++++++-- .../glide/api/models/BaseTransaction.java | 17 ++++++- .../glide/api/models/commands/SetOptions.java | 4 +- .../test/java/glide/api/RedisClientTest.java | 21 +++++++++ .../glide/api/RedisClusterClientTest.java | 42 +++++++++++++++++ .../glide/api/models/TransactionTests.java | 4 ++ .../cluster/ClusterTransactionTests.java | 10 +++++ .../test/java/glide/cluster/CommandTests.java | 15 +++++++ .../java/glide/standalone/CommandTests.java | 9 ++++ .../glide/standalone/TransactionTests.java | 11 +++++ 15 files changed, 213 insertions(+), 13 deletions(-) diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index b825979af1..1cb34b610d 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -150,6 +150,7 @@ enum RequestType { SetRange = 107; ZRemRangeByLex = 108; ZLexCount = 109; + LastSave = 118; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 1f960ce2d4..d6bae2c3ce 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -118,6 +118,7 @@ pub enum RequestType { SetRange = 107, ZRemRangeByLex = 108, ZLexCount = 109, + LastSave = 118, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -239,6 +240,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::SetRange => RequestType::SetRange, ProtobufRequestType::ZRemRangeByLex => RequestType::ZRemRangeByLex, ProtobufRequestType::ZLexCount => RequestType::ZLexCount, + ProtobufRequestType::LastSave => RequestType::LastSave, } } } @@ -356,6 +358,7 @@ impl RequestType { RequestType::SetRange => Some(cmd("SETRANGE")), RequestType::ZRemRangeByLex => Some(cmd("ZREMRANGEBYLEX")), RequestType::ZLexCount => Some(cmd("ZLEXCOUNT")), + RequestType::LastSave => Some(cmd("LastSave")), } } } diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index 2c27613762..2d1aeda088 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -12,6 +12,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.LastSave; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.Select; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -132,4 +133,9 @@ public CompletableFuture time() { return commandManager.submitNewCommand( Time, new String[0], response -> castArray(handleArrayResponse(response), String.class)); } + + @Override + public CompletableFuture lastsave() { + return commandManager.submitNewCommand(LastSave, new String[0], this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index 2e36351232..c2aec33d34 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -13,6 +13,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.LastSave; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -279,4 +280,21 @@ public CompletableFuture> time(@NonNull Route route) { : ClusterValue.ofMultiValue( castMapOfArrays(handleMapResponse(response), String.class))); } + + @Override + public CompletableFuture lastsave() { + return commandManager.submitNewCommand(LastSave, new String[0], this::handleLongResponse); + } + + @Override + public CompletableFuture> lastsave(@NonNull Route route) { + return commandManager.submitNewCommand( + LastSave, + new String[0], + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.of(handleLongResponse(response)) + : ClusterValue.of(handleMapResponse(response))); + } } diff --git a/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java b/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java index b9c3865529..db05056651 100644 --- a/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java @@ -246,9 +246,9 @@ public interface ServerManagementClusterCommands { * The command will be routed to a random node. * * @see redis.io for details. - * @return The current server time as a String array with two elements: A Unix - * timestamp and the amount of microseconds already elapsed in the current second. The - * returned array is in a [Unix timestamp, Microseconds already elapsed] format. + * @return The current server time as a String array with two elements: A + * UNIX TIME and the amount of microseconds already elapsed in the current second. The + * returned array is in a [UNIX TIME, Microseconds already elapsed] format. * @example *
{@code
      * String[] serverTime = client.time().get();
@@ -263,9 +263,9 @@ public interface ServerManagementClusterCommands {
      * @see redis.io for details.
      * @param route Specifies the routing configuration for the command. The client will route the
      *     command to the nodes defined by route.
-     * @return The current server time as a String array with two elements: A Unix
-     *     timestamp and the amount of microseconds already elapsed in the current second. The
-     *     returned array is in a [Unix timestamp, Microseconds already elapsed] format.
+     * @return The current server time as a String array with two elements: A 
+     *     UNIX TIME and the amount of microseconds already elapsed in the current second. The
+     *     returned array is in a [UNIX TIME, Microseconds already elapsed] format.
      * @example
      *     
{@code
      * // Command sent to a single random node via RANDOM route, expecting a SingleValue result.
@@ -282,4 +282,37 @@ public interface ServerManagementClusterCommands {
      * }
*/ CompletableFuture> time(Route route); + + /** + * Returns UNIX TIME of the last DB save timestamp or startup timestamp if no save + * was made since then.
+ * The command will be routed to a random node. + * + * @see redis.io for details. + * @return UNIX TIME of the last DB save executed with success. + * @example + *
{@code
+     * Long timestamp = client.lastsave().get();
+     * System.out.printf("Last DB save was done at %s%n", Instant.ofEpochSecond(timestamp));
+     * }
+ */ + CompletableFuture lastsave(); + + /** + * Returns UNIX TIME of the last DB save timestamp or startup timestamp if no save + * was made since then. + * + * @see redis.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return UNIX TIME of the last DB save executed with success. + * @example + *
{@code
+     * ClusterValue data = client.lastsave(ALL_NODES).get();
+     * for (Map.Entry entry : data.getMultiValue().entrySet()) {
+     *     System.out.printf("Last DB save on node %s was made at %s%n", entry.getKey(), Instant.ofEpochSecond(entry.getValue()));
+     * }
+     * }
+ */ + CompletableFuture> lastsave(Route route); } diff --git a/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java b/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java index 177be2bbb0..30dd8f15a2 100644 --- a/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java +++ b/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java @@ -123,9 +123,9 @@ public interface ServerManagementCommands { * Returns the server time. * * @see redis.io for details. - * @return The current server time as a String array with two elements: A Unix - * timestamp and the amount of microseconds already elapsed in the current second. The - * returned array is in a [Unix timestamp, Microseconds already elapsed] format. + * @return The current server time as a String array with two elements: A + * UNIX TIME and the amount of microseconds already elapsed in the current second. The + * returned array is in a [UNIX TIME, Microseconds already elapsed] format. * @example *
{@code
      * String[] serverTime = client.time().get();
@@ -133,4 +133,18 @@ public interface ServerManagementCommands {
      * }
*/ CompletableFuture time(); + + /** + * Returns UNIX TIME of the last DB save timestamp or startup timestamp if no save + * was made since then. + * + * @see redis.io for details. + * @return UNIX TIME of the last DB save executed with success. + * @example + *
{@code
+     * Long timestamp = client.lastsave().get();
+     * System.out.printf("Last DB save was done at %s%n", Instant.ofEpochSecond(timestamp));
+     * }
+ */ + CompletableFuture lastsave(); } 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 d04a6ecdd1..37976cf3d5 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -47,6 +47,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.LRange; import static redis_request.RedisRequestOuterClass.RequestType.LRem; import static redis_request.RedisRequestOuterClass.RequestType.LTrim; +import static redis_request.RedisRequestOuterClass.RequestType.LastSave; import static redis_request.RedisRequestOuterClass.RequestType.Lindex; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; @@ -1766,8 +1767,8 @@ public T persist(@NonNull String key) { * * @see redis.io for details. * @return Command Response - The current server time as a String array with two - * elements: A Unix timestamp and the amount of microseconds already elapsed in the current - * second. The returned array is in a [Unix timestamp, Microseconds already elapsed] + * elements: A UNIX TIME and the amount of microseconds already elapsed in the + * current second. The returned array is in a [UNIX TIME, Microseconds already elapsed] * format. */ public T time() { @@ -1775,6 +1776,18 @@ public T time() { return getThis(); } + /** + * Returns UNIX TIME of the last DB save timestamp or startup timestamp if no save + * was made since then. + * + * @see redis.io for details. + * @return Command Response - Unix time of the last DB save executed with success. + */ + public T lastsave() { + protobufTransaction.addCommands(buildCommand(LastSave)); + return getThis(); + } + /** * Returns the string representation of the type of the value stored at key. * 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 index ed3bfbd8ae..831e29c1b1 100644 --- a/java/client/src/main/java/glide/api/models/commands/SetOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/SetOptions.java @@ -108,7 +108,7 @@ public static Expiry Milliseconds(Long 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 + * @param unixSeconds UNIX TIME to expire, in seconds. * @return Expiry */ public static Expiry UnixSeconds(Long unixSeconds) { @@ -119,7 +119,7 @@ public static Expiry UnixSeconds(Long 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 + * @param unixMilliseconds UNIX TIME to expire, in milliseconds. * @return Expiry */ public static Expiry UnixMilliseconds(Long unixMilliseconds) { diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 1f8166ad04..247e4053b2 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -67,6 +67,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.LRange; import static redis_request.RedisRequestOuterClass.RequestType.LRem; import static redis_request.RedisRequestOuterClass.RequestType.LTrim; +import static redis_request.RedisRequestOuterClass.RequestType.LastSave; import static redis_request.RedisRequestOuterClass.RequestType.Lindex; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; @@ -2772,6 +2773,26 @@ public void time_returns_success() { assertEquals(payload, response.get()); } + @SneakyThrows + @Test + public void lastsave_returns_success() { + // setup + Long value = 42L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LastSave), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lastsave(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + @SneakyThrows @Test public void linsert_returns_success() { diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index 21fc05225e..89ef402f0a 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -21,6 +21,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.LastSave; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -771,4 +772,45 @@ public void time_returns_with_route_success() { assertEquals(testResponse, response); assertEquals(payload, response.get().getSingleValue()); } + + @SneakyThrows + @Test + public void lastsave_returns_success() { + // setup + Long value = 42L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LastSave), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lastsave(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lastsave_returns_with_route_success() { + // setup + Long value = 42L; + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(ClusterValue.ofSingleValue(value)); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(LastSave), eq(new String[0]), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lastsave(RANDOM); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get().getSingleValue()); + } } 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 01e537d015..aa11e32cb0 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -52,6 +52,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.LRange; import static redis_request.RedisRequestOuterClass.RequestType.LRem; import static redis_request.RedisRequestOuterClass.RequestType.LTrim; +import static redis_request.RedisRequestOuterClass.RequestType.LastSave; import static redis_request.RedisRequestOuterClass.RequestType.Lindex; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; @@ -406,6 +407,9 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) transaction.time(); results.add(Pair.of(Time, buildArgs())); + transaction.lastsave(); + results.add(Pair.of(LastSave, buildArgs())); + transaction.persist("key"); results.add(Pair.of(Persist, buildArgs("key"))); diff --git a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java index 2b2729c919..498893466f 100644 --- a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java +++ b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java @@ -15,6 +15,8 @@ import glide.api.models.ClusterTransaction; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClusterClientConfiguration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import lombok.SneakyThrows; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -82,4 +84,12 @@ public void test_cluster_transactions() { Object[] results = clusterClient.exec(transaction, RANDOM).get(); assertArrayEquals(expectedResult, results); } + + @Test + @SneakyThrows + public void lastsave() { + var yesterday = Instant.now().minus(1, ChronoUnit.DAYS); + var response = clusterClient.exec(new ClusterTransaction().lastsave()).get(); + assertTrue(Instant.ofEpochSecond((long) response[0]).isAfter(yesterday)); + } } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 78d685d885..81dd2586be 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -36,6 +36,7 @@ import glide.api.models.exceptions.RedisException; import glide.api.models.exceptions.RequestException; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -556,4 +557,18 @@ public void time_with_route() { "Time() result (" + serverTime[0] + ") should be greater than now (" + now + ")"); assertTrue(Long.parseLong((String) serverTime[1]) < 1000000); } + + @Test + @SneakyThrows + public void lastsave() { + long result = clusterClient.lastsave().get(); + var yesterday = Instant.now().minus(1, ChronoUnit.DAYS); + + assertTrue(Instant.ofEpochSecond(result).isAfter(yesterday)); + + ClusterValue data = clusterClient.lastsave(ALL_NODES).get(); + for (var value : data.getMultiValue().values()) { + assertTrue(Instant.ofEpochSecond(value).isAfter(yesterday)); + } + } } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 26c7adccb2..16343a2560 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -25,6 +25,7 @@ import glide.api.models.configuration.RedisClientConfiguration; import glide.api.models.exceptions.RequestException; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -266,4 +267,12 @@ public void time() { "Time() result (" + result[0] + ") should be greater than now (" + now + ")"); assertTrue(Long.parseLong(result[1]) < 1000000); } + + @Test + @SneakyThrows + public void lastsave() { + long result = regularClient.lastsave().get(); + var yesterday = Instant.now().minus(1, ChronoUnit.DAYS); + assertTrue(Instant.ofEpochSecond(result).isAfter(yesterday)); + } } diff --git a/java/integTest/src/test/java/glide/standalone/TransactionTests.java b/java/integTest/src/test/java/glide/standalone/TransactionTests.java index 2f9192f99d..532adb7703 100644 --- a/java/integTest/src/test/java/glide/standalone/TransactionTests.java +++ b/java/integTest/src/test/java/glide/standalone/TransactionTests.java @@ -15,6 +15,8 @@ import glide.api.models.commands.InfoOptions; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClientConfiguration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.UUID; import lombok.SneakyThrows; import org.apache.commons.lang3.ArrayUtils; @@ -110,4 +112,13 @@ public void test_standalone_transactions() { Object[] result = client.exec(transaction).get(); assertArrayEquals(expectedResult, result); } + + @Test + @SneakyThrows + public void lastsave() { + var yesterday = Instant.now().minus(1, ChronoUnit.DAYS); + + var response = client.exec(new Transaction().lastsave()).get(); + assertTrue(Instant.ofEpochSecond((long) response[0]).isAfter(yesterday)); + } } From c3d3aa80e596da62122f7914db54fdce582195ce Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 16 Apr 2024 17:37:50 -0700 Subject: [PATCH 2/2] Apply suggestions from code review Signed-off-by: Yury-Fridlyand Co-authored-by: Aaron <69273634+aaron-congo@users.noreply.github.com> --- glide-core/src/request_type.rs | 2 +- java/client/src/main/java/glide/api/models/BaseTransaction.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index cc3b290cf6..fc2c9a600f 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -373,7 +373,7 @@ impl RequestType { RequestType::ZRangeStore => Some(cmd("ZRANGESTORE")), RequestType::GetRange => Some(cmd("GETRANGE")), RequestType::SMove => Some(cmd("SMOVE")), - RequestType::LastSave => Some(cmd("LastSave")), + RequestType::LastSave => Some(cmd("LASTSAVE")), } } } 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 308ee21229..28d1ba2d82 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -1889,7 +1889,7 @@ public T time() { * was made since then. * * @see redis.io for details. - * @return Command Response - Unix time of the last DB save executed with success. + * @return Command Response - UNIX TIME of the last DB save executed with success. */ public T lastsave() { protobufTransaction.addCommands(buildCommand(LastSave));