From c6ba95f27143f3e896ac848222966f435c715d8e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 11 Feb 2025 11:32:38 +0100 Subject: [PATCH] Polishing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce command variants with Condition to avoid duplicate expireHashField(…) implementations. Move expireHashField to default method and rename it to applyExpiration(…). Rename Expiration to TimeToLive and methods to getTimeToLive(…). Fix since tags. Extract BoundHashFieldExpirationOperations to provide a fluent way of interacting with expirations on BoundHashOps and RedisMap. Add Hash Field Expiration commands to TypeHints for Lettuce. See #3054 --- .../antora/modules/ROOT/pages/appendix.adoc | 374 +++++++++--------- .../DefaultStringRedisConnection.java | 57 +-- .../connection/DefaultedRedisConnection.java | 52 ++- .../data/redis/connection/Hash.java | 56 ++- .../connection/ReactiveHashCommands.java | 25 +- .../redis/connection/RedisHashCommands.java | 250 +++++++++--- .../connection/StringRedisConnection.java | 174 +++++--- .../jedis/JedisClusterHashCommands.java | 143 +++---- .../connection/jedis/JedisHashCommands.java | 73 ++-- .../connection/lettuce/LettuceConnection.java | 8 + .../lettuce/LettuceHashCommands.java | 74 ++-- .../lettuce/LettuceReactiveHashCommands.java | 6 +- .../BoundHashFieldExpirationOperations.java | 126 ++++++ .../data/redis/core/BoundHashOperations.java | 131 ++---- ...ultBoundHashFieldExpirationOperations.java | 93 +++++ .../redis/core/DefaultHashOperations.java | 24 +- .../core/DefaultReactiveHashOperations.java | 18 +- .../data/redis/core/ExpireChanges.java | 22 +- .../data/redis/core/HashOperations.java | 120 ++++-- .../redis/core/ReactiveHashOperations.java | 29 +- .../data/redis/core/RedisCommand.java | 7 + .../data/redis/core/RedisOperations.java | 1 + .../redis/core/{ => types}/Expirations.java | 180 +++++---- .../support/collections/DefaultRedisMap.java | 29 +- .../redis/support/collections/RedisMap.java | 77 +--- .../support/collections/RedisProperties.java | 50 +-- .../AbstractConnectionIntegrationTests.java | 42 +- ...DefaultHashOperationsIntegrationTests.java | 65 ++- ...eactiveHashOperationsIntegrationTests.java | 17 +- .../core/{ => types}/ExpirationsUnitTest.java | 31 +- .../AbstractRedisMapIntegrationTests.java | 29 +- .../RedisPropertiesIntegrationTests.java | 6 - 32 files changed, 1461 insertions(+), 928 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/core/BoundHashFieldExpirationOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/DefaultBoundHashFieldExpirationOperations.java rename src/main/java/org/springframework/data/redis/core/{ => types}/Expirations.java (62%) rename src/test/java/org/springframework/data/redis/core/{ => types}/ExpirationsUnitTest.java (80%) diff --git a/src/main/antora/modules/ROOT/pages/appendix.adoc b/src/main/antora/modules/ROOT/pages/appendix.adoc index 46feff061..669bf8220 100644 --- a/src/main/antora/modules/ROOT/pages/appendix.adoc +++ b/src/main/antora/modules/ROOT/pages/appendix.adoc @@ -8,193 +8,201 @@ link:https://www.springframework.org/schema/redis/spring-redis-1.0.xsd[Spring Da [[supported-commands]] == Supported Commands + .Redis commands supported by `RedisTemplate` [width="50%",cols="<2,^1",options="header"] |========================================================= |Command |Template Support -|APPEND |X -|AUTH |X -|BGREWRITEAOF |X -|BGSAVE |X -|BITCOUNT |X -|BITFIELD |X -|BITOP |X -|BLPOP |X -|BRPOP |X -|BRPOPLPUSH |X -|CLIENT KILL |X -|CLIENT GETNAME |X -|CLIENT LIST |X -|CLIENT SETNAME |X -|CLUSTER SLOTS |- -|COMMAND |- -|COMMAND COUNT |- -|COMMAND GETKEYS |- -|COMMAND INFO |- -|CONFIG GET |X -|CONFIG RESETSTAT |X -|CONFIG REWRITE |- -|CONFIG SET |X -|DBSIZE |X -|DEBUG OBJECT |- -|DEBUG SEGFAULT |- -|DECR |X -|DECRBY |X -|DEL |X -|DISCARD |X -|DUMP |X -|ECHO |X -|EVAL |X -|EVALSHA |X -|EXEC |X -|EXISTS |X -|EXPIRE |X -|EXPIREAT |X -|FLUSHALL |X -|FLUSHDB |X -|GEOADD |X -|GEODIST |X -|GEOHASH |X -|GEOPOS |X -|GEORADIUS |X -|GEORADIUSBYMEMBER |X -|GEOSEARCH |X -|GEOSEARCHSTORE |X -|GET |X -|GETBIT |X -|GETRANGE |X -|GETSET |X -|HDEL |X -|HEXISTS |X -|HGET |X -|HGETALL |X -|HINCRBY |X -|HINCRBYFLOAT |X -|HKEYS |X -|HLEN |X -|HMGET |X -|HMSET |X -|HSCAN |X -|HSET |X -|HSETNX |X -|HVALS |X -|INCR |X -|INCRBY |X -|INCRBYFLOAT |X -|INFO |X -|KEYS |X -|LASTSAVE |X -|LINDEX |X -|LINSERT |X -|LLEN |X -|LPOP |X -|LPUSH |X -|LPUSHX |X -|LRANGE |X -|LREM |X -|LSET |X -|LTRIM |X -|MGET |X -|MIGRATE |- -|MONITOR |- -|MOVE |X -|MSET |X -|MSETNX |X -|MULTI |X -|OBJECT |- -|PERSIST |X -|PEXIPRE |X -|PEXPIREAT |X -|PFADD |X -|PFCOUNT |X -|PFMERGE |X -|PING |X -|PSETEX |X -|PSUBSCRIBE |X -|PTTL |X -|PUBLISH |X -|PUBSUB |- -|PUBSUBSCRIBE |- -|QUIT |X -|RANDOMKEY |X -|RENAME |X -|RENAMENX |X -|REPLICAOF |X -|RESTORE |X -|ROLE |- -|RPOP |X -|RPOPLPUSH |X -|RPUSH |X -|RPUSHX |X -|SADD |X -|SAVE |X -|SCAN |X -|SCARD |X -|SCRIPT EXITS |X -|SCRIPT FLUSH |X -|SCRIPT KILL |X -|SCRIPT LOAD |X -|SDIFF |X -|SDIFFSTORE |X -|SELECT |X -|SENTINEL FAILOVER |X +|APPEND |X +|AUTH |X +|BGREWRITEAOF |X +|BGSAVE |X +|BITCOUNT |X +|BITFIELD |X +|BITOP |X +|BLPOP |X +|BRPOP |X +|BRPOPLPUSH |X +|CLIENT KILL |X +|CLIENT GETNAME |X +|CLIENT LIST |X +|CLIENT SETNAME |X +|CLUSTER SLOTS |- +|COMMAND |- +|COMMAND COUNT |- +|COMMAND GETKEYS |- +|COMMAND INFO |- +|CONFIG GET |X +|CONFIG RESETSTAT |X +|CONFIG REWRITE |- +|CONFIG SET |X +|DBSIZE |X +|DEBUG OBJECT |- +|DEBUG SEGFAULT |- +|DECR |X +|DECRBY |X +|DEL |X +|DISCARD |X +|DUMP |X +|ECHO |X +|EVAL |X +|EVALSHA |X +|EXEC |X +|EXISTS |X +|EXPIRE |X +|EXPIREAT |X +|FLUSHALL |X +|FLUSHDB |X +|GEOADD |X +|GEODIST |X +|GEOHASH |X +|GEOPOS |X +|GEORADIUS |X +|GEORADIUSBYMEMBER |X +|GEOSEARCH |X +|GEOSEARCHSTORE |X +|GET |X +|GETBIT |X +|GETRANGE |X +|GETSET |X +|HDEL |X +|HEXISTS |X +|HEXPIRE |X +|HEXPIREAT |X +|HPEXPIRE |X +|HPEXPIREAT |X +|HPERSIST |X +|HTTL |X +|HPTTL |X +|HGET |X +|HGETALL |X +|HINCRBY |X +|HINCRBYFLOAT |X +|HKEYS |X +|HLEN |X +|HMGET |X +|HMSET |X +|HSCAN |X +|HSET |X +|HSETNX |X +|HVALS |X +|INCR |X +|INCRBY |X +|INCRBYFLOAT |X +|INFO |X +|KEYS |X +|LASTSAVE |X +|LINDEX |X +|LINSERT |X +|LLEN |X +|LPOP |X +|LPUSH |X +|LPUSHX |X +|LRANGE |X +|LREM |X +|LSET |X +|LTRIM |X +|MGET |X +|MIGRATE |- +|MONITOR |- +|MOVE |X +|MSET |X +|MSETNX |X +|MULTI |X +|OBJECT |- +|PERSIST |X +|PEXIPRE |X +|PEXPIREAT |X +|PFADD |X +|PFCOUNT |X +|PFMERGE |X +|PING |X +|PSETEX |X +|PSUBSCRIBE |X +|PTTL |X +|PUBLISH |X +|PUBSUB |- +|PUBSUBSCRIBE |- +|QUIT |X +|RANDOMKEY |X +|RENAME |X +|RENAMENX |X +|REPLICAOF |X +|RESTORE |X +|ROLE |- +|RPOP |X +|RPOPLPUSH |X +|RPUSH |X +|RPUSHX |X +|SADD |X +|SAVE |X +|SCAN |X +|SCARD |X +|SCRIPT EXITS |X +|SCRIPT FLUSH |X +|SCRIPT KILL |X +|SCRIPT LOAD |X +|SDIFF |X +|SDIFFSTORE |X +|SELECT |X +|SENTINEL FAILOVER |X |SENTINEL GET-MASTER-ADD-BY-NAME |- -|SENTINEL MASTER | - -|SENTINEL MASTERS |X -|SENTINEL MONITOR |X -|SENTINEL REMOVE |X -|SENTINEL RESET |- -|SENTINEL SET |- -|SENTINEL SLAVES |X -|SET |X -|SETBIT |X -|SETEX |X -|SETNX |X -|SETRANGE |X -|SHUTDOWN |X -|SINTER |X -|SINTERSTORE |X -|SISMEMBER |X -|SLAVEOF |X -|SLOWLOG |- -|SMEMBERS |X -|SMOVE |X -|SORT |X -|SPOP |X -|SRANDMEMBER |X -|SREM |X -|SSCAN |X -|STRLEN |X -|SUBSCRIBE |X -|SUNION |X -|SUNIONSTORE |X -|SYNC |- -|TIME |X -|TTL |X -|TYPE |X -|UNSUBSCRIBE |X -|UNWATCH |X -|WATCH |X -|ZADD |X -|ZCARD |X -|ZCOUNT |X -|ZINCRBY |X -|ZINTERSTORE |X -|ZLEXCOUNT |- -|ZRANGE |X -|ZRANGEBYLEX |- -|ZREVRANGEBYLEX |- -|ZRANGEBYSCORE |X -|ZRANGESTORE |X -|ZRANK |X -|ZREM |X -|ZREMRANGEBYLEX |- -|ZREMRANGEBYRANK |X -|ZREVRANGE |X -|ZREVRANGEBYSCORE |X -|ZREVRANK |X -|ZSCAN |X -|ZSCORE |X -|ZUNINONSTORE |X +|SENTINEL MASTER | - +|SENTINEL MASTERS |X +|SENTINEL MONITOR |X +|SENTINEL REMOVE |X +|SENTINEL RESET |- +|SENTINEL SET |- +|SENTINEL SLAVES |X +|SET |X +|SETBIT |X +|SETEX |X +|SETNX |X +|SETRANGE |X +|SHUTDOWN |X +|SINTER |X +|SINTERSTORE |X +|SISMEMBER |X +|SLAVEOF |X +|SLOWLOG |- +|SMEMBERS |X +|SMOVE |X +|SORT |X +|SPOP |X +|SRANDMEMBER |X +|SREM |X +|SSCAN |X +|STRLEN |X +|SUBSCRIBE |X +|SUNION |X +|SUNIONSTORE |X +|SYNC |- +|TIME |X +|TTL |X +|TYPE |X +|UNSUBSCRIBE |X +|UNWATCH |X +|WATCH |X +|ZADD |X +|ZCARD |X +|ZCOUNT |X +|ZINCRBY |X +|ZINTERSTORE |X +|ZLEXCOUNT |- +|ZRANGE |X +|ZRANGEBYLEX |- +|ZREVRANGEBYLEX |- +|ZRANGEBYSCORE |X +|ZRANGESTORE |X +|ZRANK |X +|ZREM |X +|ZREMRANGEBYLEX |- +|ZREMRANGEBYRANK |X +|ZREVRANGE |X +|ZREVRANGEBYSCORE |X +|ZREVRANK |X +|ZSCAN |X +|ZSCORE |X +|ZUNINONSTORE |X |========================================================= diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java index c3f13ef4a..831a46ece 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -23,6 +23,7 @@ import java.util.function.IntFunction; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.core.convert.converter.Converter; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; @@ -35,10 +36,19 @@ import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.connection.convert.ListConverter; import org.springframework.data.redis.connection.convert.MapConverter; import org.springframework.data.redis.connection.convert.SetConverter; -import org.springframework.data.redis.connection.stream.*; +import org.springframework.data.redis.connection.stream.ByteRecord; +import org.springframework.data.redis.connection.stream.Consumer; +import org.springframework.data.redis.connection.stream.MapRecord; +import org.springframework.data.redis.connection.stream.PendingMessages; +import org.springframework.data.redis.connection.stream.PendingMessagesSummary; +import org.springframework.data.redis.connection.stream.ReadOffset; +import org.springframework.data.redis.connection.stream.RecordId; import org.springframework.data.redis.connection.stream.StreamInfo.XInfoConsumers; import org.springframework.data.redis.connection.stream.StreamInfo.XInfoGroups; import org.springframework.data.redis.connection.stream.StreamInfo.XInfoStream; +import org.springframework.data.redis.connection.stream.StreamOffset; +import org.springframework.data.redis.connection.stream.StreamReadOptions; +import org.springframework.data.redis.connection.stream.StringRecord; import org.springframework.data.redis.connection.zset.Aggregate; import org.springframework.data.redis.connection.zset.DefaultTuple; import org.springframework.data.redis.connection.zset.Tuple; @@ -2566,35 +2576,36 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return this.delegate.hScan(key, options); } - @Nullable @Override public Long hStrLen(byte[] key, byte[] field) { return convertAndReturn(delegate.hStrLen(key, field), Converters.identityConverter()); } - public @Nullable List expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, + public @Nullable List applyExpiration(byte[] key, + org.springframework.data.redis.core.types.Expiration expiration, FieldExpirationOptions options, byte[]... fields) { - return this.delegate.expireHashField(key, expiration, options, fields); + return this.delegate.applyExpiration(key, expiration, options, fields); } @Override - public List hExpire(byte[] key, long seconds, byte[]... fields) { - return this.delegate.hExpire(key, seconds, fields); + public List hExpire(byte[] key, long seconds, FieldExpirationOptions.Condition condition, byte[]... fields) { + return this.delegate.hExpire(key, seconds, condition, fields); } @Override - public List hpExpire(byte[] key, long millis, byte[]... fields) { - return this.delegate.hpExpire(key, millis, fields); + public List hpExpire(byte[] key, long millis, FieldExpirationOptions.Condition condition, byte[]... fields) { + return this.delegate.hpExpire(key, millis, condition, fields); } @Override - public List hExpireAt(byte[] key, long unixTime, byte[]... fields) { - return this.delegate.hExpireAt(key, unixTime, fields); + public List hExpireAt(byte[] key, long unixTime, FieldExpirationOptions.Condition condition, byte[]... fields) { + return this.delegate.hExpireAt(key, unixTime, condition, fields); } @Override - public List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { - return this.delegate.hpExpireAt(key, unixTimeInMillis, fields); + public List hpExpireAt(byte[] key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, + byte[]... fields) { + return this.delegate.hpExpireAt(key, unixTimeInMillis, condition, fields); } @Override @@ -2617,29 +2628,31 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return this.delegate.hTtl(key, timeUnit, fields); } - public @Nullable List expireHashField(String key, org.springframework.data.redis.core.types.Expiration expiration, + public @Nullable List applyExpiration(String key, + org.springframework.data.redis.core.types.Expiration expiration, FieldExpirationOptions options, String... fields) { - return expireHashField(serialize(key), expiration, options, serializeMulti(fields)); + return applyExpiration(serialize(key), expiration, options, serializeMulti(fields)); } @Override - public List hExpire(String key, long seconds, String... fields) { - return hExpire(serialize(key), seconds, serializeMulti(fields)); + public List hExpire(String key, long seconds, FieldExpirationOptions.Condition condition, String... fields) { + return hExpire(serialize(key), seconds, condition, serializeMulti(fields)); } @Override - public List hpExpire(String key, long millis, String... fields) { - return hpExpire(serialize(key), millis, serializeMulti(fields)); + public List hpExpire(String key, long millis, FieldExpirationOptions.Condition condition, String... fields) { + return hpExpire(serialize(key), millis, condition, serializeMulti(fields)); } @Override - public List hExpireAt(String key, long unixTime, String... fields) { - return hExpireAt(serialize(key), unixTime, serializeMulti(fields)); + public List hExpireAt(String key, long unixTime, FieldExpirationOptions.Condition condition, String... fields) { + return hExpireAt(serialize(key), unixTime, condition, serializeMulti(fields)); } @Override - public List hpExpireAt(String key, long unixTimeInMillis, String... fields) { - return hpExpireAt(serialize(key), unixTimeInMillis, serializeMulti(fields)); + public List hpExpireAt(String key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, + String... fields) { + return hpExpireAt(serialize(key), unixTimeInMillis, condition, serializeMulti(fields)); } @Override diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java index 979cf5300..8190caa6a 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -1483,28 +1483,58 @@ public interface DefaultedRedisConnection extends RedisCommands, RedisCommandsPr @Override @Deprecated default List hExpire(byte[] key, long seconds, byte[]... fields) { - return hashCommands().hExpire(key, seconds, fields); + return hashCommands().hExpire(key, seconds, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hExpire(byte[] key, long seconds, FieldExpirationOptions.Condition condition, byte[]... fields) { + return hashCommands().hExpire(key, seconds, condition, fields); } /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @Override @Deprecated default List hpExpire(byte[] key, long millis, byte[]... fields) { - return hashCommands().hpExpire(key, millis, fields); + return hashCommands().hpExpire(key, millis, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hpExpire(byte[] key, long millis, FieldExpirationOptions.Condition condition, byte[]... fields) { + return hashCommands().hpExpire(key, millis, condition, fields); } /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @Override @Deprecated default List hExpireAt(byte[] key, long unixTime, byte[]... fields) { - return hashCommands().hExpireAt(key, unixTime, fields); + return hashCommands().hExpireAt(key, unixTime, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hExpireAt(byte[] key, long unixTime, FieldExpirationOptions.Condition condition, + byte[]... fields) { + return hashCommands().hExpireAt(key, unixTime, condition, fields); } /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @Override @Deprecated default List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { - return hashCommands().hpExpireAt(key, unixTimeInMillis, fields); + return hashCommands().hpExpireAt(key, unixTimeInMillis, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hpExpireAt(byte[] key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, + byte[]... fields) { + return hashCommands().hpExpireAt(key, unixTimeInMillis, condition, fields); } /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @@ -1538,9 +1568,10 @@ public interface DefaultedRedisConnection extends RedisCommands, RedisCommandsPr /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @Override @Deprecated - default @Nullable List expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, - FieldExpirationOptions options, byte[]... fields) { - return hashCommands().expireHashField(key, expiration, options, fields); + default @Nullable List applyExpiration(byte[] key, + org.springframework.data.redis.core.types.Expiration expiration, FieldExpirationOptions options, + byte[]... fields) { + return hashCommands().applyExpiration(key, expiration, options, fields); } // GEO COMMANDS @@ -1914,9 +1945,8 @@ public interface DefaultedRedisConnection extends RedisCommands, RedisCommandsPr /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ @Override @Deprecated - default Long zRangeStoreByLex(byte[] dstKey, byte[] srcKey, - org.springframework.data.domain.Range range, - org.springframework.data.redis.connection.Limit limit) { + default Long zRangeStoreByLex(byte[] dstKey, byte[] srcKey, org.springframework.data.domain.Range range, + org.springframework.data.redis.connection.Limit limit) { return zSetCommands().zRangeStoreByLex(dstKey, srcKey, range, limit); } @@ -1933,7 +1963,7 @@ public interface DefaultedRedisConnection extends RedisCommands, RedisCommandsPr @Deprecated default Long zRangeStoreByScore(byte[] dstKey, byte[] srcKey, org.springframework.data.domain.Range range, - org.springframework.data.redis.connection.Limit limit) { + org.springframework.data.redis.connection.Limit limit) { return zSetCommands().zRangeStoreByScore(dstKey, srcKey, range, limit); } diff --git a/src/main/java/org/springframework/data/redis/connection/Hash.java b/src/main/java/org/springframework/data/redis/connection/Hash.java index c8d0e8a24..51e326dd2 100644 --- a/src/main/java/org/springframework/data/redis/connection/Hash.java +++ b/src/main/java/org/springframework/data/redis/connection/Hash.java @@ -18,21 +18,25 @@ package org.springframework.data.redis.connection; import java.util.Objects; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** + * Types for interacting with Hash data structures. + * * @author Christoph Strobl * @since 3.5 */ public interface Hash { + /** + * Expiration options for Hash Expiation updates. + */ class FieldExpirationOptions { - private static final FieldExpirationOptions NONE = new FieldExpirationOptions(null); - private @Nullable Condition condition; + private static final FieldExpirationOptions NONE = new FieldExpirationOptions(Condition.ALWAYS); + private final Condition condition; - FieldExpirationOptions(@Nullable Condition condition) { + FieldExpirationOptions(Condition condition) { this.condition = condition; } @@ -40,12 +44,11 @@ public interface Hash { return NONE; } - @Contract("_ -> new") public static FieldExpireOptionsBuilder builder() { return new FieldExpireOptionsBuilder(); } - public @Nullable Condition getCondition() { + public Condition getCondition() { return condition; } @@ -68,48 +71,67 @@ public interface Hash { public static class FieldExpireOptionsBuilder { - @Nullable Condition condition; + private Condition condition = Condition.ALWAYS; - @Contract("_ -> this") + @Contract("-> this") public FieldExpireOptionsBuilder nx() { this.condition = Condition.NX; return this; } - @Contract("_ -> this") + @Contract("-> this") public FieldExpireOptionsBuilder xx() { this.condition = Condition.XX; return this; } - @Contract("_ -> this") + @Contract("-> this") public FieldExpireOptionsBuilder gt() { this.condition = Condition.GT; return this; } - @Contract("_ -> this") + @Contract("-> this") public FieldExpireOptionsBuilder lt() { this.condition = Condition.LT; return this; } - @Contract("_ -> !null") public FieldExpirationOptions build() { - return condition == null ? NONE : new FieldExpirationOptions(condition); + return condition == Condition.ALWAYS ? NONE : new FieldExpirationOptions(condition); } + } public enum Condition { - /** Set expiration only when the field has no expiration. */ + /** + * Always apply expiration. + */ + ALWAYS, + + /** + * Set expiration only when the field has no expiration. + */ NX, - /** Set expiration only when the field has an existing expiration. */ + + /** + * Set expiration only when the field has an existing expiration. + */ XX, - /** Set expiration only when the new expiration is greater than current one. */ + + /** + * Set expiration only when the new expiration is greater than current one. + */ GT, - /** Set expiration only when the new expiration is greater than current one. */ + + /** + * Set expiration only when the new expiration is greater than current one. + */ LT + } + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java index f58f4e32a..1e9fd94f3 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java @@ -858,7 +858,9 @@ public interface ReactiveHashCommands { private ExpireCommand(@Nullable ByteBuffer key, List fields, Expiration expiration, FieldExpirationOptions options) { + super(key, fields); + this.expiration = expiration; this.options = options; } @@ -921,6 +923,7 @@ public interface ReactiveHashCommands { default Mono hExpire(ByteBuffer key, Duration duration, ByteBuffer field) { Assert.notNull(duration, "Duration must not be null"); + return hExpire(key, duration, Collections.singletonList(field)).singleOrEmpty(); } @@ -939,9 +942,10 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Flux hExpire(ByteBuffer key, Duration duration, List fields) { + Assert.notNull(duration, "Duration must not be null"); - return expireHashField(Flux.just(ExpireCommand.expire(fields, duration).from(key))) + return applyExpiration(Flux.just(ExpireCommand.expire(fields, duration).from(key))) .mapNotNull(NumericResponse::getOutput); } @@ -957,7 +961,7 @@ public interface ReactiveHashCommands { * @since 3.5 * @see Redis Documentation: HEXPIRE */ - Flux> expireHashField(Publisher commands); + Flux> applyExpiration(Publisher commands); /** * Expire a given {@literal field} after a given {@link Duration} of time, measured in milliseconds, has passed. @@ -975,6 +979,7 @@ public interface ReactiveHashCommands { default Mono hpExpire(ByteBuffer key, Duration duration, ByteBuffer field) { Assert.notNull(duration, "Duration must not be null"); + return hpExpire(key, duration, Collections.singletonList(field)).singleOrEmpty(); } @@ -995,7 +1000,8 @@ public interface ReactiveHashCommands { default Flux hpExpire(ByteBuffer key, Duration duration, List fields) { Assert.notNull(duration, "Duration must not be null"); - return expireHashField(Flux.just(new ExpireCommand(key, fields, + + return applyExpiration(Flux.just(new ExpireCommand(key, fields, Expiration.from(duration.toMillis(), TimeUnit.MILLISECONDS), FieldExpirationOptions.none()))) .mapNotNull(NumericResponse::getOutput); } @@ -1017,6 +1023,7 @@ public interface ReactiveHashCommands { default Mono hExpireAt(ByteBuffer key, Instant expireAt, ByteBuffer field) { Assert.notNull(expireAt, "Duration must not be null"); + return hExpireAt(key, expireAt, Collections.singletonList(field)).singleOrEmpty(); } @@ -1035,9 +1042,10 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Flux hExpireAt(ByteBuffer key, Instant expireAt, List fields) { + Assert.notNull(expireAt, "Duration must not be null"); - return expireHashField(Flux.just(ExpireCommand.expireAt(fields, expireAt, TimeUnit.SECONDS).from(key))) + return applyExpiration(Flux.just(ExpireCommand.expireAt(fields, expireAt, TimeUnit.SECONDS).from(key))) .mapNotNull(NumericResponse::getOutput); } @@ -1058,6 +1066,7 @@ public interface ReactiveHashCommands { default Mono hpExpireAt(ByteBuffer key, Instant expireAt, ByteBuffer field) { Assert.notNull(expireAt, "Duration must not be null"); + return hpExpireAt(key, expireAt, Collections.singletonList(field)).singleOrEmpty(); } @@ -1076,9 +1085,10 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Flux hpExpireAt(ByteBuffer key, Instant expireAt, List fields) { + Assert.notNull(expireAt, "Duration must not be null"); - return expireHashField(Flux.just(ExpireCommand.expireAt(fields, expireAt, TimeUnit.MILLISECONDS).from(key))) + return applyExpiration(Flux.just(ExpireCommand.expireAt(fields, expireAt, TimeUnit.MILLISECONDS).from(key))) .mapNotNull(NumericResponse::getOutput); } @@ -1135,7 +1145,6 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Mono hTtl(ByteBuffer key, ByteBuffer field) { - return hTtl(key, Collections.singletonList(field)).singleOrEmpty(); } @@ -1151,7 +1160,6 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Flux hTtl(ByteBuffer key, List fields) { - return hTtl(Flux.just(new HashFieldsCommand(key, fields))).mapNotNull(NumericResponse::getOutput); } @@ -1179,7 +1187,6 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Mono hpTtl(ByteBuffer key, ByteBuffer field) { - return hpTtl(key, Collections.singletonList(field)).singleOrEmpty(); } @@ -1195,7 +1202,6 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Flux hpTtl(ByteBuffer key, List fields) { - return hpTtl(Flux.just(new HashFieldsCommand(key, fields))).mapNotNull(NumericResponse::getOutput); } @@ -1210,4 +1216,5 @@ public interface ReactiveHashCommands { * @see Redis Documentation: HPTTL */ Flux> hpTtl(Publisher commands); + } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java index 5fde9d5db..f2d736b8e 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java @@ -25,6 +25,7 @@ import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; /** * Hash-specific commands supported by Redis. @@ -254,41 +255,78 @@ public interface RedisHashCommands { @Nullable Long hStrLen(byte[] key, byte[] field); - default @Nullable List expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, - byte[]... fields) { - return expireHashField(key, expiration, FieldExpirationOptions.none(), fields); + default @Nullable List applyExpiration(byte[] key, + org.springframework.data.redis.core.types.Expiration expiration, byte[]... fields) { + return applyExpiration(key, expiration, FieldExpirationOptions.none(), fields); } - - @Nullable List expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, - FieldExpirationOptions options, byte[]... fields); - - /** - * Set time to live for given {@code fields} in seconds. - * - * @param key must not be {@literal null}. - * @param seconds the amount of time after which the fields will be expired in seconds, must not be {@literal null}. - * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HEXPIRE - * @since 3.5 - */ @Nullable - List hExpire(byte[] key, long seconds, byte[]... fields); + default List applyExpiration(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, + FieldExpirationOptions options, byte[]... fields) { + + if (expiration.isPersistent()) { + return hPersist(key, fields); + } + + if (ObjectUtils.nullSafeEquals(FieldExpirationOptions.none(), options)) { + if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { + if (expiration.isUnixTimestamp()) { + return hpExpireAt(key, expiration.getExpirationTimeInMilliseconds(), fields); + } + return hpExpire(key, expiration.getExpirationTimeInMilliseconds(), fields); + } + if (expiration.isUnixTimestamp()) { + return hExpireAt(key, expiration.getExpirationTimeInSeconds(), fields); + } + return hExpire(key, expiration.getExpirationTimeInSeconds(), fields); + } + + if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { + if (expiration.isUnixTimestamp()) { + return hpExpireAt(key, expiration.getExpirationTimeInMilliseconds(), options.getCondition(), fields); + } + + return hpExpire(key, expiration.getExpirationTimeInMilliseconds(), options.getCondition(), fields); + } + + if (expiration.isUnixTimestamp()) { + return hExpireAt(key, expiration.getExpirationTimeInSeconds(), options.getCondition(), fields); + } + + return hExpire(key, expiration.getExpirationTimeInSeconds(), options.getCondition(), fields); + } + + /** + * Set time to live for given {@code fields} in seconds. + * + * @param key must not be {@literal null}. + * @param seconds the amount of time after which the fields will be expired in seconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + default List hExpire(byte[] key, long seconds, byte[]... fields) { + return hExpire(key, seconds, FieldExpirationOptions.Condition.ALWAYS, fields); + } /** * Set time to live for given {@code fields}. * * @param key must not be {@literal null}. - * @param ttl the amount of time after which the fields will be expired in {@link Duration#toSeconds() seconds} precision, must not be {@literal null}. + * @param ttl the amount of time after which the fields will be expired in {@link Duration#toSeconds() seconds} + * precision, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HEXPIRE * @since 3.5 */ @@ -298,31 +336,55 @@ public interface RedisHashCommands { } /** - * Set time to live for given {@code fields} in milliseconds. + * Set time to live for given {@code fields} in seconds. * * @param key must not be {@literal null}. - * @param millis the amount of time after which the fields will be expired in milliseconds, must not be {@literal null}. + * @param seconds the amount of time after which the fields will be expired in seconds, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HPEXPIRE + * @param condition the condition for expiration, must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HEXPIRE * @since 3.5 */ @Nullable - List hpExpire(byte[] key, long millis, byte[]... fields); + List hExpire(byte[] key, long seconds, FieldExpirationOptions.Condition condition, byte[]... fields); /** * Set time to live for given {@code fields} in milliseconds. * * @param key must not be {@literal null}. - * @param ttl the amount of time after which the fields will be expired in {@link Duration#toMillis() milliseconds} precision, must not be {@literal null}. + * @param millis the amount of time after which the fields will be expired in milliseconds, must not be + * {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HPEXPIRE + * @since 3.5 + */ + @Nullable + default List hpExpire(byte[] key, long millis, byte[]... fields) { + return hpExpire(key, millis, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set time to live for given {@code fields} in milliseconds. + * + * @param key must not be {@literal null}. + * @param ttl the amount of time after which the fields will be expired in {@link Duration#toMillis() milliseconds} + * precision, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HPEXPIRE * @since 3.5 */ @@ -331,21 +393,61 @@ public interface RedisHashCommands { return hpExpire(key, ttl.toMillis(), fields); } + /** + * Set time to live for given {@code fields} in milliseconds. + * + * @param key must not be {@literal null}. + * @param millis the amount of time after which the fields will be expired in milliseconds, must not be + * {@literal null}. + * @param condition the condition for expiration, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HPEXPIRE + * @since 3.5 + */ + @Nullable + List hpExpire(byte[] key, long millis, FieldExpirationOptions.Condition condition, byte[]... fields); + /** * Set the expiration for given {@code field} as a {@literal UNIX} timestamp. * * @param key must not be {@literal null}. * @param unixTime the moment in time in which the field expires, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not - * met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HEXPIREAT * @since 3.5 */ @Nullable - List hExpireAt(byte[] key, long unixTime, byte[]... fields); + default List hExpireAt(byte[] key, long unixTime, byte[]... fields) { + return hExpireAt(key, unixTime, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp. + * + * @param key must not be {@literal null}. + * @param unixTime the moment in time in which the field expires, must not be {@literal null}. + * @param condition the condition for expiration, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HEXPIREAT + * @since 3.5 + */ + @Nullable + List hExpireAt(byte[] key, long unixTime, FieldExpirationOptions.Condition condition, byte[]... fields); /** * Set the expiration for given {@code field} as a {@literal UNIX} timestamp in milliseconds. @@ -353,24 +455,47 @@ public interface RedisHashCommands { * @param key must not be {@literal null}. * @param unixTimeInMillis the moment in time in which the field expires in milliseconds, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not - * met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HPEXPIREAT * @since 3.5 */ @Nullable - List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields); + default List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { + return hpExpireAt(key, unixTimeInMillis, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp in milliseconds. + * + * @param key must not be {@literal null}. + * @param unixTimeInMillis the moment in time in which the field expires in milliseconds, must not be {@literal null}. + * @param condition the condition for expiration, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HPEXPIREAT + * @since 3.5 + */ + @Nullable + List hpExpireAt(byte[] key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, + byte[]... fields); /** * Remove the expiration from given {@code field}. * * @param key must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is removed; - * {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such field; - * {@literal null} when used in pipeline / transaction.{@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is + * removed; {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction.{@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HPERSIST * @since 3.5 */ @@ -382,9 +507,10 @@ public interface RedisHashCommands { * * @param key must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the field exists but has no associated expiration time. - * The command returns {@code -2} if the field does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the field exists but has no associated + * expiration time. The command returns {@code -2} if the field does not exist; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @@ -397,9 +523,10 @@ public interface RedisHashCommands { * @param key must not be {@literal null}. * @param timeUnit must not be {@literal null}. * @param fields must not be {@literal null}. - * @return for each of the fields supplied - the time to live in the {@link TimeUnit} provided; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. - * The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return for each of the fields supplied - the time to live in the {@link TimeUnit} provided; or a negative value to + * signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. + * The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @@ -413,9 +540,10 @@ public interface RedisHashCommands { * * @param key must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. - * The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the key exists but has no associated expiration + * time. The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HTTL * @since 3.5 */ diff --git a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index ed0101641..5ff0e9946 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -71,7 +71,6 @@ import org.springframework.util.CollectionUtils; * @author Andrey Shlykov * @author ihaohong * @author Shyngys Sapraliyev - * * @see RedisCallback * @see RedisSerializer * @see StringRedisTemplate @@ -1661,7 +1660,6 @@ public interface StringRedisConnection extends RedisConnection { */ Long zRemRange(String key, long start, long end); - /** * Remove all elements between the lexicographical {@link Range}. * @@ -1941,7 +1939,8 @@ public interface StringRedisConnection extends RedisConnection { * @return * @since 1.6 * @see Redis Documentation: ZRANGEBYLEX - * @see RedisZSetCommands#zRangeByLex(byte[], org.springframework.data.domain.Range, org.springframework.data.redis.connection.Limit) + * @see RedisZSetCommands#zRangeByLex(byte[], org.springframework.data.domain.Range, + * org.springframework.data.redis.connection.Limit) */ Set zRangeByLex(String key, org.springframework.data.domain.Range range, org.springframework.data.redis.connection.Limit limit); @@ -1983,7 +1982,8 @@ public interface StringRedisConnection extends RedisConnection { * @return * @since 2.4 * @see Redis Documentation: ZREVRANGEBYLEX - * @see RedisZSetCommands#zRevRangeByLex(byte[], org.springframework.data.domain.Range, org.springframework.data.redis.connection.Limit) + * @see RedisZSetCommands#zRevRangeByLex(byte[], org.springframework.data.domain.Range, + * org.springframework.data.redis.connection.Limit) */ Set zRevRangeByLex(String key, org.springframework.data.domain.Range range, org.springframework.data.redis.connection.Limit limit); @@ -2333,22 +2333,41 @@ public interface StringRedisConnection extends RedisConnection { @Nullable Long hStrLen(String key, String field); - // TODO: why why whay is this such a shitty api that there's missing all the NX, XX, GT Options /** * Set time to live for given {@code field} in seconds. * * @param key must not be {@literal null}. * @param seconds the amount of time after which the key will be expired in seconds, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HEXPIRE - * @since 3.4 + * @since 3.5 */ @Nullable - List hExpire(String key, long seconds, String... fields); + default List hExpire(String key, long seconds, String... fields) { + return hExpire(key, seconds, Hash.FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set time to live for given {@code field} in seconds. + * + * @param key must not be {@literal null}. + * @param seconds the amount of time after which the key will be expired in seconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + List hExpire(String key, long seconds, Hash.FieldExpirationOptions.Condition condition, String... fields); /** * Set time to live for given {@code field} in milliseconds. @@ -2356,15 +2375,35 @@ public interface StringRedisConnection extends RedisConnection { * @param key must not be {@literal null}. * @param millis the amount of time after which the key will be expired in milliseconds, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HPEXPIRE - * @since 3.4 + * @since 3.5 */ @Nullable - List hpExpire(String key, long millis, String... fields); + default List hpExpire(String key, long millis, String... fields) { + return hpExpire(key, millis, Hash.FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set time to live for given {@code field} in milliseconds. + * + * @param key must not be {@literal null}. + * @param millis the amount of time after which the key will be expired in milliseconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HPEXPIRE + * @since 3.5 + */ + @Nullable + List hpExpire(String key, long millis, Hash.FieldExpirationOptions.Condition condition, String... fields); /** * Set the expiration for given {@code field} as a {@literal UNIX} timestamp. @@ -2372,15 +2411,35 @@ public interface StringRedisConnection extends RedisConnection { * @param key must not be {@literal null}. * @param unixTime the moment in time in which the field expires, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not - * met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HEXPIREAT - * @since 3.4 + * @since 3.5 */ @Nullable - List hExpireAt(String key, long unixTime, String... fields); + default List hExpireAt(String key, long unixTime, String... fields) { + return hExpireAt(key, unixTime, Hash.FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp. + * + * @param key must not be {@literal null}. + * @param unixTime the moment in time in which the field expires, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HEXPIREAT + * @since 3.5 + */ + @Nullable + List hExpireAt(String key, long unixTime, Hash.FieldExpirationOptions.Condition condition, String... fields); /** * Set the expiration for given {@code field} as a {@literal UNIX} timestamp in milliseconds. @@ -2388,26 +2447,48 @@ public interface StringRedisConnection extends RedisConnection { * @param key must not be {@literal null}. * @param unixTimeInMillis the moment in time in which the field expires in milliseconds, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not - * met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HPEXPIREAT - * @since 3.4 + * @since 3.5 */ @Nullable - List hpExpireAt(String key, long unixTimeInMillis, String... fields); + default List hpExpireAt(String key, long unixTimeInMillis, String... fields) { + return hpExpireAt(key, unixTimeInMillis, Hash.FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp in milliseconds. + * + * @param key must not be {@literal null}. + * @param unixTimeInMillis the moment in time in which the field expires in milliseconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HPEXPIREAT + * @since 3.5 + */ + @Nullable + List hpExpireAt(String key, long unixTimeInMillis, Hash.FieldExpirationOptions.Condition condition, + String... fields); /** * Remove the expiration from given {@code field}. * * @param key must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is removed; - * {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such field; - * {@literal null} when used in pipeline / transaction.{@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is + * removed; {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction.{@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HPERSIST - * @since 3.4 + * @since 3.5 */ @Nullable List hPersist(String key, String... fields); @@ -2417,9 +2498,10 @@ public interface StringRedisConnection extends RedisConnection { * * @param key must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in milliseconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. - * The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in milliseconds; or a + * negative value to signal an error. The command returns {@code -1} if the key exists but has no associated + * expiration time. The command returns {@code -2} if the key does not exist; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @@ -2432,9 +2514,10 @@ public interface StringRedisConnection extends RedisConnection { * @param key must not be {@literal null}. * @param timeUnit must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in the {@link TimeUnit} provided; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. - * The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in the {@link TimeUnit} + * provided; or a negative value to signal an error. The command returns {@code -1} if the key exists but has + * no associated expiration time. The command returns {@code -2} if the key does not exist; {@literal null} + * when used in pipeline / transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @@ -2446,9 +2529,10 @@ public interface StringRedisConnection extends RedisConnection { * * @param key must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in milliseconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. - * The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in milliseconds; or a + * negative value to signal an error. The command returns {@code -1} if the key exists but has no associated + * expiration time. The command returns {@code -2} if the key does not exist; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @@ -2678,8 +2762,7 @@ public interface StringRedisConnection extends RedisConnection { /** * Return the members of a geo set which are within the borders of the area specified by a given {@link GeoShape - * shape}. The query's center point is provided by - * {@link GeoReference}. + * shape}. The query's center point is provided by {@link GeoReference}. * * @param key must not be {@literal null}. * @param reference must not be {@literal null}. @@ -2695,8 +2778,7 @@ public interface StringRedisConnection extends RedisConnection { /** * Query the members of a geo set which are within the borders of the area specified by a given {@link GeoShape shape} - * and store the result at {@code destKey}. The query's center point is provided by - * {@link GeoReference}. + * and store the result at {@code destKey}. The query's center point is provided by {@link GeoReference}. * * @param key must not be {@literal null}. * @param reference must not be {@literal null}. diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java index 3326a00d6..1223ab4c0 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java @@ -33,10 +33,8 @@ import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanCursor; import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; -import org.springframework.data.redis.core.types.Expiration; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * Cluster {@link RedisHashCommands} implementation for Jedis. @@ -292,88 +290,71 @@ class JedisClusterHashCommands implements RedisHashCommands { }.open(); } - @Nullable @Override - public List expireHashField(byte[] key, Expiration expiration, FieldExpirationOptions options, + public List hExpire(byte[] key, long seconds, FieldExpirationOptions.Condition condition, byte[]... fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.getCluster().hexpire(key, seconds, fields); + } + + return connection.getCluster().hexpire(key, seconds, ExpiryOption.valueOf(condition.name()), fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hpExpire(byte[] key, long millis, FieldExpirationOptions.Condition condition, byte[]... fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.getCluster().hpexpire(key, millis, fields); + } + + return connection.getCluster().hpexpire(key, millis, ExpiryOption.valueOf(condition.name()), fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hExpireAt(byte[] key, long unixTime, FieldExpirationOptions.Condition condition, byte[]... fields) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.getCluster().hexpireAt(key, unixTime, fields); + } + + return connection.getCluster().hexpireAt(key, unixTime, ExpiryOption.valueOf(condition.name()), fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hpExpireAt(byte[] key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, byte[]... fields) { - if (expiration.isPersistent()) { - return hPersist(key, fields); - } + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - if (ObjectUtils.nullSafeEquals(FieldExpirationOptions.none(), options)) { - if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { - if (expiration.isUnixTimestamp()) { - return hpExpireAt(key, expiration.getExpirationTimeInMilliseconds(), fields); - } - return hpExpire(key, expiration.getExpirationTimeInMilliseconds(), fields); + try { + + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.getCluster().hpexpireAt(key, unixTimeInMillis, fields); } - if (expiration.isUnixTimestamp()) { - return hExpireAt(key, expiration.getExpirationTimeInSeconds(), fields); - } - return hExpire(key, expiration.getExpirationTimeInSeconds(), fields); - } - ExpiryOption option = ExpiryOption.valueOf(options.getCondition().name()); - - if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { - if (expiration.isUnixTimestamp()) { - return connection.getCluster().hpexpireAt(key, expiration.getExpirationTimeInMilliseconds(), option, fields); - } - return connection.getCluster().hpexpire(key, expiration.getExpirationTimeInMilliseconds(), option, fields); - } - - if (expiration.isUnixTimestamp()) { - return connection.getCluster().hexpireAt(key, expiration.getExpirationTimeInSeconds(), option, fields); - } - return connection.getCluster().hexpire(key, expiration.getExpirationTimeInSeconds(), option, fields); - - } - - @Override - public List hExpire(byte[] key, long seconds, byte[]... fields) { - - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); - - try { - return connection.getCluster().hexpire(key, seconds, fields); - } catch (Exception ex) { - throw convertJedisAccessException(ex); - } - } - - @Override - public List hpExpire(byte[] key, long millis, byte[]... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); - - try { - return connection.getCluster().hpexpire(key, millis, fields); - } catch (Exception ex) { - throw convertJedisAccessException(ex); - } - } - - @Override - public List hExpireAt(byte[] key, long unixTime, byte[]... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); - - try { - return connection.getCluster().hexpireAt(key, unixTime, fields); - } catch (Exception ex) { - throw convertJedisAccessException(ex); - } - } - - @Override - public List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); - - try { - return connection.getCluster().hpexpireAt(key, unixTimeInMillis, fields); + return connection.getCluster().hpexpireAt(key, unixTimeInMillis, ExpiryOption.valueOf(condition.name()), fields); } catch (Exception ex) { throw convertJedisAccessException(ex); } @@ -381,6 +362,7 @@ class JedisClusterHashCommands implements RedisHashCommands { @Override public List hPersist(byte[] key, byte[]... fields) { + Assert.notNull(key, "Key must not be null"); Assert.notNull(fields, "Fields must not be null"); @@ -393,6 +375,7 @@ class JedisClusterHashCommands implements RedisHashCommands { @Override public List hTtl(byte[] key, byte[]... fields) { + Assert.notNull(key, "Key must not be null"); Assert.notNull(fields, "Fields must not be null"); @@ -405,6 +388,7 @@ class JedisClusterHashCommands implements RedisHashCommands { @Override public List hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields) { + Assert.notNull(key, "Key must not be null"); Assert.notNull(fields, "Fields must not be null"); @@ -418,6 +402,7 @@ class JedisClusterHashCommands implements RedisHashCommands { @Override public List hpTtl(byte[] key, byte[]... fields) { + Assert.notNull(key, "Key must not be null"); Assert.notNull(fields, "Fields must not be null"); @@ -439,7 +424,7 @@ class JedisClusterHashCommands implements RedisHashCommands { } private DataAccessException convertJedisAccessException(Exception ex) { - return connection.convertJedisAccessException(ex); } + } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java index e8751e85c..2e83d8aba 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java @@ -39,7 +39,6 @@ import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * {@link RedisHashCommands} implementation for Jedis. @@ -257,62 +256,48 @@ class JedisHashCommands implements RedisHashCommands { } @Override - public List hExpire(byte[] key, long seconds, byte[]... fields) { - return connection.invoke().just(Jedis::hexpire, PipelineBinaryCommands::hexpire, key, seconds, fields); + public List hExpire(byte[] key, long seconds, FieldExpirationOptions.Condition condition, byte[]... fields) { + + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.invoke().just(Jedis::hexpire, PipelineBinaryCommands::hexpire, key, seconds, fields); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); + return connection.invoke().just(Jedis::hexpire, PipelineBinaryCommands::hexpire, key, seconds, option, fields); } @Override - public List hpExpire(byte[] key, long millis, byte[]... fields) { - return connection.invoke().just(Jedis::hpexpire, PipelineBinaryCommands::hpexpire, key, millis, fields); + public List hpExpire(byte[] key, long millis, FieldExpirationOptions.Condition condition, byte[]... fields) { + + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.invoke().just(Jedis::hpexpire, PipelineBinaryCommands::hpexpire, key, millis, fields); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); + return connection.invoke().just(Jedis::hpexpire, PipelineBinaryCommands::hpexpire, key, millis, option, fields); } @Override - public @Nullable List expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, - FieldExpirationOptions options, byte[]... fields) { + public List hExpireAt(byte[] key, long unixTime, FieldExpirationOptions.Condition condition, byte[]... fields) { - if (expiration.isPersistent()) { - return hPersist(key, fields); + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.invoke().just(Jedis::hexpireAt, PipelineBinaryCommands::hexpireAt, key, unixTime, fields); } - if (ObjectUtils.nullSafeEquals(FieldExpirationOptions.none(), options)) { - if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { - if (expiration.isUnixTimestamp()) { - return hpExpireAt(key, expiration.getExpirationTimeInMilliseconds(), fields); - } - return hpExpire(key, expiration.getExpirationTimeInMilliseconds(), fields); - } - if (expiration.isUnixTimestamp()) { - return hExpireAt(key, expiration.getExpirationTimeInSeconds(), fields); - } - return hExpire(key, expiration.getExpirationTimeInSeconds(), fields); - } - - ExpiryOption option = ExpiryOption.valueOf(options.getCondition().name()); - - if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { - if (expiration.isUnixTimestamp()) { - return connection.invoke().just(Jedis::hpexpireAt, PipelineBinaryCommands::hpexpireAt, key, - expiration.getExpirationTimeInMilliseconds(), option, fields); - } - return connection.invoke().just(Jedis::hpexpire, PipelineBinaryCommands::hpexpire, key, - expiration.getExpirationTimeInMilliseconds(), option, fields); - } - - if (expiration.isUnixTimestamp()) { - return connection.invoke().just(Jedis::hexpireAt, PipelineBinaryCommands::hexpireAt, key, - expiration.getExpirationTimeInSeconds(), option, fields); - } - return connection.invoke().just(Jedis::hexpire, PipelineBinaryCommands::hexpire, key, - expiration.getExpirationTimeInSeconds(), option, fields); + ExpiryOption option = ExpiryOption.valueOf(condition.name()); + return connection.invoke().just(Jedis::hexpireAt, PipelineBinaryCommands::hexpireAt, key, unixTime, option, fields); } @Override - public List hExpireAt(byte[] key, long unixTime, byte[]... fields) { - return connection.invoke().just(Jedis::hexpireAt, PipelineBinaryCommands::hexpireAt, key, unixTime, fields); - } + public List hpExpireAt(byte[] key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, + byte[]... fields) { - @Override - public List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.invoke().just(Jedis::hpexpireAt, PipelineBinaryCommands::hpexpireAt, key, unixTimeInMillis, + fields); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); return connection.invoke().just(Jedis::hpexpireAt, PipelineBinaryCommands::hpexpireAt, key, unixTimeInMillis, fields); } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java index fc8460d51..fe646b3a1 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java @@ -1160,6 +1160,14 @@ public class LettuceConnection extends AbstractRedisConnection { COMMAND_OUTPUT_TYPE_MAPPING.put(PFMERGE, IntegerOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(PFADD, IntegerOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HEXPIRE, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HEXPIREAT, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HPEXPIRE, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HPEXPIREAT, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HPERSIST, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HTTL, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HPTTL, IntegerListOutput.class); + // DOUBLE COMMAND_OUTPUT_TYPE_MAPPING.put(HINCRBYFLOAT, DoubleOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(INCRBYFLOAT, DoubleOutput.class); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java index 16564fd1e..032d6230d 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java @@ -215,63 +215,28 @@ class LettuceHashCommands implements RedisHashCommands { } @Override - public @Nullable List expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, - FieldExpirationOptions options, byte[]... fields) { - - if (expiration.isPersistent()) { - return hPersist(key, fields); - } - - ExpireArgs option = new ExpireArgs() { - @Override - public void build(CommandArgs args) { - - if(ObjectUtils.nullSafeEquals(options, FieldExpirationOptions.none())) { - return; - } - - args.add(options.getCondition().name()); - } - }; - - if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { - if (expiration.isUnixTimestamp()) { - return connection.invoke().fromMany(RedisHashAsyncCommands::hpexpireat, key, - expiration.getExpirationTimeInMilliseconds(), option, fields).toList(); - } - return connection.invoke() - .fromMany(RedisHashAsyncCommands::hpexpire, key, expiration.getExpirationTimeInMilliseconds(), option, fields) - .toList(); - } - - if (expiration.isUnixTimestamp()) { - return connection.invoke() - .fromMany(RedisHashAsyncCommands::hexpireat, key, expiration.getExpirationTimeInSeconds(), option, fields) - .toList(); - } - return connection.invoke() - .fromMany(RedisHashAsyncCommands::hexpire, key, expiration.getExpirationTimeInSeconds(), option, fields) + public List hExpire(byte[] key, long seconds, FieldExpirationOptions.Condition condition, byte[]... fields) { + return connection.invoke().fromMany(RedisHashAsyncCommands::hexpire, key, seconds, getExpireArgs(condition), fields) .toList(); } @Override - public List hExpire(byte[] key, long seconds, byte[]... fields) { - return connection.invoke().fromMany(RedisHashAsyncCommands::hexpire, key, seconds, fields).toList(); + public List hpExpire(byte[] key, long millis, FieldExpirationOptions.Condition condition, byte[]... fields) { + return connection.invoke().fromMany(RedisHashAsyncCommands::hpexpire, key, millis, getExpireArgs(condition), fields) + .toList(); } @Override - public List hpExpire(byte[] key, long millis, byte[]... fields) { - return connection.invoke().fromMany(RedisHashAsyncCommands::hpexpire, key, millis, fields).toList(); + public List hExpireAt(byte[] key, long unixTime, FieldExpirationOptions.Condition condition, byte[]... fields) { + return connection.invoke() + .fromMany(RedisHashAsyncCommands::hexpireat, key, unixTime, getExpireArgs(condition), fields).toList(); } @Override - public List hExpireAt(byte[] key, long unixTime, byte[]... fields) { - return connection.invoke().fromMany(RedisHashAsyncCommands::hexpireat, key, unixTime, fields).toList(); - } - - @Override - public List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { - return connection.invoke().fromMany(RedisHashAsyncCommands::hpexpireat, key, unixTimeInMillis, fields).toList(); + public List hpExpireAt(byte[] key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, + byte[]... fields) { + return connection.invoke() + .fromMany(RedisHashAsyncCommands::hpexpireat, key, unixTimeInMillis, getExpireArgs(condition), fields).toList(); } @Override @@ -349,4 +314,19 @@ class LettuceHashCommands implements RedisHashCommands { return value.hasValue() ? Converters.entryOf(value.getKey(), value.getValue()) : null; } + private ExpireArgs getExpireArgs(FieldExpirationOptions.Condition condition) { + + return new ExpireArgs() { + @Override + public void build(CommandArgs args) { + + if (ObjectUtils.nullSafeEquals(condition, FieldExpirationOptions.Condition.ALWAYS)) { + return; + } + + args.add(condition.name()); + } + }; + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java index 3cc7bfd9c..84dd2ca90 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java @@ -269,7 +269,8 @@ class LettuceReactiveHashCommands implements ReactiveHashCommands { } @Override - public Flux> expireHashField(Publisher commands) { + public Flux> applyExpiration(Publisher commands) { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKey(), "Key must not be null"); @@ -317,6 +318,7 @@ class LettuceReactiveHashCommands implements ReactiveHashCommands { @Override public Flux> hPersist(Publisher commands) { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKey(), "Key must not be null"); @@ -329,6 +331,7 @@ class LettuceReactiveHashCommands implements ReactiveHashCommands { @Override public Flux> hTtl(Publisher commands) { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKey(), "Key must not be null"); @@ -341,6 +344,7 @@ class LettuceReactiveHashCommands implements ReactiveHashCommands { @Override public Flux> hpTtl(Publisher commands) { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKey(), "Key must not be null"); diff --git a/src/main/java/org/springframework/data/redis/core/BoundHashFieldExpirationOperations.java b/src/main/java/org/springframework/data/redis/core/BoundHashFieldExpirationOperations.java new file mode 100644 index 000000000..33e0ff82e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/BoundHashFieldExpirationOperations.java @@ -0,0 +1,126 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.redis.connection.Hash; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.lang.Nullable; + +/** + * Hash Field Expiration operations bound to a certain hash key and set of hash fields. + * + * @param type of the hash field names. + * @author Mark Paluch + * @since 3.5 + */ +public interface BoundHashFieldExpirationOperations { + + /** + * Apply {@link Expiration} to the hash without any additional constraints. + * + * @param expiration the expiration definition. + * @return changes to the hash fields. + */ + default ExpireChanges expire(Expiration expiration) { + return expire(expiration, Hash.FieldExpirationOptions.none()); + } + + /** + * Apply {@link Expiration} to the hash fields given {@link Hash.FieldExpirationOptions expiration options}. + * + * @param expiration the expiration definition. + * @param options expiration options. + * @return changes to the hash fields. + */ + ExpireChanges expire(Expiration expiration, Hash.FieldExpirationOptions options); + + /** + * Set time to live for given {@code hashKey}. + * + * @param timeout the amount of time after which the key will be expired, must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @throws IllegalArgumentException if the timeout is {@literal null}. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + ExpireChanges expire(Duration timeout); + + /** + * Set the expiration for given {@code hashKey} as a {@literal date} timestamp. + * + * @param expireAt must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + ExpireChanges expireAt(Instant expireAt); + + /** + * Remove the expiration from given {@code hashKey} . + * + * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is + * removed; {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HPERSIST + * @since 3.5 + */ + @Nullable + ExpireChanges persist(); + + /** + * Get the time to live for {@code hashKey} in seconds. + * + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the key exists but has no associated expiration + * time. The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + Expirations getTimeToLive(); + + /** + * Get the time to live for {@code hashKey} and convert it to the given {@link TimeUnit}. + * + * @param timeUnit must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the key exists but has no associated expiration + * time. The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + Expirations getTimeToLive(TimeUnit timeUnit); + +} diff --git a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java index 0503c3309..0d287e929 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java @@ -15,16 +15,12 @@ */ package org.springframework.data.redis.core; -import java.time.Duration; -import java.time.Instant; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; -import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; -import org.springframework.data.redis.core.types.Expiration; import org.springframework.lang.Nullable; /** @@ -96,7 +92,7 @@ public interface BoundHashOperations extends BoundKeyOperations { Double increment(HK key, double delta); /** - * Return a random key (aka field) from the hash stored at the bound key. + * Return a random key from the hash stored at the bound key. * * @return {@literal null} if the hash does not exist or when used in pipeline / transaction. * @since 2.6 @@ -116,10 +112,10 @@ public interface BoundHashOperations extends BoundKeyOperations { Map.Entry randomEntry(); /** - * Return a random keys (aka fields) from the hash stored at the bound key. If the provided {@code count} argument is - * positive, return a list of distinct keys, capped either at {@code count} or the hash size. If {@code count} is - * negative, the behavior changes and the command is allowed to return the same key multiple times. In this case, the - * number of returned keys is the absolute value of the specified count. + * Return a random keys from the hash stored at the bound key. If the provided {@code count} argument is positive, + * return a list of distinct keys, capped either at {@code count} or the hash size. If {@code count} is negative, the + * behavior changes and the command is allowed to return the same key multiple times. In this case, the number of + * returned keys is the absolute value of the specified count. * * @param count number of keys to return. * @return {@literal null} if key does not exist or when used in pipeline / transaction. @@ -159,84 +155,6 @@ public interface BoundHashOperations extends BoundKeyOperations { @Nullable Long lengthOfValue(HK hashKey); - default ExpireChanges expire(Expiration expiration, Collection hashKeys) { - return expire(expiration, FieldExpirationOptions.none(), hashKeys); - } - - ExpireChanges expire(Expiration expiration, FieldExpirationOptions options, Collection hashKeys); - - /** - * Set time to live for given {@code hashKey} (aka field). - * - * @param timeout the amount of time after which the key will be expired, must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); {@code -2} - * indicating there is no such field; {@literal null} when used in pipeline / transaction. - * @throws IllegalArgumentException if the timeout is {@literal null}. - * @see Redis Documentation: HEXPIRE - * @since 3.5 - */ - @Nullable - ExpireChanges expire(Duration timeout, Collection hashKeys); - - /** - * Set the expiration for given {@code hashKey} (aka field) as a {@literal date} timestamp. - * - * @param expireAt must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. - * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. - * @see Redis Documentation: HEXPIRE - * @since 3.5 - */ - @Nullable - ExpireChanges expireAt(Instant expireAt, Collection hashKeys); - - /** - * Remove the expiration from given {@code hashKey} (aka field). - * - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is removed; - * {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such field; {@literal null} when - * used in pipeline / transaction. - * @see Redis Documentation: HPERSIST - * @since 3.5 - */ - @Nullable - ExpireChanges persist(Collection hashKeys); - - /** - * Get the time to live for {@code hashKey} (aka field) in seconds. - * - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. The command - * returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HTTL - * @since 3.5 - */ - @Nullable - Expirations getExpire(Collection hashKeys); - - /** - * Get the time to live for {@code hashKey} (aka field) and convert it to the given {@link TimeUnit}. - * - * @param timeUnit must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. The command - * returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HTTL - * @since 3.5 - */ - @Nullable - Expirations getExpire(TimeUnit timeUnit, Collection hashKeys); - /** * Get size of hash at the bound key. * @@ -297,8 +215,45 @@ public interface BoundHashOperations extends BoundKeyOperations { */ Cursor> scan(ScanOptions options); + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@code key}. Operations on the expiration object obtain keys at the time of invoking any expiration + * operation. + * + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration() { + return new DefaultBoundHashFieldExpirationOperations<>(getOperations().opsForHash(), getKey(), this::keys); + } + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@code key} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration(HK... hashFields) { + return expiration(Arrays.asList(hashFields)); + } + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@code key} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration(Collection hashFields) { + return new DefaultBoundHashFieldExpirationOperations<>(getOperations().opsForHash(), getKey(), () -> hashFields); + } + /** * @return never {@literal null}. */ RedisOperations getOperations(); + } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultBoundHashFieldExpirationOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultBoundHashFieldExpirationOperations.java new file mode 100644 index 000000000..8dbabe6dd --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundHashFieldExpirationOperations.java @@ -0,0 +1,93 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import org.springframework.data.redis.connection.Hash; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link BoundHashFieldExpirationOperations}. + * + * @author Mark Paluch + * @since 3.5 + */ +class DefaultBoundHashFieldExpirationOperations implements BoundHashFieldExpirationOperations { + + private final HashOperations operations; + private final H key; + private final Supplier> hashFields; + + public DefaultBoundHashFieldExpirationOperations(HashOperations operations, H key, + Supplier> hashFields) { + + this.operations = operations; + this.key = key; + this.hashFields = hashFields; + } + + @Override + public ExpireChanges expire(Expiration expiration, Hash.FieldExpirationOptions options) { + return operations.expire(key, expiration, options, getHashKeys()); + } + + @Nullable + @Override + public ExpireChanges expire(Duration timeout) { + return operations.expire(key, timeout, getHashKeys()); + } + + @Nullable + @Override + public ExpireChanges expireAt(Instant expireAt) { + return operations.expireAt(key, expireAt, getHashKeys()); + } + + @Nullable + @Override + public ExpireChanges persist() { + return operations.persist(key, getHashKeys()); + } + + @Nullable + @Override + public Expirations getTimeToLive() { + return operations.getTimeToLive(key, getHashKeys()); + } + + @Nullable + @Override + public Expirations getTimeToLive(TimeUnit timeUnit) { + return operations.getTimeToLive(key, timeUnit, getHashKeys()); + } + + private Collection getHashKeys() { + + Collection hks = hashFields.get(); + + Assert.state(hks != null, "Hash keys must not be null"); + return hks; + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java index 2be7e0bd3..804617616 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java @@ -29,8 +29,9 @@ import java.util.concurrent.TimeUnit; import org.springframework.core.convert.converter.Converter; import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import org.springframework.data.redis.connection.convert.Converters; -import org.springframework.data.redis.core.Expirations.Timeouts; import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.data.redis.core.types.Expirations.Timeouts; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -224,14 +225,11 @@ class DefaultHashOperations extends AbstractOperations imp byte[] rawKey = rawKey(key); byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray()); - boolean splitSecond = TimeoutUtils.hasMillis(duration); + boolean hasMillis = TimeoutUtils.hasMillis(duration); - List raw = execute(connection -> { - if (splitSecond) { - return connection.hashCommands().hpExpire(rawKey, duration.toMillis(), rawHashKeys); - } - return connection.hashCommands().hExpire(rawKey, TimeoutUtils.toSeconds(duration), rawHashKeys); - }); + List raw = execute(connection -> TimeoutUtils.hasMillis(duration) + ? connection.hashCommands().hpExpire(rawKey, duration.toMillis(), rawHashKeys) + : connection.hashCommands().hExpire(rawKey, TimeoutUtils.toSeconds(duration), rawHashKeys)); return raw != null ? ExpireChanges.of(orderedKeys, raw) : null; } @@ -243,8 +241,7 @@ class DefaultHashOperations extends AbstractOperations imp byte[] rawKey = rawKey(key); byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray()); - - Long millis = instant.toEpochMilli(); + long millis = instant.toEpochMilli(); List raw = execute(connection -> TimeoutUtils.containsSplitSecond(millis) ? connection.hashCommands().hpExpireAt(rawKey, millis, rawHashKeys) @@ -257,10 +254,10 @@ class DefaultHashOperations extends AbstractOperations imp public ExpireChanges expire(K key, Expiration expiration, FieldExpirationOptions options, Collection hashKeys) { List orderedKeys = List.copyOf(hashKeys); - byte[] rawKey = rawKey(key); byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray()); - List raw = execute(connection -> connection.hashCommands().expireHashField(rawKey, expiration, options, rawHashKeys)); + List raw = execute( + connection -> connection.hashCommands().applyExpiration(rawKey, expiration, options, rawHashKeys)); return raw != null ? ExpireChanges.of(orderedKeys, raw) : null; } @@ -269,7 +266,6 @@ class DefaultHashOperations extends AbstractOperations imp public ExpireChanges persist(K key, Collection hashKeys) { List orderedKeys = List.copyOf(hashKeys); - byte[] rawKey = rawKey(key); byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray()); @@ -279,7 +275,7 @@ class DefaultHashOperations extends AbstractOperations imp } @Override - public Expirations getExpire(K key, TimeUnit timeUnit, Collection hashKeys) { + public Expirations getTimeToLive(K key, TimeUnit timeUnit, Collection hashKeys) { if(timeUnit.compareTo(TimeUnit.MILLISECONDS) < 0) { throw new IllegalArgumentException("%s precision is not supported must be >= MILLISECONDS".formatted(timeUnit)); diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java index d373a7f06..8c97c43fc 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java @@ -15,10 +15,6 @@ */ package org.springframework.data.redis.core; -import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; -import org.springframework.data.redis.connection.ReactiveHashCommands.ExpireCommand; -import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; -import org.springframework.data.redis.core.types.Expiration; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -33,10 +29,16 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.reactivestreams.Publisher; + import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import org.springframework.data.redis.connection.ReactiveHashCommands; +import org.springframework.data.redis.connection.ReactiveHashCommands.ExpireCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; import org.springframework.data.redis.connection.convert.Converters; -import org.springframework.data.redis.core.Expirations.Timeouts; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.data.redis.core.types.Expirations.Timeouts; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -254,7 +256,9 @@ class DefaultReactiveHashOperations implements ReactiveHashOperations List rawHashKeys = orderedKeys.stream().map(this::rawHashKey).toList(); Mono> raw =createFlux(connection -> { - return connection.expireHashField(Mono.just(ExpireCommand.expire(rawHashKeys, expiration).from(rawKey).withOptions(options))).map(NumericResponse::getOutput); + return connection + .applyExpiration(Mono.just(ExpireCommand.expire(rawHashKeys, expiration).from(rawKey).withOptions(options))) + .map(NumericResponse::getOutput); }).collectList(); return raw.map(values -> ExpireChanges.of(orderedKeys, values)); @@ -288,7 +292,7 @@ class DefaultReactiveHashOperations implements ReactiveHashOperations @Nullable @Override - public Mono> getExpire(H key, TimeUnit timeUnit, Collection hashKeys) { + public Mono> getTimeToLive(H key, TimeUnit timeUnit, Collection hashKeys) { if (timeUnit.compareTo(TimeUnit.MILLISECONDS) < 0) { throw new IllegalArgumentException("%s precision is not supported must be >= MILLISECONDS".formatted(timeUnit)); diff --git a/src/main/java/org/springframework/data/redis/core/ExpireChanges.java b/src/main/java/org/springframework/data/redis/core/ExpireChanges.java index b9486f639..029922d96 100644 --- a/src/main/java/org/springframework/data/redis/core/ExpireChanges.java +++ b/src/main/java/org/springframework/data/redis/core/ExpireChanges.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.core; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -23,6 +24,7 @@ import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; /** @@ -35,7 +37,7 @@ import org.springframework.util.CollectionUtils; *
  • {@link #skipped()} returns keys for which the time to live has not been set because a precondition was not * met
  • * - * + * * @author Christoph Strobl * @since 3.5 */ @@ -50,23 +52,25 @@ public class ExpireChanges { /** * Factory Method to create {@link ExpireChanges} from raw sources. * - * @param keys the keys to associated with the raw values in states. Defines the actual order of entries within + * @param fields the fields to associated with the raw values in states. Defines the actual order of entries within * {@link ExpireChanges}. * @param states the raw Redis state change values. * @return new instance of {@link ExpireChanges}. * @param the key type used */ - public static ExpireChanges of(List keys, List states) { + public static ExpireChanges of(List fields, List states) { - if (keys.size() == 1) { - return new ExpireChanges<>(Map.of(keys.iterator().next(), stateFromValue(states.iterator().next()))); + Assert.isTrue(fields.size() == states.size(), "Keys and States must have the same number of elements"); + + if (fields.size() == 1) { + return new ExpireChanges<>(Map.of(fields.iterator().next(), stateFromValue(states.iterator().next()))); } - Map target = CollectionUtils.newLinkedHashMap(keys.size()); - for (int i = 0; i < keys.size(); i++) { - target.put(keys.get(i), stateFromValue(states.get(i))); + Map target = CollectionUtils.newLinkedHashMap(fields.size()); + for (int i = 0; i < fields.size(); i++) { + target.put(fields.get(i), stateFromValue(states.get(i))); } - return new ExpireChanges<>(target); + return new ExpireChanges<>(Collections.unmodifiableMap(target)); } /** diff --git a/src/main/java/org/springframework/data/redis/core/HashOperations.java b/src/main/java/org/springframework/data/redis/core/HashOperations.java index c32c33983..67c4ce0fa 100644 --- a/src/main/java/org/springframework/data/redis/core/HashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/HashOperations.java @@ -17,6 +17,7 @@ package org.springframework.data.redis.core; import java.time.Duration; import java.time.Instant; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; @@ -25,6 +26,7 @@ import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; import org.springframework.lang.Nullable; /** @@ -96,7 +98,7 @@ public interface HashOperations { Double increment(H key, HK hashKey, double delta); /** - * Return a random hash key (aka field) from the hash stored at {@code key}. + * Return a random hash key from the hash stored at {@code key}. * * @param key must not be {@literal null}. * @return {@literal null} if key does not exist or when used in pipeline / transaction. @@ -118,10 +120,10 @@ public interface HashOperations { Map.Entry randomEntry(H key); /** - * Return random hash keys (aka fields) from the hash stored at {@code key}. If the provided {@code count} argument is - * positive, return a list of distinct hash keys, capped either at {@code count} or the hash size. If {@code count} is - * negative, the behavior changes and the command is allowed to return the same hash key multiple times. In this case, - * the number of returned fields is the absolute value of the specified count. + * Return random hash keys from the hash stored at {@code key}. If the provided {@code count} argument is positive, + * return a list of distinct hash keys, capped either at {@code count} or the hash size. If {@code count} is negative, + * the behavior changes and the command is allowed to return the same hash key multiple times. In this case, the + * number of returned fields is the absolute value of the specified count. * * @param key must not be {@literal null}. * @param count number of fields to return. @@ -228,15 +230,16 @@ public interface HashOperations { Cursor> scan(H key, ScanOptions options); /** - * Set time to live for given {@code hashKey} (aka field). + * Set time to live for given {@code hashKey} . * * @param key must not be {@literal null}. * @param timeout the amount of time after which the key will be expired, must not be {@literal null}. * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); {@code -2} - * indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. * @throws IllegalArgumentException if the timeout is {@literal null}. * @see Redis Documentation: HEXPIRE * @since 3.5 @@ -245,15 +248,16 @@ public interface HashOperations { ExpireChanges expire(H key, Duration timeout, Collection hashKeys); /** - * Set the expiration for given {@code hashKey} (aka field) as a {@literal date} timestamp. + * Set the expiration for given {@code hashKeys} as a {@literal date} timestamp. * * @param key must not be {@literal null}. * @param expireAt must not be {@literal null}. * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. * @see Redis Documentation: HEXPIRE * @since 3.5 @@ -261,16 +265,33 @@ public interface HashOperations { @Nullable ExpireChanges expireAt(H key, Instant expireAt, Collection hashKeys); + /** + * Apply the expiration for given {@code hashKeys} as a {@literal date} timestamp. + * + * @param key must not be {@literal null}. + * @param expiration must not be {@literal null}. + * @param options must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable ExpireChanges expire(H key, Expiration expiration, FieldExpirationOptions options, Collection hashKeys); /** - * Remove the expiration from given {@code hashKey} (aka field). + * Remove the expiration from given {@code hashKeys} . * * @param key must not be {@literal null}. * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is removed; - * {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such field; {@literal null} when - * used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is + * removed; {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction. * @see Redis Documentation: HPERSIST * @since 3.5 */ @@ -278,38 +299,77 @@ public interface HashOperations { ExpireChanges persist(H key, Collection hashKeys); /** - * Get the time to live for {@code hashKey} (aka field) in seconds. + * Get the time to live for {@code hashKeys} in seconds. * * @param key must not be {@literal null}. * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. The command - * returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the key exists but has no associated expiration + * time. The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @Nullable - default Expirations getExpire(H key, Collection hashKeys) { - return getExpire(key, TimeUnit.SECONDS, hashKeys); + default Expirations getTimeToLive(H key, Collection hashKeys) { + return getTimeToLive(key, TimeUnit.SECONDS, hashKeys); } /** - * Get the time to live for {@code hashKey} (aka field) and convert it to the given {@link TimeUnit}. + * Get the time to live for {@code hashKeys} and convert it to the given {@link TimeUnit}. * * @param key must not be {@literal null}. * @param timeUnit must not be {@literal null}. * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. The command - * returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the key exists but has no associated expiration + * time. The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @Nullable - Expirations getExpire(H key, TimeUnit timeUnit, Collection hashKeys); + Expirations getTimeToLive(H key, TimeUnit timeUnit, Collection hashKeys); + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at + * {@code key}. Operations on the expiration object obtain keys at the time of invoking any expiration operation. + * + * @param key must not be {@literal null}. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration(H key) { + return new DefaultBoundHashFieldExpirationOperations<>(this, key, () -> keys(key)); + } + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at + * {@code key} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration(H key, HK... hashFields) { + return expiration(key, Arrays.asList(hashFields)); + } + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at + * {@code key} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration(H key, Collection hashFields) { + return new DefaultBoundHashFieldExpirationOperations<>(this, key, () -> hashFields); + } /** * @return never {@literal null}. */ RedisOperations getOperations(); + } diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java index 2d0cbcaf1..757dbcea7 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java @@ -15,8 +15,6 @@ */ package org.springframework.data.redis.core; -import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; -import org.springframework.data.redis.core.types.Expiration; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -28,6 +26,9 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; import org.springframework.lang.Nullable; /** @@ -102,7 +103,7 @@ public interface ReactiveHashOperations { Mono increment(H key, HK hashKey, double delta); /** - * Return a random hash key (aka field) from the hash stored at {@code key}. + * Return a random hash key from the hash stored at {@code key}. * * @param key must not be {@literal null}. * @return @@ -122,10 +123,10 @@ public interface ReactiveHashOperations { Mono> randomEntry(H key); /** - * Return random hash keys (aka fields) from the hash stored at {@code key}. If the provided {@code count} argument is - * positive, return a list of distinct hash keys, capped either at {@code count} or the hash size. If {@code count} is - * negative, the behavior changes and the command is allowed to return the same hash key multiple times. In this case, - * the number of returned fields is the absolute value of the specified count. + * Return random hash keys from the hash stored at {@code key}. If the provided {@code count} argument is positive, + * return a list of distinct hash keys, capped either at {@code count} or the hash size. If {@code count} is negative, + * the behavior changes and the command is allowed to return the same hash key multiple times. In this case, the + * number of returned fields is the absolute value of the specified count. * * @param key must not be {@literal null}. * @param count number of fields to return. @@ -242,7 +243,7 @@ public interface ReactiveHashOperations { Mono> expire(H key, Expiration expiration, FieldExpirationOptions options, Collection hashKeys); /** - * Set the expiration for given {@code hashKey} (aka field) as a {@literal date} timestamp. + * Set the expiration for given {@code hashKey} as a {@literal date} timestamp. * * @param key must not be {@literal null}. * @param expireAt must not be {@literal null}. @@ -260,7 +261,7 @@ public interface ReactiveHashOperations { Mono> expireAt(H key, Instant expireAt, Collection hashKeys); /** - * Remove the expiration from given {@code hashKey} (aka field). + * Remove the expiration from given {@code hashKey} . * * @param key must not be {@literal null}. * @param hashKeys must not be {@literal null}. @@ -274,7 +275,7 @@ public interface ReactiveHashOperations { Mono> persist(H key, Collection hashKeys); /** - * Get the time to live for {@code hashKey} (aka field) in seconds. + * Get the time to live for {@code hashKey} in seconds. * * @param key must not be {@literal null}. * @param hashKeys must not be {@literal null}. @@ -286,12 +287,12 @@ public interface ReactiveHashOperations { * @since 3.5 */ @Nullable - default Mono> getExpire(H key, Collection hashKeys) { - return getExpire(key, TimeUnit.SECONDS, hashKeys); + default Mono> getTimeToLive(H key, Collection hashKeys) { + return getTimeToLive(key, TimeUnit.SECONDS, hashKeys); } /** - * Get the time to live for {@code hashKey} (aka field) and convert it to the given {@link TimeUnit}. + * Get the time to live for {@code hashKey} and convert it to the given {@link TimeUnit}. * * @param key must not be {@literal null}. * @param timeUnit must not be {@literal null}. @@ -304,7 +305,7 @@ public interface ReactiveHashOperations { * @since 3.5 */ @Nullable - Mono> getExpire(H key, TimeUnit timeUnit, Collection hashKeys); + Mono> getTimeToLive(H key, TimeUnit timeUnit, Collection hashKeys); /** * Removes the given {@literal key}. diff --git a/src/main/java/org/springframework/data/redis/core/RedisCommand.java b/src/main/java/org/springframework/data/redis/core/RedisCommand.java index e9303233d..53dc940a9 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisCommand.java +++ b/src/main/java/org/springframework/data/redis/core/RedisCommand.java @@ -107,6 +107,13 @@ public enum RedisCommand { HSET("w", 3, 3), // HSETNX("w", 3, 3), // HVALS("r", 1, 1), // + HEXPIRE("w", 5), // + HEXPIREAT("w", 5), // + HPEXPIRE("w", 5), // + HPEXPIREAT("w", 5), // + HPERSIST("w", 4), // + HTTL("r", 4), // + HPTTL("r", 4), // // -- I INCR("rw", 1), // INCRBYFLOAT("rw", 2, 2), // diff --git a/src/main/java/org/springframework/data/redis/core/RedisOperations.java b/src/main/java/org/springframework/data/redis/core/RedisOperations.java index 8c1ad67ad..def0dca04 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/RedisOperations.java @@ -376,6 +376,7 @@ public interface RedisOperations { @Nullable Boolean persist(K key); + // TODO: Add TimeToLive (getTimeToLive) /** * Get the time to live for {@code key} in seconds. * diff --git a/src/main/java/org/springframework/data/redis/core/Expirations.java b/src/main/java/org/springframework/data/redis/core/types/Expirations.java similarity index 62% rename from src/main/java/org/springframework/data/redis/core/Expirations.java rename to src/main/java/org/springframework/data/redis/core/types/Expirations.java index 958f90e3a..7101cb60f 100644 --- a/src/main/java/org/springframework/data/redis/core/Expirations.java +++ b/src/main/java/org/springframework/data/redis/core/types/Expirations.java @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.redis.core; +package org.springframework.data.redis.core.types; import java.time.Duration; +import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -29,31 +30,32 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; /** - * Value Object linking a number of keys to their {@link Expiration} retaining the order of the original source. + * Value Object linking a number of keys to their {@link TimeToLive} retaining the order of the original source. * Dedicated higher level methods interpret raw expiration values retrieved from a Redis Client. *
      *
    1. {@link #persistent()} returns keys that do not have an associated time to live
    2. *
    3. {@link #missing()} returns keys that do not exist and therefore have no associated time to live
    4. - *
    5. {@link #expirations()} returns the ordered list of {@link Expiration expirations} based on the raw values
    6. + *
    7. {@link #ttl()} returns the ordered list of {@link TimeToLive expirations} based on the raw values
    8. *
    9. {@link #expiring()} returns the expiring keys along with their {@link Duration time to live}
    10. *
    - * + * * @author Christoph Strobl + * @author Mark Paluch * @since 3.5 */ -public class Expirations { // TODO: should we move this to let's say Hash.class or another place +public class Expirations { private final TimeUnit unit; - private final Map expirations; + private final Map expirations; - Expirations(TimeUnit unit, Map expirations) { + Expirations(TimeUnit unit, Map expirations) { this.unit = unit; this.expirations = expirations; } /** * Factory Method to create {@link Expirations} from raw sources provided in a given {@link TimeUnit}. - * + * * @param targetUnit the actual time unit of the raw timeToLive values. * @param keys the keys to associated with the raw values in timeToLive. Defines the actual order of entries within * {@link Expirations}. @@ -69,12 +71,12 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla } if (keys.size() == 1) { return new Expirations<>(targetUnit, - Map.of(keys.iterator().next(), Expiration.of(timeouts.raw().iterator().next(), timeouts.timeUnit()))); + Map.of(keys.iterator().next(), TimeToLive.of(timeouts.raw().iterator().next(), timeouts.timeUnit()))); } - Map target = CollectionUtils.newLinkedHashMap(keys.size()); + Map target = CollectionUtils.newLinkedHashMap(keys.size()); for (int i = 0; i < keys.size(); i++) { - target.put(keys.get(i), Expiration.of(timeouts.get(i), timeouts.timeUnit())); + target.put(keys.get(i), TimeToLive.of(timeouts.get(i), timeouts.timeUnit())); } return new Expirations<>(targetUnit, target); } @@ -83,87 +85,98 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla * @return an ordered set of keys that do not have a time to live. */ public Set persistent() { - return filterByState(Expiration.PERSISTENT); + return filterByState(TimeToLive.PERSISTENT); } /** - * @return an ordered set of keys that do not exists and therefore do not have a time to live. + * @return an ordered set of keys that do not exist and therefore do not have a time to live. */ public Set missing() { - return filterByState(Expiration.MISSING); + return filterByState(TimeToLive.MISSING); } /** - * @return an ordered set of all {@link Expirations expirations} where the {@link Expiration#value()} is using the - * {@link TimeUnit} defined in {@link #precision()}. + * @return an ordered set of all {@link Expirations expirations} where the {@link TimeToLive#value()} is using the + * {@link TimeUnit} defined in {@link #timeUnit()}. */ - public List expirations() { + public List ttl() { return expirations.values().stream().map(it -> it.convert(this.unit)).toList(); } /** - * @return the {@link TimeUnit} for {@link Expiration expirations} held by this instance. + * @return the {@link TimeUnit} for {@link TimeToLive expirations} held by this instance. */ - public TimeUnit precision() { + public TimeUnit timeUnit() { return unit; } /** * @return an ordered {@link List} of {@link java.util.Map.Entry entries} combining keys with their actual time to - * live. {@link Expiration#isMissing() Missing} and {@link Expiration#isPersistent() persistent} entries are + * live. {@link TimeToLive#isMissing() Missing} and {@link TimeToLive#isPersistent() persistent} entries are * skipped. */ public List> expiring() { + return expirations.entrySet().stream().filter(it -> !it.getValue().isMissing() && !it.getValue().isPersistent()) .map(it -> Map.entry(it.getKey(), toDuration(it.getValue()))).toList(); } + /** + * @return the ordered collection of keys that are associated with an expiration. + */ + public Collection keys() { + return expirations.keySet(); + } + /** * @param key - * @return the {@link Expirations expirations} where the {@link Expiration#value()} is using the {@link TimeUnit} - * defined in {@link #precision()} or {@literal null} if no entry could be found. + * @return the {@link Expirations expirations} where the {@link TimeToLive#value()} is using the {@link TimeUnit} + * defined in {@link #timeUnit()} or {@literal null} if no entry could be found. */ @Nullable - public Expiration expirationOf(K key) { + public TimeToLive expirationOf(K key) { - Expiration expiration = expirations.get(key); - if (expiration == null) { + TimeToLive timeToLive = expirations.get(key); + + if (timeToLive == null) { return null; } - return expiration.convert(this.unit); + return timeToLive.convert(this.unit); } /** * @param key * @return the time to live value of the requested key if it exists and the expiration is neither - * {@link Expiration#isMissing() missing} nor {@link Expiration#isPersistent() persistent}, {@literal null} + * {@link TimeToLive#isMissing() missing} nor {@link TimeToLive#isPersistent() persistent}, {@literal null} * otherwise. */ @Nullable public Duration ttlOf(K key) { - - Expiration expiration = expirationOf(key); - if (expiration == null) { - return null; - } - return toDuration(expiration); + return toDuration(expirationOf(key)); } - private Set filterByState(Expiration filter) { + private Set filterByState(TimeToLive filter) { return expirations.entrySet().stream().filter(entry -> entry.getValue().equals(filter)).map(Map.Entry::getKey) .collect(Collectors.toCollection(LinkedHashSet::new)); } @Nullable - static Duration toDuration(Expiration expiration) { + static Duration toDuration(@Nullable TimeToLive timeToLive) { - if (expiration.sourceUnit == null) { + if (timeToLive == null || timeToLive.sourceUnit == null) { return null; } - return Duration.of(expiration.raw(), expiration.sourceUnit.toChronoUnit()); + + return Duration.of(timeToLive.raw(), timeToLive.sourceUnit.toChronoUnit()); } + /** + * Collection of timeouts associated with a {@link TimeUnit}. + * + * @param timeUnit + * @param raw + */ public record Timeouts(TimeUnit timeUnit, List raw) { Long get(int index) { @@ -173,6 +186,7 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla public int size() { return raw.size(); } + } /** @@ -182,30 +196,57 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla * {@link #PERSISTENT} mark predefined states returned by Redis indicating a time to live value could not be retrieved * due to various reasons. */ - public static class Expiration { // TODO: is Expiry a better name for this type? + public static class TimeToLive { + /** + * Predefined {@link TimeToLive} for a key that does not exist and therefore does not have a time to live. + */ + public static TimeToLive MISSING = new TimeToLive(-2L); + + /** + * Predefined {@link TimeToLive} for a key that exists but does not expire. + */ + public static TimeToLive PERSISTENT = new TimeToLive(-1L); + + private final @Nullable TimeUnit sourceUnit; + private final @Nullable TimeUnit targetUnit; private final long raw; - @Nullable TimeUnit sourceUnit; - @Nullable TimeUnit targetUnit; - public Expiration(long value) { + TimeToLive(long value) { this(value, null); } - public Expiration(long value, @Nullable TimeUnit sourceUnit) { + TimeToLive(long value, @Nullable TimeUnit sourceUnit) { this(value, sourceUnit, null); } - public Expiration(long value, @Nullable TimeUnit sourceUnit, @Nullable TimeUnit targetUnit) { + TimeToLive(long value, @Nullable TimeUnit sourceUnit, @Nullable TimeUnit targetUnit) { this.raw = value; this.sourceUnit = sourceUnit; this.targetUnit = targetUnit; } + /** + * Factory method for creating {@link TimeToLive} instances, returning predefined ones if the value matches a known + * reserved state. + * + * @param value the TTL value. + * @param timeUnit time unit for the given value. + * @return the {@link TimeToLive} for the given raw value. + */ + public static TimeToLive of(Number value, TimeUnit timeUnit) { + + return switch (value.intValue()) { + case -2 -> MISSING; + case -1 -> PERSISTENT; + default -> new TimeToLive(value.longValue(), timeUnit); + }; + } + /** * The raw source value as returned by the Redis Client. * - * @return the raw data + * @return the raw data. */ public long raw() { return raw; @@ -219,58 +260,36 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla if (sourceUnit == null || targetUnit == null) { return raw; } + return targetUnit.convert(raw, sourceUnit); } /** * @param timeUnit must not be {@literal null}. - * @return the {@link Expiration} instance with new target {@link TimeUnit} set for obtaining the {@link #value() + * @return the {@link TimeToLive} instance with new target {@link TimeUnit} set for obtaining the {@link #value() * value}, or the same instance raw value cannot or must not be converted. */ - public Expiration convert(TimeUnit timeUnit) { + public TimeToLive convert(TimeUnit timeUnit) { if (sourceUnit == null || ObjectUtils.nullSafeEquals(sourceUnit, timeUnit)) { return this; } - return new Expiration(raw, sourceUnit, timeUnit); + + return new TimeToLive(raw, sourceUnit, timeUnit); } - /** - * Predefined {@link Expiration} for a key that does not exists and therefore does not have a time to live. - */ - public static Expiration MISSING = new Expiration(-2L); - - /** - * Predefined {@link Expiration} for a key that exists but does not expire. - */ - public static Expiration PERSISTENT = new Expiration(-1L); - /** * @return {@literal true} if key exists but does not expire. */ public boolean isPersistent() { - return PERSISTENT.equals(this); + return PERSISTENT.raw() == raw(); } /** - * @return {@literal true} if key does not exists and therefore does not have a time to live. + * @return {@literal true} if key does not exist and therefore does not have a time to live. */ public boolean isMissing() { - return MISSING.equals(this); - } - - /** - * Factory method for creating {@link Expiration} instances, returning predefined ones if the value matches a known - * reserved state. - * - * @return the {@link Expiration} for the given raw value. - */ - static Expiration of(Number value, TimeUnit timeUnit) { - return switch (value.intValue()) { - case -2 -> MISSING; - case -1 -> PERSISTENT; - default -> new Expiration(value.longValue(), timeUnit); - }; + return MISSING.raw() == raw(); } @Override @@ -280,7 +299,7 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla return true; } - if (!(o instanceof Expiration that)) { + if (!(o instanceof Expirations.TimeToLive that)) { return false; } @@ -299,5 +318,16 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla public int hashCode() { return Objects.hash(raw); } + + @Override + public String toString() { + + return switch ((int) raw()) { + case -2 -> "MISSING"; + case -1 -> "PERSISTENT"; + default -> "%d %s".formatted(raw(), sourceUnit); + }; + } } + } diff --git a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisMap.java b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisMap.java index ad22195ad..18c3c24e2 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisMap.java +++ b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisMap.java @@ -15,19 +15,15 @@ */ package org.springframework.data.redis.support.collections; -import java.time.Duration; -import java.time.Instant; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.DataType; -import org.springframework.data.redis.core.Expirations; -import org.springframework.data.redis.core.ExpireChanges; +import org.springframework.data.redis.core.BoundHashFieldExpirationOperations; import org.springframework.data.redis.core.BoundHashOperations; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisOperations; @@ -328,28 +324,13 @@ public class DefaultRedisMap implements RedisMap { } @Override - public ExpireChanges expire(Duration timeout, Collection hashKeys) { - return Objects.requireNonNull(hashOps.expire(timeout, hashKeys)); + public BoundHashFieldExpirationOperations expiration() { + return hashOps.expiration(); } @Override - public ExpireChanges expireAt(Instant expireAt, Collection hashKeys) { - return Objects.requireNonNull(hashOps.expireAt(expireAt, hashKeys)); - } - - @Override - public ExpireChanges persist(Collection hashKeys) { - return Objects.requireNonNull(hashOps.persist(hashKeys)); - } - - @Override - public Expirations getExpire(Collection hashKeys) { - return Objects.requireNonNull(hashOps.getExpire(hashKeys)); - } - - @Override - public Expirations getExpire(TimeUnit timeUnit, Collection hashKeys) { - return Objects.requireNonNull(hashOps.getExpire(timeUnit, hashKeys)); + public BoundHashFieldExpirationOperations expiration(Collection hashFields) { + return hashOps.expiration(hashFields); } private void checkResult(@Nullable Object obj) { diff --git a/src/main/java/org/springframework/data/redis/support/collections/RedisMap.java b/src/main/java/org/springframework/data/redis/support/collections/RedisMap.java index 54d002d54..955942eb2 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisMap.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisMap.java @@ -15,16 +15,13 @@ */ package org.springframework.data.redis.support.collections; -import java.time.Duration; -import java.time.Instant; +import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.TimeUnit; -import org.springframework.data.redis.core.Expirations; -import org.springframework.data.redis.core.ExpireChanges; +import org.springframework.data.redis.core.BoundHashFieldExpirationOperations; import org.springframework.lang.Nullable; /** @@ -33,6 +30,7 @@ import org.springframework.lang.Nullable; * @author Costin Leau * @author Christoph Strobl * @author Tihomi Mateev + * @author Mark Paluch */ public interface RedisMap extends RedisStore, ConcurrentMap { @@ -71,7 +69,7 @@ public interface RedisMap extends RedisStore, ConcurrentMap { * @since 2.6 */ @Nullable - Map.Entry randomEntry(); + Map.Entry randomEntry(); /** * @since 1.4 @@ -80,69 +78,34 @@ public interface RedisMap extends RedisStore, ConcurrentMap { Iterator> scan(); /** - * Set time to live for given {hash {@code key}. + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at + * {@code key}. Operations on the expiration object obtain keys at the time of invoking any expiration operation. * - * @param timeout the amount of time after which the key will be expired, must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); {@code -2} - * indicating there is no such field; {@literal null} when used in pipeline / transaction. - * @throws IllegalArgumentException if the timeout is {@literal null}. - * @see Redis Documentation: HEXPIRE + * @return the bound operations object to perform operations on the hash field expiration. * @since 3.5 */ - ExpireChanges expire(Duration timeout, Collection hashKeys); + BoundHashFieldExpirationOperations expiration(); /** - * Set the expiration for given hash {@code key} as a {@literal date} timestamp. + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@code key} for the given hash fields. * - * @param expireAt must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. - * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. - * @see Redis Documentation: HEXPIRE + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. * @since 3.5 */ - ExpireChanges expireAt(Instant expireAt, Collection hashKeys); + default BoundHashFieldExpirationOperations expiration(K... hashFields) { + return expiration(Arrays.asList(hashFields)); + } /** - * Remove the expiration from given hash {@code key}. + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@code key} for the given hash fields. * - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is removed; - * {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such field; {@literal null} when - * used in pipeline / transaction. - * @see Redis Documentation: HPERSIST + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. * @since 3.5 */ - ExpireChanges persist(Collection hashKeys); + BoundHashFieldExpirationOperations expiration(Collection hashFields); - /** - * Get the time to live for hash {@code key} in seconds. - * - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. The command - * returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HTTL - * @since 3.5 - */ - Expirations getExpire(Collection hashKeys); - - /** - * Get the time to live for hash {@code key} and convert it to the given {@link TimeUnit}. - * - * @param timeUnit must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. The command - * returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HTTL - * @since 3.5 - */ - Expirations getExpire(TimeUnit timeUnit, Collection hashKeys); } diff --git a/src/main/java/org/springframework/data/redis/support/collections/RedisProperties.java b/src/main/java/org/springframework/data/redis/support/collections/RedisProperties.java index 54d7f0c9d..c6a5f0a45 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisProperties.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisProperties.java @@ -17,15 +17,20 @@ package org.springframework.data.redis.support.collections; import java.io.IOException; import java.io.OutputStream; -import java.time.Duration; -import java.time.Instant; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.DataType; -import org.springframework.data.redis.core.Expirations; -import org.springframework.data.redis.core.ExpireChanges; +import org.springframework.data.redis.core.BoundHashFieldExpirationOperations; import org.springframework.data.redis.core.BoundHashOperations; import org.springframework.data.redis.core.RedisOperations; import org.springframework.lang.Nullable; @@ -298,42 +303,17 @@ public class RedisProperties extends Properties implements RedisMap> scan() { - throw new UnsupportedOperationException(); + return (Iterator) delegate.scan(); } @Override - public ExpireChanges expire(Duration timeout, Collection hashKeys) { - - Collection keys = hashKeys.stream().map(key -> (String) key).toList(); - return (ExpireChanges) hashOps.expire(timeout, keys); + public BoundHashFieldExpirationOperations expiration() { + return (BoundHashFieldExpirationOperations) delegate.expiration(); } @Override - public ExpireChanges expireAt(Instant expireAt, Collection hashKeys) { - - Collection keys = hashKeys.stream().map(key -> (String) key).toList(); - return (ExpireChanges) hashOps.expireAt(expireAt, keys); - } - - @Override - public ExpireChanges persist(Collection hashKeys) { - - Collection keys = hashKeys.stream().map(key -> (String) key).toList(); - return (ExpireChanges) hashOps.persist(keys); - } - - @Override - public Expirations getExpire(Collection hashKeys) { - - Collection keys = hashKeys.stream().map(key -> (String) key).toList(); - return (Expirations) hashOps.getExpire(keys); - } - - @Override - public Expirations getExpire(TimeUnit timeUnit, Collection hashKeys) { - - Collection keys = hashKeys.stream().map(key -> (String) key).toList(); - return (Expirations) hashOps.getExpire(timeUnit, keys); + public BoundHashFieldExpirationOperations expiration(Collection hashFields) { + return (BoundHashFieldExpirationOperations) delegate.expiration((Collection) hashFields); } } diff --git a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java index ffa5bcd10..94b8db765 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -98,6 +98,7 @@ import org.springframework.data.redis.test.condition.LongRunningTest; import org.springframework.data.redis.test.condition.RedisDriver; import org.springframework.data.redis.test.util.HexStringUtils; import org.springframework.data.util.Streamable; +import org.springframework.util.ObjectUtils; /** * Base test class for AbstractConnection integration tests @@ -752,6 +753,45 @@ public abstract class AbstractConnectionIntegrationTests { assertThat(stringSerializer.deserialize((byte[]) getResults().get(1))).isEqualTo("bar"); } + @Test + @EnabledOnCommand("HEXPIRE") + void testExecuteHashFieldExpiration() { + + actual.add(connection.hSet("foo", "bar", "field")); + actual.add(connection.execute("HTTL", "foo", "FIELDS", "1", "bar")); + actual.add(connection.execute("HEXPIRE", "foo", "100", "NX", "FIELDS", "1", "bar")); + actual.add(connection.execute("HPERSIST", "foo", "FIELDS", "1", "bar")); + actual.add(connection.execute("HTTL", "foo", "FIELDS", "1", "bar")); + + List results = getResults(); + + assertThat(deserializeList(results, 1, stringSerializer)).containsOnly(-1L); + assertThat(deserializeList(results, 2, stringSerializer)).containsOnly(1L); + assertThat(deserializeList(results, 3, stringSerializer)).containsOnly(1L); + assertThat(deserializeList(results, 4, stringSerializer)).containsOnly(-1L); + } + + List deserializeList(List objects, int index, RedisSerializer serializer) { + + List result = new ArrayList<>(); + Object o = objects.get(index); + if (o instanceof List ls) { + for (Object obj : ls) { + + if (obj instanceof byte[]) { + result.add(serializer.deserialize((byte[]) obj)); + } else { + result.add(obj); + } + } + + return result; + } + + throw new IllegalArgumentException( + "Object at index " + index + " is not a list but " + ObjectUtils.nullSafeToString(o)); + } + @Test void testExecuteNoArgs() { @@ -3436,7 +3476,7 @@ public abstract class AbstractConnectionIntegrationTests { @Test @EnabledOnCommand("HEXPIRE") public void hExpireReturnsSuccessAndSetsTTL() { - + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); actual.add(connection.hExpire("hash-hexpire", 5L, "key-2")); actual.add(connection.hTtl("hash-hexpire", "key-2")); diff --git a/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java index 4abd23dac..bbb84a76d 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java @@ -15,9 +15,8 @@ */ package org.springframework.data.redis.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assumptions.assumeThat; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; import java.io.IOException; import java.time.Duration; @@ -30,15 +29,15 @@ import java.util.concurrent.TimeUnit; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; + import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.RawObjectFactory; import org.springframework.data.redis.StringObjectFactory; import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.connection.jedis.extension.JedisConnectionFactoryExtension; -import org.springframework.data.redis.core.Expirations.Expiration; import org.springframework.data.redis.core.ExpireChanges.ExpiryChangeState; +import org.springframework.data.redis.core.types.Expirations.TimeToLive; import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.RedisStanalone; import org.springframework.data.redis.test.extension.parametrized.MethodSource; @@ -228,11 +227,11 @@ public class DefaultHashOperationsIntegrationTests { assertThat(redisTemplate.opsForHash().expire(key, Duration.ofMillis(500), List.of(key1))) .satisfies(ExpireChanges::allOk); - assertThat(redisTemplate.opsForHash().getExpire(key, List.of(key1))).satisfies(expirations -> { + assertThat(redisTemplate.opsForHash().getTimeToLive(key, List.of(key1))).satisfies(expirations -> { assertThat(expirations.missing()).isEmpty(); - assertThat(expirations.precision()).isEqualTo(TimeUnit.SECONDS); - assertThat(expirations.expirationOf(key1)).extracting(Expiration::raw, InstanceOfAssertFactories.LONG) + assertThat(expirations.timeUnit()).isEqualTo(TimeUnit.SECONDS); + assertThat(expirations.expirationOf(key1)).extracting(TimeToLive::raw, InstanceOfAssertFactories.LONG) .isBetween(0L, 1L); assertThat(expirations.ttlOf(key1)).isBetween(Duration.ZERO, Duration.ofSeconds(1)); }); @@ -259,16 +258,48 @@ public class DefaultHashOperationsIntegrationTests { assertThat(changes.stateChanges()).map(ExpiryChangeState::value).containsExactly(1L, 1L); }); - assertThat(redisTemplate.opsForHash().getExpire(key, TimeUnit.SECONDS, List.of(key1, key2))) + assertThat(redisTemplate.opsForHash().getTimeToLive(key, TimeUnit.SECONDS, List.of(key1, key2))) .satisfies(expirations -> { assertThat(expirations.missing()).isEmpty(); - assertThat(expirations.precision()).isEqualTo(TimeUnit.SECONDS); - assertThat(expirations.expirationOf(key1)).extracting(Expiration::raw, InstanceOfAssertFactories.LONG) + assertThat(expirations.timeUnit()).isEqualTo(TimeUnit.SECONDS); + assertThat(expirations.expirationOf(key1)).extracting(TimeToLive::raw, InstanceOfAssertFactories.LONG) .isBetween(0L, 5L); assertThat(expirations.ttlOf(key1)).isBetween(Duration.ofSeconds(1), Duration.ofSeconds(5)); }); } + @ParameterizedRedisTest + @EnabledOnCommand("HEXPIRE") + void testBoundExpireAndGetExpireSeconds() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + hashOps.put(key, key1, val1); + hashOps.put(key, key2, val2); + + BoundHashOperations hashOps = redisTemplate.boundHashOps(key); + BoundHashFieldExpirationOperations exp = hashOps.expiration(key1, key2); + + assertThat(exp.expire(Duration.ofSeconds(5))).satisfies(changes -> { + assertThat(changes.allOk()).isTrue(); + assertThat(changes.stateOf(key1)).isEqualTo(ExpiryChangeState.OK); + assertThat(changes.ok()).containsExactlyInAnyOrder(key1, key2); + assertThat(changes.missed()).isEmpty(); + assertThat(changes.stateChanges()).map(ExpiryChangeState::value).containsExactly(1L, 1L); + }); + + assertThat(exp.getTimeToLive(TimeUnit.SECONDS)).satisfies(expirations -> { + assertThat(expirations.missing()).isEmpty(); + assertThat(expirations.timeUnit()).isEqualTo(TimeUnit.SECONDS); + assertThat(expirations.expirationOf(key1)).extracting(TimeToLive::raw, InstanceOfAssertFactories.LONG) + .isBetween(0L, 5L); + assertThat(expirations.ttlOf(key1)).isBetween(Duration.ofSeconds(1), Duration.ofSeconds(5)); + }); + } + @ParameterizedRedisTest @EnabledOnCommand("HEXPIRE") void testExpireAtAndGetExpireMillis() { @@ -284,27 +315,29 @@ public class DefaultHashOperationsIntegrationTests { assertThat(redisTemplate.opsForHash().expireAt(key, Instant.now().plusMillis(500), List.of(key1, key2))) .satisfies(ExpireChanges::allOk); - assertThat(redisTemplate.opsForHash().getExpire(key, TimeUnit.MILLISECONDS, List.of(key1, key2))) + assertThat(redisTemplate.opsForHash().getTimeToLive(key, TimeUnit.MILLISECONDS, List.of(key1, key2))) .satisfies(expirations -> { assertThat(expirations.missing()).isEmpty(); - assertThat(expirations.precision()).isEqualTo(TimeUnit.MILLISECONDS); - assertThat(expirations.expirationOf(key1)).extracting(Expiration::raw, InstanceOfAssertFactories.LONG) + assertThat(expirations.timeUnit()).isEqualTo(TimeUnit.MILLISECONDS); + assertThat(expirations.expirationOf(key1)).extracting(TimeToLive::raw, InstanceOfAssertFactories.LONG) .isBetween(0L, 500L); assertThat(expirations.ttlOf(key1)).isBetween(Duration.ZERO, Duration.ofMillis(500)); }); } @ParameterizedRedisTest + @EnabledOnCommand("HEXPIRE") void expireThrowsErrorOfNanoPrecision() { K key = keyFactory.instance(); HK key1 = hashKeyFactory.instance(); assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> redisTemplate.opsForHash().getExpire(key, TimeUnit.NANOSECONDS, List.of(key1))); + .isThrownBy(() -> redisTemplate.opsForHash().getTimeToLive(key, TimeUnit.NANOSECONDS, List.of(key1))); } @ParameterizedRedisTest + @EnabledOnCommand("HEXPIRE") void testExpireWithOptionsNone() { K key = keyFactory.instance(); @@ -360,7 +393,7 @@ public class DefaultHashOperationsIntegrationTests { assertThat(redisTemplate.opsForHash().persist(key, List.of(key2))).satisfies(ExpireChanges::allOk); - assertThat(redisTemplate.opsForHash().getExpire(key, List.of(key1, key2))).satisfies(expirations -> { + assertThat(redisTemplate.opsForHash().getTimeToLive(key, List.of(key1, key2))).satisfies(expirations -> { assertThat(expirations.expirationOf(key1).isPersistent()).isFalse(); assertThat(expirations.expirationOf(key2).isPersistent()).isTrue(); }); diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java index a128a2929..2d574ee12 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java @@ -15,11 +15,10 @@ */ package org.springframework.data.redis.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assumptions.assumeThat; -import static org.junit.jupiter.api.condition.OS.MAC; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; +import static org.junit.jupiter.api.condition.OS.*; -import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import reactor.test.StepVerifier; import java.time.Duration; @@ -34,10 +33,12 @@ import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.condition.DisabledOnOs; + import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.RawObjectFactory; import org.springframework.data.redis.SettingsUtils; import org.springframework.data.redis.StringObjectFactory; +import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.convert.Converters; @@ -523,7 +524,7 @@ public class DefaultReactiveHashOperationsIntegrationTests { assertThat(changes.allOk()).isTrue(); }).verifyComplete(); - hashOperations.getExpire(key, List.of(key1)) // + hashOperations.getTimeToLive(key, List.of(key1)) // .as(StepVerifier::create) // .assertNext(it -> { assertThat(it.expirationOf(key1).raw()).isBetween(0L, 2L); @@ -576,7 +577,7 @@ public class DefaultReactiveHashOperationsIntegrationTests { assertThat(changes.allOk()).isTrue(); }).verifyComplete(); - hashOperations.getExpire(key, TimeUnit.SECONDS, List.of(key1, key2)) // + hashOperations.getTimeToLive(key, TimeUnit.SECONDS, List.of(key1, key2)) // .as(StepVerifier::create) // .assertNext(it -> { assertThat(it.expirationOf(key1).raw()).isBetween(0L, 5L); @@ -603,7 +604,7 @@ public class DefaultReactiveHashOperationsIntegrationTests { assertThat(changes.allOk()).isTrue(); }).verifyComplete(); - redisTemplate.opsForHash().getExpire(key, List.of(key1, key2)).as(StepVerifier::create)// + redisTemplate.opsForHash().getTimeToLive(key, List.of(key1, key2)).as(StepVerifier::create)// .assertNext(it -> { assertThat(it.expirationOf(key1).raw()).isBetween(0L, 2L); assertThat(it.expirationOf(key2).raw()).isBetween(0L, 2L); @@ -633,7 +634,7 @@ public class DefaultReactiveHashOperationsIntegrationTests { assertThat(changes.allOk()).isTrue(); }).verifyComplete(); - redisTemplate.opsForHash().getExpire(key, List.of(key1, key2)).as(StepVerifier::create)// + redisTemplate.opsForHash().getTimeToLive(key, List.of(key1, key2)).as(StepVerifier::create)// .assertNext(expirations -> { assertThat(expirations.persistent()).contains(key1, key2); }).verifyComplete(); diff --git a/src/test/java/org/springframework/data/redis/core/ExpirationsUnitTest.java b/src/test/java/org/springframework/data/redis/core/types/ExpirationsUnitTest.java similarity index 80% rename from src/test/java/org/springframework/data/redis/core/ExpirationsUnitTest.java rename to src/test/java/org/springframework/data/redis/core/types/ExpirationsUnitTest.java index 5fc1953d3..db5ba9860 100644 --- a/src/test/java/org/springframework/data/redis/core/ExpirationsUnitTest.java +++ b/src/test/java/org/springframework/data/redis/core/types/ExpirationsUnitTest.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.redis.core; +package org.springframework.data.redis.core.types; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import java.time.Duration; import java.util.List; @@ -26,11 +26,14 @@ import java.util.stream.IntStream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -import org.springframework.data.redis.core.Expirations.Timeouts; + +import org.springframework.data.redis.core.types.Expirations.Timeouts; /** + * Unit test for {@link Expirations} + * * @author Christoph Strobl - * @since 2025/02 + * @author Mark Paluch */ class ExpirationsUnitTest { @@ -38,19 +41,19 @@ class ExpirationsUnitTest { static final String KEY_2 = "key-2"; static final String KEY_3 = "key-3"; - @ParameterizedTest + @ParameterizedTest // GH-3054 @EnumSource(TimeUnit.class) void expirationMemorizesSourceUnit(TimeUnit targetUnit) { Expirations exp = Expirations.of(targetUnit, List.of(KEY_1), new Timeouts(TimeUnit.SECONDS, List.of(120L))); - assertThat(exp.expirations().get(0)).satisfies(expiration -> { + assertThat(exp.ttl().get(0)).satisfies(expiration -> { assertThat(expiration.raw()).isEqualTo(120L); assertThat(expiration.value()).isEqualTo(targetUnit.convert(120, TimeUnit.SECONDS)); }); } - @Test + @Test // GH-3054 void expirationsCategorizesElements() { Expirations exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L))); @@ -60,7 +63,7 @@ class ExpirationsUnitTest { assertThat(exp.expiring()).containsExactly(Map.entry(KEY_3, Duration.ofMinutes(2))); } - @Test + @Test // GH-3054 void returnsNullForMissingElements() { Expirations exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L))); @@ -69,7 +72,7 @@ class ExpirationsUnitTest { assertThat(exp.ttlOf("missing")).isNull(); } - @Test + @Test // GH-3054 void ttlReturnsDurationForEntriesWithTimeout() { Expirations exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L))); @@ -77,7 +80,7 @@ class ExpirationsUnitTest { assertThat(exp.ttlOf(KEY_3)).isEqualTo(Duration.ofMinutes(2)); } - @Test + @Test // GH-3054 void ttlReturnsNullForPersistentAndMissingEntries() { Expirations exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L))); @@ -86,6 +89,14 @@ class ExpirationsUnitTest { assertThat(exp.ttlOf(KEY_2)).isNull(); } + @Test // GH-3054 + void shouldRenderToString() { + + assertThat(Expirations.TimeToLive.PERSISTENT).hasToString("PERSISTENT"); + assertThat(Expirations.TimeToLive.MISSING).hasToString("MISSING"); + assertThat(Expirations.TimeToLive.of(1, TimeUnit.SECONDS)).hasToString("1 SECONDS"); + } + static Expirations createExpirations(Timeouts timeouts) { List keys = IntStream.range(1, timeouts.raw().size() + 1).mapToObj("key-%s"::formatted).toList(); diff --git a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisMapIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisMapIntegrationTests.java index 5ce8e5441..0a03b7340 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisMapIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisMapIntegrationTests.java @@ -15,9 +15,8 @@ */ package org.springframework.data.redis.support.collections; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assumptions.assumeThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.*; +import static org.assertj.core.api.Assumptions.*; import java.io.IOException; import java.text.DecimalFormat; @@ -36,14 +35,16 @@ import java.util.concurrent.TimeUnit; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.BeforeEach; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.DoubleAsStringObjectFactory; import org.springframework.data.redis.LongAsStringObjectFactory; import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.RawObjectFactory; import org.springframework.data.redis.RedisSystemException; -import org.springframework.data.redis.core.ExpireChanges; +import org.springframework.data.redis.core.BoundHashFieldExpirationOperations; import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ExpireChanges; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; @@ -204,15 +205,15 @@ public abstract class AbstractRedisMapIntegrationTests { V v1 = getValue(); assertThat(map.put(k1, v1)).isEqualTo(null); - Collection keys = Collections.singletonList(k1); - assertThat(map.expire(Duration.ofSeconds(5), keys)).satisfies(ExpireChanges::allOk); - assertThat(map.getExpire(keys)).satisfies(expiration -> { + BoundHashFieldExpirationOperations ops = map.expiration(Collections.singletonList(k1)); + assertThat(ops.expire(Duration.ofSeconds(5))).satisfies(ExpireChanges::allOk); + assertThat(ops.getTimeToLive()).satisfies(expiration -> { assertThat(expiration.expirationOf(k1).raw()).isBetween(1L, 5L); }); - assertThat(map.getExpire(TimeUnit.MILLISECONDS, keys)).satisfies(expiration -> { + assertThat(ops.getTimeToLive(TimeUnit.MILLISECONDS)).satisfies(expiration -> { assertThat(expiration.expirationOf(k1).raw()).isBetween(1000L, 5000L); }); - assertThat(map.persist(keys)).satisfies(ExpireChanges::allOk); + assertThat(ops.persist()).satisfies(ExpireChanges::allOk); } @ParameterizedRedisTest @@ -223,15 +224,15 @@ public abstract class AbstractRedisMapIntegrationTests { V v1 = getValue(); assertThat(map.put(k1, v1)).isEqualTo(null); - Collection keys = Collections.singletonList(k1); - assertThat(map.expireAt(Instant.now().plusSeconds(5), keys)).satisfies(ExpireChanges::allOk); - assertThat(map.getExpire(keys)).satisfies(expiration -> { + BoundHashFieldExpirationOperations ops = map.expiration(Collections.singletonList(k1)); + assertThat(ops.expireAt(Instant.now().plusSeconds(5))).satisfies(ExpireChanges::allOk); + assertThat(ops.getTimeToLive()).satisfies(expiration -> { assertThat(expiration.expirationOf(k1).raw()).isBetween(1L, 5L); }); - assertThat(map.getExpire(TimeUnit.MILLISECONDS, keys)).satisfies(expiration -> { + assertThat(ops.getTimeToLive(TimeUnit.MILLISECONDS)).satisfies(expiration -> { assertThat(expiration.expirationOf(k1).raw()).isBetween(1000L, 5000L); }); - assertThat(map.persist(keys)).satisfies(ExpireChanges::allOk); + assertThat(ops.persist()).satisfies(ExpireChanges::allOk); } @ParameterizedRedisTest diff --git a/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java index 17e63c311..be3e627a2 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java @@ -185,12 +185,6 @@ public class RedisPropertiesIntegrationTests extends RedisMapIntegrationTests { assertThat(keys.contains(key3)).isTrue(); } - @ParameterizedRedisTest - @Override - public void testScanWorksCorrectly() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> super.testScanWorksCorrectly()); - } - // DATAREDIS-241 public static Collection testParams() {