Polishing.

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
This commit is contained in:
Mark Paluch
2025-02-11 11:32:38 +01:00
parent 5228bc7008
commit c6ba95f271
32 changed files with 1461 additions and 928 deletions

View File

@@ -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
|=========================================================

View File

@@ -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<Long> expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration,
public @Nullable List<Long> 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<Long> hExpire(byte[] key, long seconds, byte[]... fields) {
return this.delegate.hExpire(key, seconds, fields);
public List<Long> hExpire(byte[] key, long seconds, FieldExpirationOptions.Condition condition, byte[]... fields) {
return this.delegate.hExpire(key, seconds, condition, fields);
}
@Override
public List<Long> hpExpire(byte[] key, long millis, byte[]... fields) {
return this.delegate.hpExpire(key, millis, fields);
public List<Long> hpExpire(byte[] key, long millis, FieldExpirationOptions.Condition condition, byte[]... fields) {
return this.delegate.hpExpire(key, millis, condition, fields);
}
@Override
public List<Long> hExpireAt(byte[] key, long unixTime, byte[]... fields) {
return this.delegate.hExpireAt(key, unixTime, fields);
public List<Long> hExpireAt(byte[] key, long unixTime, FieldExpirationOptions.Condition condition, byte[]... fields) {
return this.delegate.hExpireAt(key, unixTime, condition, fields);
}
@Override
public List<Long> hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) {
return this.delegate.hpExpireAt(key, unixTimeInMillis, fields);
public List<Long> 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<Long> expireHashField(String key, org.springframework.data.redis.core.types.Expiration expiration,
public @Nullable List<Long> 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<Long> hExpire(String key, long seconds, String... fields) {
return hExpire(serialize(key), seconds, serializeMulti(fields));
public List<Long> hExpire(String key, long seconds, FieldExpirationOptions.Condition condition, String... fields) {
return hExpire(serialize(key), seconds, condition, serializeMulti(fields));
}
@Override
public List<Long> hpExpire(String key, long millis, String... fields) {
return hpExpire(serialize(key), millis, serializeMulti(fields));
public List<Long> hpExpire(String key, long millis, FieldExpirationOptions.Condition condition, String... fields) {
return hpExpire(serialize(key), millis, condition, serializeMulti(fields));
}
@Override
public List<Long> hExpireAt(String key, long unixTime, String... fields) {
return hExpireAt(serialize(key), unixTime, serializeMulti(fields));
public List<Long> hExpireAt(String key, long unixTime, FieldExpirationOptions.Condition condition, String... fields) {
return hExpireAt(serialize(key), unixTime, condition, serializeMulti(fields));
}
@Override
public List<Long> hpExpireAt(String key, long unixTimeInMillis, String... fields) {
return hpExpireAt(serialize(key), unixTimeInMillis, serializeMulti(fields));
public List<Long> hpExpireAt(String key, long unixTimeInMillis, FieldExpirationOptions.Condition condition,
String... fields) {
return hpExpireAt(serialize(key), unixTimeInMillis, condition, serializeMulti(fields));
}
@Override

View File

@@ -1483,28 +1483,58 @@ public interface DefaultedRedisConnection extends RedisCommands, RedisCommandsPr
@Override
@Deprecated
default List<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<byte[]> range,
org.springframework.data.redis.connection.Limit limit) {
default Long zRangeStoreByLex(byte[] dstKey, byte[] srcKey, org.springframework.data.domain.Range<byte[]> 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<? extends Number> range,
org.springframework.data.redis.connection.Limit limit) {
org.springframework.data.redis.connection.Limit limit) {
return zSetCommands().zRangeStoreByScore(dstKey, srcKey, range, limit);
}

View File

@@ -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
}
}
}

View File

@@ -858,7 +858,9 @@ public interface ReactiveHashCommands {
private ExpireCommand(@Nullable ByteBuffer key, List<ByteBuffer> fields, Expiration expiration,
FieldExpirationOptions options) {
super(key, fields);
this.expiration = expiration;
this.options = options;
}
@@ -921,6 +923,7 @@ public interface ReactiveHashCommands {
default Mono<Long> 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<Long> hExpire(ByteBuffer key, Duration duration, List<ByteBuffer> 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 <a href="https://redis.io/commands/hexpire">Redis Documentation: HEXPIRE</a>
*/
Flux<NumericResponse<ExpireCommand, Long>> expireHashField(Publisher<ExpireCommand> commands);
Flux<NumericResponse<ExpireCommand, Long>> applyExpiration(Publisher<ExpireCommand> 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<Long> 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<Long> hpExpire(ByteBuffer key, Duration duration, List<ByteBuffer> 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<Long> 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<Long> hExpireAt(ByteBuffer key, Instant expireAt, List<ByteBuffer> 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<Long> 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<Long> hpExpireAt(ByteBuffer key, Instant expireAt, List<ByteBuffer> 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<Long> hTtl(ByteBuffer key, ByteBuffer field) {
return hTtl(key, Collections.singletonList(field)).singleOrEmpty();
}
@@ -1151,7 +1160,6 @@ public interface ReactiveHashCommands {
* @since 3.5
*/
default Flux<Long> hTtl(ByteBuffer key, List<ByteBuffer> fields) {
return hTtl(Flux.just(new HashFieldsCommand(key, fields))).mapNotNull(NumericResponse::getOutput);
}
@@ -1179,7 +1187,6 @@ public interface ReactiveHashCommands {
* @since 3.5
*/
default Mono<Long> hpTtl(ByteBuffer key, ByteBuffer field) {
return hpTtl(key, Collections.singletonList(field)).singleOrEmpty();
}
@@ -1195,7 +1202,6 @@ public interface ReactiveHashCommands {
* @since 3.5
*/
default Flux<Long> hpTtl(ByteBuffer key, List<ByteBuffer> fields) {
return hpTtl(Flux.just(new HashFieldsCommand(key, fields))).mapNotNull(NumericResponse::getOutput);
}
@@ -1210,4 +1216,5 @@ public interface ReactiveHashCommands {
* @see <a href="https://redis.io/commands/hpttl">Redis Documentation: HPTTL</a>
*/
Flux<NumericResponse<HashFieldsCommand, Long>> hpTtl(Publisher<HashFieldsCommand> commands);
}

View File

@@ -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<Long> expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration,
byte[]... fields) {
return expireHashField(key, expiration, FieldExpirationOptions.none(), fields);
default @Nullable List<Long> applyExpiration(byte[] key,
org.springframework.data.redis.core.types.Expiration expiration, byte[]... fields) {
return applyExpiration(key, expiration, FieldExpirationOptions.none(), fields);
}
@Nullable List<Long> 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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
@Nullable
List<Long> hExpire(byte[] key, long seconds, byte[]... fields);
default List<Long> 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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
@Nullable
default List<Long> 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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
* @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 <a href="https://redis.io/docs/latest/commands/hpexpire/">Redis Documentation: HPEXPIRE</a>
* @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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
@Nullable
List<Long> hpExpire(byte[] key, long millis, byte[]... fields);
List<Long> 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 <a href="https://redis.io/docs/latest/commands/hpexpire/">Redis Documentation: HPEXPIRE</a>
* @since 3.5
*/
@Nullable
default List<Long> 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 <a href="https://redis.io/docs/latest/commands/hpexpire/">Redis Documentation: HPEXPIRE</a>
* @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 <a href="https://redis.io/docs/latest/commands/hpexpire/">Redis Documentation: HPEXPIRE</a>
* @since 3.5
*/
@Nullable
List<Long> 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 <a href="https://redis.io/docs/latest/commands/hexpireat/">Redis Documentation: HEXPIREAT</a>
* @since 3.5
*/
@Nullable
List<Long> hExpireAt(byte[] key, long unixTime, byte[]... fields);
default List<Long> 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 <a href="https://redis.io/docs/latest/commands/hexpireat/">Redis Documentation: HEXPIREAT</a>
* @since 3.5
*/
@Nullable
List<Long> 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 <a href="https://redis.io/docs/latest/commands/hpexpireat/">Redis Documentation: HPEXPIREAT</a>
* @since 3.5
*/
@Nullable
List<Long> hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields);
default List<Long> 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 <a href="https://redis.io/docs/latest/commands/hpexpireat/">Redis Documentation: HPEXPIREAT</a>
* @since 3.5
*/
@Nullable
List<Long> 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 <a href="https://redis.io/docs/latest/commands/hpersist/">Redis Documentation: HPERSIST</a>
* @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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HTTL</a>
* @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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HTTL</a>
* @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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HTTL</a>
* @since 3.5
*/

View File

@@ -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 <a href="https://redis.io/commands/zrangebylex">Redis Documentation: ZRANGEBYLEX</a>
* @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<String> zRangeByLex(String key, org.springframework.data.domain.Range<String> range,
org.springframework.data.redis.connection.Limit limit);
@@ -1983,7 +1982,8 @@ public interface StringRedisConnection extends RedisConnection {
* @return
* @since 2.4
* @see <a href="https://redis.io/commands/zrevrangebylex">Redis Documentation: ZREVRANGEBYLEX</a>
* @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<String> zRevRangeByLex(String key, org.springframework.data.domain.Range<String> 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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
* @since 3.4
* @since 3.5
*/
@Nullable
List<Long> hExpire(String key, long seconds, String... fields);
default List<Long> 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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
@Nullable
List<Long> 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 <a href="https://redis.io/docs/latest/commands/hpexpire/">Redis Documentation: HPEXPIRE</a>
* @since 3.4
* @since 3.5
*/
@Nullable
List<Long> hpExpire(String key, long millis, String... fields);
default List<Long> 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 <a href="https://redis.io/docs/latest/commands/hpexpire/">Redis Documentation: HPEXPIRE</a>
* @since 3.5
*/
@Nullable
List<Long> 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 <a href="https://redis.io/docs/latest/commands/hexpireat/">Redis Documentation: HEXPIREAT</a>
* @since 3.4
* @since 3.5
*/
@Nullable
List<Long> hExpireAt(String key, long unixTime, String... fields);
default List<Long> 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 <a href="https://redis.io/docs/latest/commands/hexpireat/">Redis Documentation: HEXPIREAT</a>
* @since 3.5
*/
@Nullable
List<Long> 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 <a href="https://redis.io/docs/latest/commands/hpexpireat/">Redis Documentation: HPEXPIREAT</a>
* @since 3.4
* @since 3.5
*/
@Nullable
List<Long> hpExpireAt(String key, long unixTimeInMillis, String... fields);
default List<Long> 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 <a href="https://redis.io/docs/latest/commands/hpexpireat/">Redis Documentation: HPEXPIREAT</a>
* @since 3.5
*/
@Nullable
List<Long> 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 <a href="https://redis.io/docs/latest/commands/hpersist/">Redis Documentation: HPERSIST</a>
* @since 3.4
* @since 3.5
*/
@Nullable
List<Long> 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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HTTL</a>
* @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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HTTL</a>
* @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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HTTL</a>
* @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}.

View File

@@ -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<Long> expireHashField(byte[] key, Expiration expiration, FieldExpirationOptions options,
public List<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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);
}
}

View File

@@ -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<Long> hExpire(byte[] key, long seconds, byte[]... fields) {
return connection.invoke().just(Jedis::hexpire, PipelineBinaryCommands::hexpire, key, seconds, fields);
public List<Long> 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<Long> hpExpire(byte[] key, long millis, byte[]... fields) {
return connection.invoke().just(Jedis::hpexpire, PipelineBinaryCommands::hpexpire, key, millis, fields);
public List<Long> 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<Long> expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration,
FieldExpirationOptions options, byte[]... fields) {
public List<Long> 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<Long> hExpireAt(byte[] key, long unixTime, byte[]... fields) {
return connection.invoke().just(Jedis::hexpireAt, PipelineBinaryCommands::hexpireAt, key, unixTime, fields);
}
public List<Long> hpExpireAt(byte[] key, long unixTimeInMillis, FieldExpirationOptions.Condition condition,
byte[]... fields) {
@Override
public List<Long> 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);
}

View File

@@ -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);

View File

@@ -215,63 +215,28 @@ class LettuceHashCommands implements RedisHashCommands {
}
@Override
public @Nullable List<Long> 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 <K, V> void build(CommandArgs<K, V> 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<Long> 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<Long> hExpire(byte[] key, long seconds, byte[]... fields) {
return connection.invoke().fromMany(RedisHashAsyncCommands::hexpire, key, seconds, fields).toList();
public List<Long> 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<Long> hpExpire(byte[] key, long millis, byte[]... fields) {
return connection.invoke().fromMany(RedisHashAsyncCommands::hpexpire, key, millis, fields).toList();
public List<Long> 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<Long> hExpireAt(byte[] key, long unixTime, byte[]... fields) {
return connection.invoke().fromMany(RedisHashAsyncCommands::hexpireat, key, unixTime, fields).toList();
}
@Override
public List<Long> hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) {
return connection.invoke().fromMany(RedisHashAsyncCommands::hpexpireat, key, unixTimeInMillis, fields).toList();
public List<Long> 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 <K, V> void build(CommandArgs<K, V> args) {
if (ObjectUtils.nullSafeEquals(condition, FieldExpirationOptions.Condition.ALWAYS)) {
return;
}
args.add(condition.name());
}
};
}
}

View File

@@ -269,7 +269,8 @@ class LettuceReactiveHashCommands implements ReactiveHashCommands {
}
@Override
public Flux<NumericResponse<ExpireCommand, Long>> expireHashField(Publisher<ExpireCommand> commands) {
public Flux<NumericResponse<ExpireCommand, Long>> applyExpiration(Publisher<ExpireCommand> 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<NumericResponse<HashFieldsCommand, Long>> hPersist(Publisher<HashFieldsCommand> 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<NumericResponse<HashFieldsCommand, Long>> hTtl(Publisher<HashFieldsCommand> 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<NumericResponse<HashFieldsCommand, Long>> hpTtl(Publisher<HashFieldsCommand> commands) {
return connection.execute(cmd -> Flux.from(commands).concatMap(command -> {
Assert.notNull(command.getKey(), "Key must not be null");

View File

@@ -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 <HK> type of the hash field names.
* @author Mark Paluch
* @since 3.5
*/
public interface BoundHashFieldExpirationOperations<HK> {
/**
* Apply {@link Expiration} to the hash without any additional constraints.
*
* @param expiration the expiration definition.
* @return changes to the hash fields.
*/
default ExpireChanges<HK> 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<HK> 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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
@Nullable
ExpireChanges<HK> 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 <a href="https://redis.io/docs/latest/commands/hexpireat/">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
@Nullable
ExpireChanges<HK> 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 <a href="https://redis.io/docs/latest/commands/hpersist/">Redis Documentation: HPERSIST</a>
* @since 3.5
*/
@Nullable
ExpireChanges<HK> 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 <a href="https://redis.io/docs/latest/commands/httl/">Redis Documentation: HTTL</a>
* @since 3.5
*/
@Nullable
Expirations<HK> 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 <a href="https://redis.io/docs/latest/commands/httl/">Redis Documentation: HTTL</a>
* @since 3.5
*/
@Nullable
Expirations<HK> getTimeToLive(TimeUnit timeUnit);
}

View File

@@ -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<H, HK, HV> extends BoundKeyOperations<H> {
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<H, HK, HV> extends BoundKeyOperations<H> {
Map.Entry<HK, HV> 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<H, HK, HV> extends BoundKeyOperations<H> {
@Nullable
Long lengthOfValue(HK hashKey);
default ExpireChanges<HK> expire(Expiration expiration, Collection<HK> hashKeys) {
return expire(expiration, FieldExpirationOptions.none(), hashKeys);
}
ExpireChanges<HK> expire(Expiration expiration, FieldExpirationOptions options, Collection<HK> 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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
@Nullable
ExpireChanges<HK> expire(Duration timeout, Collection<HK> 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 <a href="https://redis.io/docs/latest/commands/hexpireat/">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
@Nullable
ExpireChanges<HK> expireAt(Instant expireAt, Collection<HK> 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 <a href="https://redis.io/docs/latest/commands/hpersist/">Redis Documentation: HPERSIST</a>
* @since 3.5
*/
@Nullable
ExpireChanges<HK> persist(Collection<HK> 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 <a href="https://redis.io/docs/latest/commands/httl/">Redis Documentation: HTTL</a>
* @since 3.5
*/
@Nullable
Expirations<HK> getExpire(Collection<HK> 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 <a href="https://redis.io/docs/latest/commands/httl/">Redis Documentation: HTTL</a>
* @since 3.5
*/
@Nullable
Expirations<HK> getExpire(TimeUnit timeUnit, Collection<HK> hashKeys);
/**
* Get size of hash at the bound key.
*
@@ -297,8 +215,45 @@ public interface BoundHashOperations<H, HK, HV> extends BoundKeyOperations<H> {
*/
Cursor<Map.Entry<HK, HV>> 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<HK> 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<HK> 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<HK> expiration(Collection<HK> hashFields) {
return new DefaultBoundHashFieldExpirationOperations<>(getOperations().opsForHash(), getKey(), () -> hashFields);
}
/**
* @return never {@literal null}.
*/
RedisOperations<H, ?> getOperations();
}

View File

@@ -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<H, HK> implements BoundHashFieldExpirationOperations<HK> {
private final HashOperations<H, HK, ?> operations;
private final H key;
private final Supplier<? extends Collection<HK>> hashFields;
public DefaultBoundHashFieldExpirationOperations(HashOperations<H, HK, ?> operations, H key,
Supplier<? extends Collection<HK>> hashFields) {
this.operations = operations;
this.key = key;
this.hashFields = hashFields;
}
@Override
public ExpireChanges<HK> expire(Expiration expiration, Hash.FieldExpirationOptions options) {
return operations.expire(key, expiration, options, getHashKeys());
}
@Nullable
@Override
public ExpireChanges<HK> expire(Duration timeout) {
return operations.expire(key, timeout, getHashKeys());
}
@Nullable
@Override
public ExpireChanges<HK> expireAt(Instant expireAt) {
return operations.expireAt(key, expireAt, getHashKeys());
}
@Nullable
@Override
public ExpireChanges<HK> persist() {
return operations.persist(key, getHashKeys());
}
@Nullable
@Override
public Expirations<HK> getTimeToLive() {
return operations.getTimeToLive(key, getHashKeys());
}
@Nullable
@Override
public Expirations<HK> getTimeToLive(TimeUnit timeUnit) {
return operations.getTimeToLive(key, timeUnit, getHashKeys());
}
private Collection<HK> getHashKeys() {
Collection<HK> hks = hashFields.get();
Assert.state(hks != null, "Hash keys must not be null");
return hks;
}
}

View File

@@ -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<K, HK, HV> extends AbstractOperations<K, Object> imp
byte[] rawKey = rawKey(key);
byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray());
boolean splitSecond = TimeoutUtils.hasMillis(duration);
boolean hasMillis = TimeoutUtils.hasMillis(duration);
List<Long> raw = execute(connection -> {
if (splitSecond) {
return connection.hashCommands().hpExpire(rawKey, duration.toMillis(), rawHashKeys);
}
return connection.hashCommands().hExpire(rawKey, TimeoutUtils.toSeconds(duration), rawHashKeys);
});
List<Long> 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<K, HK, HV> extends AbstractOperations<K, Object> imp
byte[] rawKey = rawKey(key);
byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray());
Long millis = instant.toEpochMilli();
long millis = instant.toEpochMilli();
List<Long> raw = execute(connection -> TimeoutUtils.containsSplitSecond(millis)
? connection.hashCommands().hpExpireAt(rawKey, millis, rawHashKeys)
@@ -257,10 +254,10 @@ class DefaultHashOperations<K, HK, HV> extends AbstractOperations<K, Object> imp
public ExpireChanges<HK> expire(K key, Expiration expiration, FieldExpirationOptions options, Collection<HK> hashKeys) {
List<HK> orderedKeys = List.copyOf(hashKeys);
byte[] rawKey = rawKey(key);
byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray());
List<Long> raw = execute(connection -> connection.hashCommands().expireHashField(rawKey, expiration, options, rawHashKeys));
List<Long> raw = execute(
connection -> connection.hashCommands().applyExpiration(rawKey, expiration, options, rawHashKeys));
return raw != null ? ExpireChanges.of(orderedKeys, raw) : null;
}
@@ -269,7 +266,6 @@ class DefaultHashOperations<K, HK, HV> extends AbstractOperations<K, Object> imp
public ExpireChanges<HK> persist(K key, Collection<HK> hashKeys) {
List<HK> orderedKeys = List.copyOf(hashKeys);
byte[] rawKey = rawKey(key);
byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray());
@@ -279,7 +275,7 @@ class DefaultHashOperations<K, HK, HV> extends AbstractOperations<K, Object> imp
}
@Override
public Expirations<HK> getExpire(K key, TimeUnit timeUnit, Collection<HK> hashKeys) {
public Expirations<HK> getTimeToLive(K key, TimeUnit timeUnit, Collection<HK> hashKeys) {
if(timeUnit.compareTo(TimeUnit.MILLISECONDS) < 0) {
throw new IllegalArgumentException("%s precision is not supported must be >= MILLISECONDS".formatted(timeUnit));

View File

@@ -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<H, HK, HV> implements ReactiveHashOperations
List<ByteBuffer> rawHashKeys = orderedKeys.stream().map(this::rawHashKey).toList();
Mono<List<Long>> 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<H, HK, HV> implements ReactiveHashOperations
@Nullable
@Override
public Mono<Expirations<HK>> getExpire(H key, TimeUnit timeUnit, Collection<HK> hashKeys) {
public Mono<Expirations<HK>> getTimeToLive(H key, TimeUnit timeUnit, Collection<HK> hashKeys) {
if (timeUnit.compareTo(TimeUnit.MILLISECONDS) < 0) {
throw new IllegalArgumentException("%s precision is not supported must be >= MILLISECONDS".formatted(timeUnit));

View File

@@ -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;
* <li>{@link #skipped()} returns keys for which the time to live has not been set because a precondition was not
* met</li>
* </ol>
*
*
* @author Christoph Strobl
* @since 3.5
*/
@@ -50,23 +52,25 @@ public class ExpireChanges<K> {
/**
* 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 <K> the key type used
*/
public static <K> ExpireChanges<K> of(List<K> keys, List<Long> states) {
public static <K> ExpireChanges<K> of(List<K> fields, List<Long> 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<K, ExpiryChangeState> target = CollectionUtils.newLinkedHashMap(keys.size());
for (int i = 0; i < keys.size(); i++) {
target.put(keys.get(i), stateFromValue(states.get(i)));
Map<K, ExpiryChangeState> 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));
}
/**

View File

@@ -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<H, HK, HV> {
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<H, HK, HV> {
Map.Entry<HK, HV> 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<H, HK, HV> {
Cursor<Map.Entry<HK, HV>> 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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
* @since 3.5
@@ -245,15 +248,16 @@ public interface HashOperations<H, HK, HV> {
ExpireChanges<HK> expire(H key, Duration timeout, Collection<HK> 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 <a href="https://redis.io/docs/latest/commands/hexpireat/">Redis Documentation: HEXPIRE</a>
* @since 3.5
@@ -261,16 +265,33 @@ public interface HashOperations<H, HK, HV> {
@Nullable
ExpireChanges<HK> expireAt(H key, Instant expireAt, Collection<HK> 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 <a href="https://redis.io/docs/latest/commands/hexpireat/">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
@Nullable
ExpireChanges<HK> expire(H key, Expiration expiration, FieldExpirationOptions options, Collection<HK> 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 <a href="https://redis.io/docs/latest/commands/hpersist/">Redis Documentation: HPERSIST</a>
* @since 3.5
*/
@@ -278,38 +299,77 @@ public interface HashOperations<H, HK, HV> {
ExpireChanges<HK> persist(H key, Collection<HK> 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 <a href="https://redis.io/docs/latest/commands/httl/">Redis Documentation: HTTL</a>
* @since 3.5
*/
@Nullable
default Expirations<HK> getExpire(H key, Collection<HK> hashKeys) {
return getExpire(key, TimeUnit.SECONDS, hashKeys);
default Expirations<HK> getTimeToLive(H key, Collection<HK> 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 <a href="https://redis.io/docs/latest/commands/httl/">Redis Documentation: HTTL</a>
* @since 3.5
*/
@Nullable
Expirations<HK> getExpire(H key, TimeUnit timeUnit, Collection<HK> hashKeys);
Expirations<HK> getTimeToLive(H key, TimeUnit timeUnit, Collection<HK> 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<HK> 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<HK> 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<HK> expiration(H key, Collection<HK> hashFields) {
return new DefaultBoundHashFieldExpirationOperations<>(this, key, () -> hashFields);
}
/**
* @return never {@literal null}.
*/
RedisOperations<H, ?> getOperations();
}

View File

@@ -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<H, HK, HV> {
Mono<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
@@ -122,10 +123,10 @@ public interface ReactiveHashOperations<H, HK, HV> {
Mono<Map.Entry<HK, HV>> 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<H, HK, HV> {
Mono<ExpireChanges<HK>> expire(H key, Expiration expiration, FieldExpirationOptions options, Collection<HK> 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<H, HK, HV> {
Mono<ExpireChanges<HK>> expireAt(H key, Instant expireAt, Collection<HK> 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<H, HK, HV> {
Mono<ExpireChanges<HK>> persist(H key, Collection<HK> 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<H, HK, HV> {
* @since 3.5
*/
@Nullable
default Mono<Expirations<HK>> getExpire(H key, Collection<HK> hashKeys) {
return getExpire(key, TimeUnit.SECONDS, hashKeys);
default Mono<Expirations<HK>> getTimeToLive(H key, Collection<HK> 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<H, HK, HV> {
* @since 3.5
*/
@Nullable
Mono<Expirations<HK>> getExpire(H key, TimeUnit timeUnit, Collection<HK> hashKeys);
Mono<Expirations<HK>> getTimeToLive(H key, TimeUnit timeUnit, Collection<HK> hashKeys);
/**
* Removes the given {@literal key}.

View File

@@ -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), //

View File

@@ -376,6 +376,7 @@ public interface RedisOperations<K, V> {
@Nullable
Boolean persist(K key);
// TODO: Add TimeToLive (getTimeToLive)
/**
* Get the time to live for {@code key} in seconds.
*

View File

@@ -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.
* <ol>
* <li>{@link #persistent()} returns keys that do not have an associated time to live</li>
* <li>{@link #missing()} returns keys that do not exist and therefore have no associated time to live</li>
* <li>{@link #expirations()} returns the ordered list of {@link Expiration expirations} based on the raw values</li>
* <li>{@link #ttl()} returns the ordered list of {@link TimeToLive expirations} based on the raw values</li>
* <li>{@link #expiring()} returns the expiring keys along with their {@link Duration time to live}</li>
* </ol>
*
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 3.5
*/
public class Expirations<K> { // TODO: should we move this to let's say Hash.class or another place
public class Expirations<K> {
private final TimeUnit unit;
private final Map<K, Expiration> expirations;
private final Map<K, TimeToLive> expirations;
Expirations(TimeUnit unit, Map<K, Expiration> expirations) {
Expirations(TimeUnit unit, Map<K, TimeToLive> 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<K> { // 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<K, Expiration> target = CollectionUtils.newLinkedHashMap(keys.size());
Map<K, TimeToLive> 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<K> { // 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<K> 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<K> 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<Expiration> expirations() {
public List<TimeToLive> 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<Map.Entry<K, Duration>> 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<K> 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<K> filterByState(Expiration filter) {
private Set<K> 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<Long> raw) {
Long get(int index) {
@@ -173,6 +186,7 @@ public class Expirations<K> { // TODO: should we move this to let's say Hash.cla
public int size() {
return raw.size();
}
}
/**
@@ -182,30 +196,57 @@ public class Expirations<K> { // 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<K> { // 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<K> { // 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<K> { // 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);
};
}
}
}

View File

@@ -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<K, V> implements RedisMap<K, V> {
}
@Override
public ExpireChanges<K> expire(Duration timeout, Collection<K> hashKeys) {
return Objects.requireNonNull(hashOps.expire(timeout, hashKeys));
public BoundHashFieldExpirationOperations<K> expiration() {
return hashOps.expiration();
}
@Override
public ExpireChanges<K> expireAt(Instant expireAt, Collection<K> hashKeys) {
return Objects.requireNonNull(hashOps.expireAt(expireAt, hashKeys));
}
@Override
public ExpireChanges<K> persist(Collection<K> hashKeys) {
return Objects.requireNonNull(hashOps.persist(hashKeys));
}
@Override
public Expirations<K> getExpire(Collection<K> hashKeys) {
return Objects.requireNonNull(hashOps.getExpire(hashKeys));
}
@Override
public Expirations<K> getExpire(TimeUnit timeUnit, Collection<K> hashKeys) {
return Objects.requireNonNull(hashOps.getExpire(timeUnit, hashKeys));
public BoundHashFieldExpirationOperations<K> expiration(Collection<K> hashFields) {
return hashOps.expiration(hashFields);
}
private void checkResult(@Nullable Object obj) {

View File

@@ -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<K, V> extends RedisStore, ConcurrentMap<K, V> {
@@ -71,7 +69,7 @@ public interface RedisMap<K, V> extends RedisStore, ConcurrentMap<K, V> {
* @since 2.6
*/
@Nullable
Map.Entry<K,V> randomEntry();
Map.Entry<K, V> randomEntry();
/**
* @since 1.4
@@ -80,69 +78,34 @@ public interface RedisMap<K, V> extends RedisStore, ConcurrentMap<K, V> {
Iterator<Map.Entry<K, V>> 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 <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
* @return the bound operations object to perform operations on the hash field expiration.
* @since 3.5
*/
ExpireChanges<K> expire(Duration timeout, Collection<K> hashKeys);
BoundHashFieldExpirationOperations<K> 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 <a href="https://redis.io/docs/latest/commands/hexpireat/">Redis Documentation: HEXPIRE</a>
* @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<K> expireAt(Instant expireAt, Collection<K> hashKeys);
default BoundHashFieldExpirationOperations<K> 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 <a href="https://redis.io/docs/latest/commands/hpersist/">Redis Documentation: HPERSIST</a>
* @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<K> persist(Collection<K> hashKeys);
BoundHashFieldExpirationOperations<K> expiration(Collection<K> 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 <a href="https://redis.io/docs/latest/commands/httl/">Redis Documentation: HTTL</a>
* @since 3.5
*/
Expirations<K> getExpire(Collection<K> 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 <a href="https://redis.io/docs/latest/commands/httl/">Redis Documentation: HTTL</a>
* @since 3.5
*/
Expirations<K> getExpire(TimeUnit timeUnit, Collection<K> hashKeys);
}

View File

@@ -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<Object, Obje
@Override
public Iterator<java.util.Map.Entry<Object, Object>> scan() {
throw new UnsupportedOperationException();
return (Iterator) delegate.scan();
}
@Override
public ExpireChanges<Object> expire(Duration timeout, Collection<Object> hashKeys) {
Collection<String> keys = hashKeys.stream().map(key -> (String) key).toList();
return (ExpireChanges) hashOps.expire(timeout, keys);
public BoundHashFieldExpirationOperations<Object> expiration() {
return (BoundHashFieldExpirationOperations) delegate.expiration();
}
@Override
public ExpireChanges<Object> expireAt(Instant expireAt, Collection<Object> hashKeys) {
Collection<String> keys = hashKeys.stream().map(key -> (String) key).toList();
return (ExpireChanges) hashOps.expireAt(expireAt, keys);
}
@Override
public ExpireChanges<Object> persist(Collection<Object> hashKeys) {
Collection<String> keys = hashKeys.stream().map(key -> (String) key).toList();
return (ExpireChanges) hashOps.persist(keys);
}
@Override
public Expirations<Object> getExpire(Collection<Object> hashKeys) {
Collection<String> keys = hashKeys.stream().map(key -> (String) key).toList();
return (Expirations) hashOps.getExpire(keys);
}
@Override
public Expirations<Object> getExpire(TimeUnit timeUnit, Collection<Object> hashKeys) {
Collection<String> keys = hashKeys.stream().map(key -> (String) key).toList();
return (Expirations) hashOps.getExpire(timeUnit, keys);
public BoundHashFieldExpirationOperations<Object> expiration(Collection<Object> hashFields) {
return (BoundHashFieldExpirationOperations) delegate.expiration((Collection) hashFields);
}
}

View File

@@ -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<Object> 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<Object> deserializeList(List<Object> objects, int index, RedisSerializer<?> serializer) {
List<Object> 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"));

View File

@@ -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<K, HK, HV> {
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<K, HK, HV> {
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<K, HK, HV> hashOps = redisTemplate.boundHashOps(key);
BoundHashFieldExpirationOperations<HK> 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<K, HK, HV> {
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<K, HK, HV> {
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();
});

View File

@@ -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<K, HK, HV> {
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<K, HK, HV> {
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<K, HK, HV> {
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<K, HK, HV> {
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();

View File

@@ -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<String> 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<String> 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<String> 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<String> 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<String> 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<String> createExpirations(Timeouts timeouts) {
List<String> keys = IntStream.range(1, timeouts.raw().size() + 1).mapToObj("key-%s"::formatted).toList();

View File

@@ -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<K, V> {
V v1 = getValue();
assertThat(map.put(k1, v1)).isEqualTo(null);
Collection<K> keys = Collections.singletonList(k1);
assertThat(map.expire(Duration.ofSeconds(5), keys)).satisfies(ExpireChanges::allOk);
assertThat(map.getExpire(keys)).satisfies(expiration -> {
BoundHashFieldExpirationOperations<K> 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<K, V> {
V v1 = getValue();
assertThat(map.put(k1, v1)).isEqualTo(null);
Collection<K> keys = Collections.singletonList(k1);
assertThat(map.expireAt(Instant.now().plusSeconds(5), keys)).satisfies(ExpireChanges::allOk);
assertThat(map.getExpire(keys)).satisfies(expiration -> {
BoundHashFieldExpirationOperations<K> 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

View File

@@ -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<Object[]> testParams() {