From 2de46dfbda74d26f2956b4909c5caec8ead12a9f Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Fri, 23 Feb 2024 11:59:37 -0800 Subject: [PATCH] Java: Add `expire`, `expireAt`, `pexpire`, `pexpireAt` and `ttl` commands (#96) * Java: Add expire and ttl commands Signed-off-by: Andrew Carbonetto * Update commands for review comments Signed-off-by: Andrew Carbonetto * Minor javadoc fix Signed-off-by: Andrew Carbonetto * Spotless Signed-off-by: Andrew Carbonetto * Added examples in documentation. Minor format RedisClientTest. * Spotless * Update java/client/src/main/java/glide/api/commands/GenericBaseCommands.java Signed-off-by: Yury-Fridlyand * Spotless --------- Signed-off-by: Andrew Carbonetto Signed-off-by: Yury-Fridlyand Co-authored-by: SanHalacogluImproving Co-authored-by: Yury-Fridlyand --- .../src/main/java/glide/api/BaseClient.java | 70 ++++++ .../api/commands/GenericBaseCommands.java | 198 +++++++++++++++- .../glide/api/models/BaseTransaction.java | 209 +++++++++++++++++ .../api/models/commands/ExpireOptions.java | 45 ++++ .../test/java/glide/api/RedisClientTest.java | 215 ++++++++++++++++++ .../glide/api/models/TransactionTests.java | 81 ++++++- .../test/java/glide/SharedCommandTests.java | 121 ++++++++++ 7 files changed, 931 insertions(+), 8 deletions(-) create mode 100644 java/client/src/main/java/glide/api/models/commands/ExpireOptions.java diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index b24ac3ab96..b29a78df0d 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -8,6 +8,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; import static redis_request.RedisRequestOuterClass.RequestType.Del; import static redis_request.RedisRequestOuterClass.RequestType.Exists; +import static redis_request.RedisRequestOuterClass.RequestType.Expire; +import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.HashDel; import static redis_request.RedisRequestOuterClass.RequestType.HashExists; @@ -22,12 +24,15 @@ import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; +import static redis_request.RedisRequestOuterClass.RequestType.PExpire; +import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; 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 static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Unlink; import glide.api.commands.ConnectionManagementCommands; @@ -35,6 +40,7 @@ import glide.api.commands.HashCommands; import glide.api.commands.SetCommands; import glide.api.commands.StringCommands; +import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.SetOptions; import glide.api.models.configuration.BaseClientConfiguration; import glide.api.models.exceptions.RedisException; @@ -371,4 +377,68 @@ public CompletableFuture exists(@NonNull String[] keys) { public CompletableFuture unlink(@NonNull String[] keys) { return commandManager.submitNewCommand(Unlink, keys, this::handleLongResponse); } + + @Override + public CompletableFuture expire(@NonNull String key, long seconds) { + return commandManager.submitNewCommand( + Expire, new String[] {key, Long.toString(seconds)}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture expire( + @NonNull String key, long seconds, @NonNull ExpireOptions expireOptions) { + String[] arguments = + ArrayUtils.addAll(new String[] {key, Long.toString(seconds)}, expireOptions.toArgs()); + return commandManager.submitNewCommand(Expire, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture expireAt(@NonNull String key, long unixSeconds) { + return commandManager.submitNewCommand( + ExpireAt, new String[] {key, Long.toString(unixSeconds)}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture expireAt( + @NonNull String key, long unixSeconds, @NonNull ExpireOptions expireOptions) { + String[] arguments = + ArrayUtils.addAll(new String[] {key, Long.toString(unixSeconds)}, expireOptions.toArgs()); + return commandManager.submitNewCommand(ExpireAt, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpire(@NonNull String key, long milliseconds) { + return commandManager.submitNewCommand( + PExpire, new String[] {key, Long.toString(milliseconds)}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpire( + @NonNull String key, long milliseconds, @NonNull ExpireOptions expireOptions) { + String[] arguments = + ArrayUtils.addAll(new String[] {key, Long.toString(milliseconds)}, expireOptions.toArgs()); + return commandManager.submitNewCommand(PExpire, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpireAt(@NonNull String key, long unixMilliseconds) { + return commandManager.submitNewCommand( + PExpireAt, + new String[] {key, Long.toString(unixMilliseconds)}, + this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpireAt( + @NonNull String key, long unixMilliseconds, @NonNull ExpireOptions expireOptions) { + String[] arguments = + ArrayUtils.addAll( + new String[] {key, Long.toString(unixMilliseconds)}, expireOptions.toArgs()); + return commandManager.submitNewCommand(PExpireAt, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture ttl(@NonNull String key) { + return commandManager.submitNewCommand(TTL, new String[] {key}, this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java index 13a5b05353..cf2f1e1567 100644 --- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java @@ -1,11 +1,11 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.commands.ExpireOptions; import java.util.concurrent.CompletableFuture; /** - * Generic Commands interface to handle generic commands for all server requests for both standalone - * and cluster clients. + * Generic Commands interface to handle generic commands for all server requests. * * @see Generic Commands */ @@ -53,4 +53,198 @@ public interface GenericBaseCommands { * */ CompletableFuture unlink(String[] keys); + + /** + * Sets a timeout on key in seconds. After the timeout has expired, the key + * will automatically be deleted. If key already has an existing expire + * set, the time to live is updated to the new value. If seconds is a + * non-positive number, the key will be deleted rather than expired. The timeout will + * only be cleared by commands that delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param seconds The timeout in seconds. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
+     * Boolean isSet = client.expire("my_key", 60).get()
+     * assert isSet //Indicates that a timeout of 60 seconds has been set for "my_key."
+     * 
+ */ + CompletableFuture expire(String key, long seconds); + + /** + * Sets a timeout on key in seconds. After the timeout has expired, the key + * will automatically be deleted. If key already has an existing expire + * set, the time to live is updated to the new value. If seconds is a + * non-positive number, the key will be deleted rather than expired. The timeout will + * only be cleared by commands that delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param seconds The timeout in seconds. + * @param expireOptions The expire options. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
+     * Boolean isSet = client.expire("my_key", 60, ExpireOptions.HAS_NO_EXPIRY).get()
+     * assert isSet //Indicates that a timeout of 60 seconds has been set for "my_key."
+     * 
+ */ + CompletableFuture expire(String key, long seconds, ExpireOptions expireOptions); + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (seconds since January + * 1, 1970) instead of specifying the number of seconds. A timestamp in the past will delete the + * key immediately. After the timeout has expired, the key will + * automatically be deleted. If key already has an existing expire set, + * the time to live is updated to the new value. The timeout will only be cleared by commands that + * delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixSeconds The timeout in an absolute Unix timestamp. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
+     * Boolean isSet = client.expireAt("my_key", 1672531200).get()
+     * assert isSet
+     * 
+ */ + CompletableFuture expireAt(String key, long unixSeconds); + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (seconds since January + * 1, 1970) instead of specifying the number of seconds. A timestamp in the past will delete the + * key immediately. After the timeout has expired, the key will + * automatically be deleted. If key already has an existing expire set, + * the time to live is updated to the new value. The timeout will only be cleared by commands that + * delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixSeconds The timeout in an absolute Unix timestamp. + * @param expireOptions The expire options. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
+     * Boolean isSet = client.expireAt("my_key", 1672531200, ExpireOptions.HasNoExpiry).get()
+     * assert isSet
+     * 
+ */ + CompletableFuture expireAt(String key, long unixSeconds, ExpireOptions expireOptions); + + /** + * Sets a timeout on key in milliseconds. After the timeout has expired, the + * key will automatically be deleted. If key already has an existing + * expire set, the time to live is updated to the new value. If milliseconds + * is a non-positive number, the key will be deleted rather than expired. The timeout + * will only be cleared by commands that delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param milliseconds The timeout in milliseconds. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
+     * Boolean isSet = client.pexpire("my_key", 60000).get()
+     * assert isSet
+     * 
+ */ + CompletableFuture pexpire(String key, long milliseconds); + + /** + * Sets a timeout on key in milliseconds. After the timeout has expired, the + * key will automatically be deleted. If key already has an existing expire + * set, the time to live is updated to the new value. If milliseconds is a + * non-positive number, the key will be deleted rather than expired. The timeout will + * only be cleared by commands that delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param milliseconds The timeout in milliseconds. + * @param expireOptions The expire options. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
+     * Boolean isSet = client.pexpire("my_key", 60000, ExpireOptions.HasNoExpiry).get()
+     * assert isSet
+     * 
+ */ + CompletableFuture pexpire(String key, long milliseconds, ExpireOptions expireOptions); + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since + * January 1, 1970) instead of specifying the number of milliseconds. A timestamp in the past will + * delete the key immediately. After the timeout has expired, the key + * will automatically be deleted. If key already has an existing expire + * set, the time to live is updated to the new value. The timeout will only be cleared by commands + * that delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixMilliseconds The timeout in an absolute Unix timestamp. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
+     * Boolean isSet = client.pexpireAt("my_key", 1672531200000).get()
+     * assert isSet
+     * 
+ */ + CompletableFuture pexpireAt(String key, long unixMilliseconds); + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since + * January 1, 1970) instead of specifying the number of milliseconds. A timestamp in the past will + * delete the key immediately. After the timeout has expired, the key + * will automatically be deleted. If key already has an existing expire + * set, the time to live is updated to the new value. The timeout will only be cleared by commands + * that delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixMilliseconds The timeout in an absolute Unix timestamp. + * @param expireOptions The expire option. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
+     * Boolean isSet = client.pexpireAt("my_key", 1672531200000, ExpireOptions.HasNoExpiry).get()
+     * assert isSet
+     * 
+ */ + CompletableFuture pexpireAt( + String key, long unixMilliseconds, ExpireOptions expireOptions); + + /** + * Returns the remaining time to live of key that has a timeout. + * + * @see redis.io for details. + * @param key The key to return its timeout. + * @return TTL in seconds, -2 if key does not exist, or -1 + * if key exists but has no associated expire. + * @example + *
+     * Long timeRemaining = client.ttl("my_key").get()
+     * assert timeRemaining == 3600L //Indicates that "my_key" has a remaining time to live of 3600 seconds.
+     * Long timeRemaining = client.ttl("nonexistent_key").get()
+     * assert timeRemaining == -2L //Returns -2 for a non-existing key.
+     * 
+ */ + CompletableFuture ttl(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 096000d458..d1c2b41cfa 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -7,6 +7,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; import static redis_request.RedisRequestOuterClass.RequestType.Del; import static redis_request.RedisRequestOuterClass.RequestType.Exists; +import static redis_request.RedisRequestOuterClass.RequestType.Expire; +import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.HashDel; import static redis_request.RedisRequestOuterClass.RequestType.HashExists; @@ -22,14 +24,18 @@ 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.PExpire; +import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; 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 static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Unlink; +import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.InfoOptions.Section; import glide.api.models.commands.SetOptions; @@ -565,6 +571,209 @@ public T unlink(String[] keys) { return getThis(); } + /** + * Sets a timeout on key in seconds. After the timeout has expired, the key + * will automatically be deleted. If key already has an existing expire + * set, the time to live is updated to the new value. If seconds is a + * non-positive number, the key will be deleted rather than expired. The timeout will + * only be cleared by commands that delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param seconds The timeout in seconds. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + */ + public T expire(@NonNull String key, long seconds) { + ArgsArray commandArgs = buildArgs(key, Long.toString(seconds)); + + protobufTransaction.addCommands(buildCommand(Expire, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key in seconds. After the timeout has expired, the key + * will automatically be deleted. If key already has an existing expire + * set, the time to live is updated to the new value. If seconds is a + * non-positive number, the key will be deleted rather than expired. The timeout will + * only be cleared by commands that delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param seconds The timeout in seconds. + * @param expireOptions The expire options. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist, or operation skipped due to the + * provided arguments. + */ + public T expire(@NonNull String key, long seconds, @NonNull ExpireOptions expireOptions) { + ArgsArray commandArgs = + buildArgs( + ArrayUtils.addAll(new String[] {key, Long.toString(seconds)}, expireOptions.toArgs())); + + protobufTransaction.addCommands(buildCommand(Expire, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (seconds since January + * 1, 1970) instead of specifying the number of seconds. A timestamp in the past will delete the + * key immediately. After the timeout has expired, the key will + * automatically be deleted. If key already has an existing expire set, + * the time to live is updated to the new value. The timeout will only be cleared by commands that + * delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixSeconds The timeout in an absolute Unix timestamp. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist, or operation skipped due to the + * provided arguments. + */ + public T expireAt(@NonNull String key, long unixSeconds) { + ArgsArray commandArgs = buildArgs(key, Long.toString(unixSeconds)); + + protobufTransaction.addCommands(buildCommand(ExpireAt, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (seconds since January + * 1, 1970) instead of specifying the number of seconds. A timestamp in the past will delete the + * key immediately. After the timeout has expired, the key will + * automatically be deleted. If key already has an existing expire set, + * the time to live is updated to the new value. The timeout will only be cleared by commands that + * delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixSeconds The timeout in an absolute Unix timestamp. + * @param expireOptions The expire options. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist, or operation skipped due to the + * provided arguments. + */ + public T expireAt(@NonNull String key, long unixSeconds, @NonNull ExpireOptions expireOptions) { + ArgsArray commandArgs = + buildArgs( + ArrayUtils.addAll( + new String[] {key, Long.toString(unixSeconds)}, expireOptions.toArgs())); + + protobufTransaction.addCommands(buildCommand(ExpireAt, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key in milliseconds. After the timeout has expired, the + * key will automatically be deleted. If key already has an existing + * expire set, the time to live is updated to the new value. If milliseconds + * is a non-positive number, the key will be deleted rather than expired. The timeout + * will only be cleared by commands that delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param milliseconds The timeout in milliseconds. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist, or operation skipped due to the + * provided arguments. + */ + public T pexpire(@NonNull String key, long milliseconds) { + ArgsArray commandArgs = buildArgs(key, Long.toString(milliseconds)); + + protobufTransaction.addCommands(buildCommand(PExpire, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key in milliseconds. After the timeout has expired, the + * key will automatically be deleted. If key already has an existing expire + * set, the time to live is updated to the new value. If milliseconds is a + * non-positive number, the key will be deleted rather than expired. The timeout will + * only be cleared by commands that delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param milliseconds The timeout in milliseconds. + * @param expireOptions The expire options. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist, or operation skipped due to the + * provided arguments. + */ + public T pexpire(@NonNull String key, long milliseconds, @NonNull ExpireOptions expireOptions) { + ArgsArray commandArgs = + buildArgs( + ArrayUtils.addAll( + new String[] {key, Long.toString(milliseconds)}, expireOptions.toArgs())); + + protobufTransaction.addCommands(buildCommand(PExpire, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since + * January 1, 1970) instead of specifying the number of milliseconds. A timestamp in the past will + * delete the key immediately. After the timeout has expired, the key + * will automatically be deleted. If key already has an existing expire + * set, the time to live is updated to the new value. The timeout will only be cleared by commands + * that delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixMilliseconds The timeout in an absolute Unix timestamp. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist, or operation skipped due to the + * provided arguments. + */ + public T pexpireAt(@NonNull String key, long unixMilliseconds) { + ArgsArray commandArgs = buildArgs(key, Long.toString(unixMilliseconds)); + + protobufTransaction.addCommands(buildCommand(PExpireAt, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since + * January 1, 1970) instead of specifying the number of milliseconds. A timestamp in the past will + * delete the key immediately. After the timeout has expired, the key + * will automatically be deleted. If key already has an existing expire + * set, the time to live is updated to the new value. The timeout will only be cleared by commands + * that delete or overwrite the contents of key. + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixMilliseconds The timeout in an absolute Unix timestamp. + * @param expireOptions The expire option. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist, or operation skipped due to the + * provided arguments. + */ + public T pexpireAt( + @NonNull String key, long unixMilliseconds, @NonNull ExpireOptions expireOptions) { + ArgsArray commandArgs = + buildArgs( + ArrayUtils.addAll( + new String[] {key, Long.toString(unixMilliseconds)}, expireOptions.toArgs())); + + protobufTransaction.addCommands(buildCommand(PExpireAt, commandArgs)); + return getThis(); + } + + /** + * Returns the remaining time to live of key that has a timeout. + * + * @see redis.io for details. + * @param key The key to return its timeout. + * @return Command response - TTL in seconds, -2 if key does not exist, + * or -1 if key exists but has no associated expire. + */ + public T ttl(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + + protobufTransaction.addCommands(buildCommand(TTL, 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/main/java/glide/api/models/commands/ExpireOptions.java b/java/client/src/main/java/glide/api/models/commands/ExpireOptions.java new file mode 100644 index 0000000000..2f51745af5 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/ExpireOptions.java @@ -0,0 +1,45 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.GenericBaseCommands; +import lombok.RequiredArgsConstructor; + +/** + * Optional arguments for {@link GenericBaseCommands#expire(String, long, ExpireOptions)}, and + * similar commands. + * + * @see redis.io + */ +@RequiredArgsConstructor +public enum ExpireOptions { + /** + * Sets expiry only when the key has no expiry. Equivalent to NX in the Redis API. + */ + HAS_NO_EXPIRY("NX"), + /** + * Sets expiry only when the key has an existing expiry. Equivalent to XX in the + * Redis API. + */ + HAS_EXISTING_EXPIRY("XX"), + /** + * Sets expiry only when the new expiry is greater than current one. Equivalent to GT + * in the Redis API. + */ + NEW_EXPIRY_GREATER_THAN_CURRENT("GT"), + /** + * Sets expiry only when the new expiry is less than current one. Equivalent to LT in + * the Redis API. + */ + NEW_EXPIRY_LESS_THAN_CURRENT("LT"); + + private final String redisApi; + + /** + * Converts ExpireOptions into a String[]. + * + * @return String[] + */ + public String[] toArgs() { + return new String[] {this.redisApi}; + } +} diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 85b4ab7d17..8c5b0a309d 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -17,6 +17,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; import static redis_request.RedisRequestOuterClass.RequestType.Del; import static redis_request.RedisRequestOuterClass.RequestType.Exists; +import static redis_request.RedisRequestOuterClass.RequestType.Expire; +import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.HashDel; import static redis_request.RedisRequestOuterClass.RequestType.HashExists; @@ -32,6 +34,8 @@ 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.PExpire; +import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; import static redis_request.RedisRequestOuterClass.RequestType.SCard; @@ -39,8 +43,10 @@ import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.Select; import static redis_request.RedisRequestOuterClass.RequestType.SetString; +import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Unlink; +import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.SetOptions; import glide.api.models.commands.SetOptions.Expiry; @@ -293,6 +299,215 @@ public void exists_returns_long_success() { assertEquals(numberExisting, result); } + @SneakyThrows + @Test + public void expire_returns_success() { + // setup + String key = "testKey"; + long seconds = 10L; + String[] arguments = new String[] {key, Long.toString(seconds)}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Expire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expire(key, seconds); + + // verify + assertNotNull(response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void expire_with_expireOptions_returns_success() { + // setup + String key = "testKey"; + long seconds = 10L; + String[] arguments = new String[] {key, Long.toString(seconds), "NX"}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(false); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Expire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expire(key, seconds, ExpireOptions.HAS_NO_EXPIRY); + + // verify + assertNotNull(response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void expireAt_returns_success() { + // setup + String key = "testKey"; + long unixSeconds = 100000L; + String[] arguments = new String[] {key, Long.toString(unixSeconds)}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expireAt(key, unixSeconds); + + // verify + assertNotNull(response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void expireAt_with_expireOptions_returns_success() { + // setup + String key = "testKey"; + long unixSeconds = 100000L; + String[] arguments = new String[] {key, Long.toString(unixSeconds), "XX"}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(false); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.expireAt(key, unixSeconds, ExpireOptions.HAS_EXISTING_EXPIRY); + + // verify + assertNotNull(response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void pexpire_returns_success() { + // setup + String key = "testKey"; + long milliseconds = 5L; + String[] arguments = new String[] {key, Long.toString(milliseconds)}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pexpire(key, milliseconds); + + // verify + assertNotNull(response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void pexpire_with_expireOptions_returns_success() { + // setup + String key = "testKey"; + long milliseconds = 5L; + String[] arguments = new String[] {key, Long.toString(milliseconds), "LT"}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(false); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.pexpire(key, milliseconds, ExpireOptions.NEW_EXPIRY_LESS_THAN_CURRENT); + + // verify + assertNotNull(response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void pexpireAt_returns_success() { + // setup + String key = "testKey"; + long unixMilliseconds = 999999L; + String[] arguments = new String[] {key, Long.toString(unixMilliseconds)}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pexpireAt(key, unixMilliseconds); + + // verify + assertNotNull(response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void pexpireAt_with_expireOptions_returns_success() { + // setup + String key = "testKey"; + long unixMilliseconds = 999999L; + String[] arguments = new String[] {key, Long.toString(unixMilliseconds), "GT"}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(false); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.pexpireAt(key, unixMilliseconds, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT); + + // verify + assertNotNull(response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void ttl_returns_success() { + // setup + String key = "testKey"; + long ttl = 999L; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(ttl); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(TTL), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ttl(key); + + // verify + assertNotNull(response); + assertEquals(ttl, response.get()); + } + @SneakyThrows @Test public void info_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 db21eef29c..ff4e824bc6 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -7,6 +7,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; import static redis_request.RedisRequestOuterClass.RequestType.Del; import static redis_request.RedisRequestOuterClass.RequestType.Exists; +import static redis_request.RedisRequestOuterClass.RequestType.Expire; +import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.HashDel; import static redis_request.RedisRequestOuterClass.RequestType.HashExists; @@ -22,14 +24,18 @@ 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.PExpire; +import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; 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 static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Unlink; +import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.SetOptions; import java.util.LinkedList; @@ -70,15 +76,9 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) .addArgs(RETURN_OLD_VALUE) .build())); - transaction.exists(new String[] {"key1", "key2"}); - results.add(Pair.of(Exists, ArgsArray.newBuilder().addArgs("key1").addArgs("key2").build())); - transaction.del(new String[] {"key1", "key2"}); results.add(Pair.of(Del, ArgsArray.newBuilder().addArgs("key1").addArgs("key2").build())); - transaction.unlink(new String[] {"key1", "key2"}); - results.add(Pair.of(Unlink, ArgsArray.newBuilder().addArgs("key1").addArgs("key2").build())); - transaction.ping(); results.add(Pair.of(Ping, ArgsArray.newBuilder().build())); @@ -149,6 +149,75 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) HashIncrByFloat, ArgsArray.newBuilder().addArgs("key").addArgs("field").addArgs("1.5").build())); + transaction.exists(new String[] {"key1", "key2"}); + results.add(Pair.of(Exists, ArgsArray.newBuilder().addArgs("key1").addArgs("key2").build())); + + transaction.unlink(new String[] {"key1", "key2"}); + results.add(Pair.of(Unlink, ArgsArray.newBuilder().addArgs("key1").addArgs("key2").build())); + + transaction.expire("key", 9L); + results.add( + Pair.of(Expire, ArgsArray.newBuilder().addArgs("key").addArgs(Long.toString(9L)).build())); + + transaction.expire("key", 99L, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT); + results.add( + Pair.of( + Expire, + ArgsArray.newBuilder() + .addArgs("key") + .addArgs(Long.toString(99L)) + .addArgs("GT") + .build())); + + transaction.expireAt("key", 999L); + results.add( + Pair.of( + ExpireAt, ArgsArray.newBuilder().addArgs("key").addArgs(Long.toString(999L)).build())); + + transaction.expireAt("key", 9999L, ExpireOptions.NEW_EXPIRY_LESS_THAN_CURRENT); + results.add( + Pair.of( + ExpireAt, + ArgsArray.newBuilder() + .addArgs("key") + .addArgs(Long.toString(9999L)) + .addArgs("LT") + .build())); + + transaction.pexpire("key", 99999L); + results.add( + Pair.of( + PExpire, ArgsArray.newBuilder().addArgs("key").addArgs(Long.toString(99999L)).build())); + + transaction.pexpire("key", 999999L, ExpireOptions.HAS_EXISTING_EXPIRY); + results.add( + Pair.of( + PExpire, + ArgsArray.newBuilder() + .addArgs("key") + .addArgs(Long.toString(999999L)) + .addArgs("XX") + .build())); + + transaction.pexpireAt("key", 9999999L); + results.add( + Pair.of( + PExpireAt, + ArgsArray.newBuilder().addArgs("key").addArgs(Long.toString(9999999L)).build())); + + transaction.pexpireAt("key", 99999999L, ExpireOptions.HAS_NO_EXPIRY); + results.add( + Pair.of( + PExpireAt, + ArgsArray.newBuilder() + .addArgs("key") + .addArgs(Long.toString(99999999L)) + .addArgs("NX") + .build())); + + transaction.ttl("key"); + results.add(Pair.of(TTL, 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 e13e6bee89..d3f8fda5db 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -2,6 +2,7 @@ package glide; import static glide.TestConfiguration.CLUSTER_PORTS; +import static glide.TestConfiguration.REDIS_VERSION; 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; @@ -18,11 +19,13 @@ import glide.api.BaseClient; import glide.api.RedisClient; import glide.api.RedisClusterClient; +import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.SetOptions; 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.time.Instant; import java.util.List; import java.util.Map; import java.util.Set; @@ -617,4 +620,122 @@ public void exists_multiple_keys(BaseClient client) { client.exists(new String[] {key1, key2, key1, UUID.randomUUID().toString()}).get(); assertEquals(3L, existsKeysNum); } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void expire_pexpire_and_ttl_with_positive_timeout(BaseClient client) { + String key = UUID.randomUUID().toString(); + assertEquals(OK, client.set(key, "expire_timeout").get()); + assertTrue(client.expire(key, 10L).get()); + assertTrue(client.ttl(key).get() <= 10L); + + // set command clears the timeout. + assertEquals(OK, client.set(key, "pexpire_timeout").get()); + if (REDIS_VERSION.feature() < 7) { + assertTrue(client.pexpire(key, 10000L).get()); + } else { + assertTrue(client.pexpire(key, 10000L, ExpireOptions.HAS_NO_EXPIRY).get()); + } + assertTrue(client.ttl(key).get() <= 10L); + + // TTL will be updated to the new value = 15 + if (REDIS_VERSION.feature() < 7) { + assertTrue(client.expire(key, 15L).get()); + } else { + assertTrue(client.expire(key, 15L, ExpireOptions.HAS_EXISTING_EXPIRY).get()); + } + assertTrue(client.ttl(key).get() <= 15L); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void expireAt_pexpireAt_and_ttl_with_positive_timeout(BaseClient client) { + String key = UUID.randomUUID().toString(); + assertEquals(OK, client.set(key, "expireAt_timeout").get()); + assertTrue(client.expireAt(key, Instant.now().getEpochSecond() + 10L).get()); + assertTrue(client.ttl(key).get() <= 10L); + + // extend TTL + if (REDIS_VERSION.feature() < 7) { + assertTrue(client.expireAt(key, Instant.now().getEpochSecond() + 50L).get()); + } else { + assertTrue( + client + .expireAt( + key, + Instant.now().getEpochSecond() + 50L, + ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT) + .get()); + } + assertTrue(client.ttl(key).get() <= 50L); + + if (REDIS_VERSION.feature() >= 7) { + // set command clears the timeout. + assertEquals(OK, client.set(key, "pexpireAt_timeout").get()); + assertFalse( + client + .pexpireAt( + key, Instant.now().getEpochSecond() + 50000L, ExpireOptions.HAS_EXISTING_EXPIRY) + .get()); + } + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void expire_pexpire_with_timestamp_in_the_past_or_negative_timeout(BaseClient client) { + String key = UUID.randomUUID().toString(); + + assertEquals(OK, client.set(key, "expire_with_past_timestamp").get()); + assertEquals(-1L, client.ttl(key).get()); + assertTrue(client.expire(key, -10L).get()); + assertEquals(-2L, client.ttl(key).get()); + + assertEquals(OK, client.set(key, "pexpire_with_past_timestamp").get()); + assertTrue(client.pexpire(key, -10000L).get()); + assertEquals(-2L, client.ttl(key).get()); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void expireAt_pexpireAt_with_timestamp_in_the_past_or_negative_timeout(BaseClient client) { + String key = UUID.randomUUID().toString(); + + assertEquals(OK, client.set(key, "expireAt_with_past_timestamp").get()); + // set timeout in the past + assertTrue(client.expireAt(key, Instant.now().getEpochSecond() - 50L).get()); + assertEquals(-2L, client.ttl(key).get()); + + assertEquals(OK, client.set(key, "pexpireAt_with_past_timestamp").get()); + // set timeout in the past + assertTrue(client.pexpireAt(key, Instant.now().getEpochSecond() - 50000L).get()); + assertEquals(-2L, client.ttl(key).get()); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void expire_pexpire_and_ttl_with_non_existing_key(BaseClient client) { + String key = UUID.randomUUID().toString(); + + assertFalse(client.expire(key, 10L).get()); + assertFalse(client.pexpire(key, 10000L).get()); + + assertEquals(-2L, client.ttl(key).get()); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void expireAt_pexpireAt_and_ttl_with_non_existing_key(BaseClient client) { + String key = UUID.randomUUID().toString(); + + assertFalse(client.expireAt(key, Instant.now().getEpochSecond() + 10L).get()); + assertFalse(client.pexpireAt(key, Instant.now().getEpochSecond() + 10000L).get()); + + assertEquals(-2L, client.ttl(key).get()); + } }