Add field expiration options to reactive API.

Make sure tests do not run when targeting older server versions.

Introduce dedicated objects for time to live and status changes.

See #3054
This commit is contained in:
Christoph Strobl
2025-02-03 11:05:47 +01:00
committed by Mark Paluch
parent 50ea34080c
commit 5228bc7008
31 changed files with 1864 additions and 370 deletions

View File

@@ -30,6 +30,7 @@ import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metric;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.RedisSystemException;
import org.springframework.data.redis.connection.Hash.FieldExpirationOptions;
import org.springframework.data.redis.connection.convert.Converters;
import org.springframework.data.redis.connection.convert.ListConverter;
import org.springframework.data.redis.connection.convert.MapConverter;
@@ -2571,6 +2572,11 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
return convertAndReturn(delegate.hStrLen(key, field), Converters.identityConverter());
}
public @Nullable List<Long> expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration,
FieldExpirationOptions options, byte[]... fields) {
return this.delegate.expireHashField(key, expiration, options, fields);
}
@Override
public List<Long> hExpire(byte[] key, long seconds, byte[]... fields) {
return this.delegate.hExpire(key, seconds, fields);
@@ -2601,11 +2607,21 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
return this.delegate.hTtl(key, fields);
}
@Override
public List<Long> hpTtl(byte[] key, byte[]... fields) {
return this.delegate.hpTtl(key, fields);
}
@Override
public List<Long> hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields) {
return this.delegate.hTtl(key, timeUnit, fields);
}
public @Nullable List<Long> expireHashField(String key, org.springframework.data.redis.core.types.Expiration expiration,
FieldExpirationOptions options, String... fields) {
return expireHashField(serialize(key), expiration, options, serializeMulti(fields));
}
@Override
public List<Long> hExpire(String key, long seconds, String... fields) {
return hExpire(serialize(key), seconds, serializeMulti(fields));
@@ -2641,6 +2657,11 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
return hTtl(serialize(key), timeUnit, serializeMulti(fields));
}
@Override
public List<Long> hpTtl(String key, String... fields) {
return hTtl(serialize(key), serializeMulti(fields));
}
@Override
public void setClientName(byte[] name) {
this.delegate.setClientName(name);

View File

@@ -28,6 +28,7 @@ import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metric;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.Hash.FieldExpirationOptions;
import org.springframework.data.redis.connection.stream.ByteRecord;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
@@ -1527,6 +1528,21 @@ public interface DefaultedRedisConnection extends RedisCommands, RedisCommandsPr
return hashCommands().hTtl(key, timeUnit, fields);
}
/** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */
@Override
@Deprecated
default List<Long> hpTtl(byte[] key, byte[]... fields) {
return hashCommands().hpTtl(key, fields);
}
/** @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);
}
// GEO COMMANDS
/** @deprecated in favor of {@link RedisConnection#geoCommands()}}. */

View File

@@ -0,0 +1,115 @@
/*
* 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.connection;
import java.util.Objects;
import org.springframework.lang.Contract;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
/**
* @author Christoph Strobl
* @since 3.5
*/
public interface Hash {
class FieldExpirationOptions {
private static final FieldExpirationOptions NONE = new FieldExpirationOptions(null);
private @Nullable Condition condition;
FieldExpirationOptions(@Nullable Condition condition) {
this.condition = condition;
}
public static FieldExpirationOptions none() {
return NONE;
}
@Contract("_ -> new")
public static FieldExpireOptionsBuilder builder() {
return new FieldExpireOptionsBuilder();
}
public @Nullable Condition getCondition() {
return condition;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FieldExpirationOptions that = (FieldExpirationOptions) o;
return ObjectUtils.nullSafeEquals(this.condition, that.condition);
}
@Override
public int hashCode() {
return Objects.hash(condition);
}
public static class FieldExpireOptionsBuilder {
@Nullable Condition condition;
@Contract("_ -> this")
public FieldExpireOptionsBuilder nx() {
this.condition = Condition.NX;
return this;
}
@Contract("_ -> this")
public FieldExpireOptionsBuilder xx() {
this.condition = Condition.XX;
return this;
}
@Contract("_ -> this")
public FieldExpireOptionsBuilder gt() {
this.condition = Condition.GT;
return this;
}
@Contract("_ -> this")
public FieldExpireOptionsBuilder lt() {
this.condition = Condition.LT;
return this;
}
@Contract("_ -> !null")
public FieldExpirationOptions build() {
return condition == null ? NONE : new FieldExpirationOptions(condition);
}
}
public enum Condition {
/** Set expiration only when the field has no expiration. */
NX,
/** Set expiration only when the field has an existing expiration. */
XX,
/** 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. */
LT
}
}
}

View File

@@ -26,10 +26,12 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
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.ReactiveRedisConnection.BooleanResponse;
import org.springframework.data.redis.connection.ReactiveRedisConnection.Command;
import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse;
@@ -38,6 +40,7 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyScan
import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse;
import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse;
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;
@@ -846,54 +849,59 @@ public interface ReactiveHashCommands {
Flux<NumericResponse<HStrLenCommand, Long>> hStrLen(Publisher<HStrLenCommand> commands);
/**
* @author Tihomir Mateev
* @see <a href="https://redis.io/commands/hexpire">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
class Expire extends HashFieldsCommand {
class ExpireCommand extends HashFieldsCommand {
private final Duration ttl;
/**
* Creates a new {@link Expire} given a {@code key}, a {@link List} of {@code fields} and a time-to-live
*
* @param key can be {@literal null}.
* @param fields must not be {@literal null}.
* @param ttl the duration of the time to live.
*/
private Expire(@Nullable ByteBuffer key, List<ByteBuffer> fields, Duration ttl) {
private final Expiration expiration;
private final FieldExpirationOptions options;
private ExpireCommand(@Nullable ByteBuffer key, List<ByteBuffer> fields, Expiration expiration,
FieldExpirationOptions options) {
super(key, fields);
this.ttl = ttl;
this.expiration = expiration;
this.options = options;
}
/**
* Specify the {@code fields} within the hash to set an expiration for.
*
* @param fields must not be {@literal null}.
* @return new instance of {@link Expire}.
*/
public static Expire expire(List<ByteBuffer> fields, Duration ttl) {
public static ExpireCommand expire(List<ByteBuffer> fields, long timeout, TimeUnit unit) {
Assert.notNull(fields, "Field must not be null");
return new Expire(null, fields, ttl);
return expire(fields, Expiration.from(timeout, unit));
}
/**
* Define the {@code key} the hash is stored at.
*
* @param key must not be {@literal null}.
* @return new instance of {@link Expire}.
*/
public Expire from(ByteBuffer key) {
return new Expire(key, getFields(), ttl);
public static ExpireCommand expire(List<ByteBuffer> fields, Duration ttl) {
Assert.notNull(fields, "Field must not be null");
return expire(fields, Expiration.from(ttl));
}
/**
* @return the ttl.
*/
public Duration getTtl() {
return ttl;
public static ExpireCommand expire(List<ByteBuffer> fields, Expiration expiration) {
return new ExpireCommand(null, fields, expiration, FieldExpirationOptions.none());
}
public static ExpireCommand expireAt(List<ByteBuffer> fields, Instant ttl, TimeUnit precision) {
if (precision.compareTo(TimeUnit.MILLISECONDS) > 0) {
return expire(fields, Expiration.unixTimestamp(ttl.getEpochSecond(), TimeUnit.SECONDS));
}
return expire(fields, Expiration.unixTimestamp(ttl.toEpochMilli(), TimeUnit.MILLISECONDS));
}
public ExpireCommand from(ByteBuffer key) {
return new ExpireCommand(key, getFields(), expiration, options);
}
public ExpireCommand withOptions(FieldExpirationOptions options) {
return new ExpireCommand(getKey(), getFields(), getExpiration(), options);
}
public Expiration getExpiration() {
return expiration;
}
public FieldExpirationOptions getOptions() {
return options;
}
}
@@ -903,51 +911,53 @@ public interface ReactiveHashCommands {
* @param key must not be {@literal null}.
* @param field must not be {@literal null}.
* @param duration must not be {@literal null}.
* @return a {@link Mono} emitting the expiration result - {@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;
* @return a {@link Mono} emitting the expiration result - {@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;
* {@code -2} indicating there is no such field;
* @see <a href="https://redis.io/commands/hexpire">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
default Mono<Long> hExpire(ByteBuffer key, Duration duration, ByteBuffer field) {
Assert.notNull(duration, "Duration must not be null");
Assert.notNull(duration, "Duration must not be null");
return hExpire(key, duration, Collections.singletonList(field)).singleOrEmpty();
}
/**
* Expire a {@link List} of {@literal field} after a given {@link Duration} of time, measured in milliseconds, has passed.
* Expire a {@link List} of {@literal field} after a given {@link Duration} of time, measured in milliseconds, has
* passed.
*
* @param key must not be {@literal null}.
* @param fields must not be {@literal null}.
* @param duration must not be {@literal null}.
* @return a {@link Flux} emitting the expiration results one by one, {@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;
* @return a {@link Flux} emitting the expiration results one by one, {@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;
* @see <a href="https://redis.io/commands/hexpire">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
default Flux<Long> hExpire(ByteBuffer key, Duration duration, List<ByteBuffer> fields) {
Assert.notNull(duration, "Duration must not be null");
return hExpire(Flux.just(Expire.expire(fields, duration).from(key)))
return expireHashField(Flux.just(ExpireCommand.expire(fields, duration).from(key)))
.mapNotNull(NumericResponse::getOutput);
}
/**
* Expire a {@link List} of {@literal field} after a given {@link Duration} of time, measured in milliseconds, has passed.
* Expire a {@link List} of {@literal field} after a given {@link Duration} of time, measured in milliseconds, has
* passed.
*
* @param commands must not be {@literal null}.
* @return a {@link Flux} emitting the expiration results one by one, {@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;
* @return a {@link Flux} emitting the expiration results one by one, {@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;
* @since 3.5
* @see <a href="https://redis.io/commands/hexpire">Redis Documentation: HEXPIRE</a>
*/
Flux<NumericResponse<Expire, Long>> hExpire(Publisher<Expire> commands);
Flux<NumericResponse<ExpireCommand, Long>> expireHashField(Publisher<ExpireCommand> commands);
/**
* Expire a given {@literal field} after a given {@link Duration} of time, measured in milliseconds, has passed.
@@ -955,104 +965,41 @@ public interface ReactiveHashCommands {
* @param key must not be {@literal null}.
* @param field must not be {@literal null}.
* @param duration must not be {@literal null}.
* @return a {@link Mono} emitting the expiration result - {@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;
* @return a {@link Mono} emitting the expiration result - {@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;
* {@code -2} indicating there is no such field;
* @see <a href="https://redis.io/commands/hexpire">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
default Mono<Long> hpExpire(ByteBuffer key, Duration duration, ByteBuffer field) {
Assert.notNull(duration, "Duration must not be null");
Assert.notNull(duration, "Duration must not be null");
return hpExpire(key, duration, Collections.singletonList(field)).singleOrEmpty();
}
/**
* Expire a {@link List} of {@literal field} after a given {@link Duration} of time, measured in milliseconds, has passed.
* Expire a {@link List} of {@literal field} after a given {@link Duration} of time, measured in milliseconds, has
* passed.
*
* @param key must not be {@literal null}.
* @param fields must not be {@literal null}.
* @param duration must not be {@literal null}.
* @return a {@link Flux} emitting the expiration results one by one, {@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;
* @return a {@link Flux} emitting the expiration results one by one, {@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;
* @see <a href="https://redis.io/commands/hexpire">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
default Flux<Long> hpExpire(ByteBuffer key, Duration duration, List<ByteBuffer> fields) {
Assert.notNull(duration, "Duration must not be null");
return hpExpire(Flux.just(Expire.expire(fields, duration).from(key)))
return expireHashField(Flux.just(new ExpireCommand(key, fields,
Expiration.from(duration.toMillis(), TimeUnit.MILLISECONDS), FieldExpirationOptions.none())))
.mapNotNull(NumericResponse::getOutput);
}
/**
* Expire a {@link List} of {@literal field} after a given {@link Duration} of time, measured in milliseconds, has passed.
*
* @param commands must not be {@literal null}.
* @return a {@link Flux} emitting the expiration results one by one, {@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;
* @since 3.5
* @see <a href="https://redis.io/commands/hexpire">Redis Documentation: HEXPIRE</a>
*/
Flux<NumericResponse<Expire, Long>> hpExpire(Publisher<Expire> commands);
/**
* @author Tihomir Mateev
* @see <a href="https://redis.io/commands/hexpireat">Redis Documentation: HEXPIREAT</a>
* @since 3.5
*/
class ExpireAt extends HashFieldsCommand {
private final Instant expireAt;
/**
* Creates a new {@link ExpireAt} given a {@code key}, a {@link List} of {@literal fields} and a {@link Instant}
*
* @param key can be {@literal null}.
* @param fields must not be {@literal null}.
* @param expireAt the {@link Instant} to expire at.
*/
private ExpireAt(@Nullable ByteBuffer key, List<ByteBuffer> fields, Instant expireAt) {
super(key, fields);
this.expireAt = expireAt;
}
/**
* Specify the {@code fields} within the hash to set an expiration for.
*
* @param fields must not be {@literal null}.
* @return new instance of {@link ExpireAt}.
*/
public static ExpireAt expireAt(List<ByteBuffer> fields, Instant expireAt) {
Assert.notNull(fields, "Fields must not be null");
return new ExpireAt(null, fields, expireAt);
}
/**
* Define the {@code key} the hash is stored at.
*
* @param key must not be {@literal null}.
* @return new instance of {@link ExpireAt}.
*/
public ExpireAt from(ByteBuffer key) {
return new ExpireAt(key, getFields(), expireAt);
}
/**
* @return the ttl.
*/
public Instant getExpireAt() {
return expireAt;
}
}
/**
* Expire a given {@literal field} in a given {@link Instant} of time, indicated as an absolute
* <a href="https://en.wikipedia.org/wiki/Unix_time">Unix timestamp</a> in seconds since Unix epoch
@@ -1060,10 +1007,10 @@ public interface ReactiveHashCommands {
* @param key must not be {@literal null}.
* @param field must not be {@literal null}.
* @param expireAt must not be {@literal null}.
* @return a {@link Mono} emitting the expiration result - {@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;
* @return a {@link Mono} emitting the expiration result - {@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;
* @see <a href="https://redis.io/commands/hexpireat">Redis Documentation: HEXPIREAT</a>
* @since 3.5
*/
@@ -1080,33 +1027,20 @@ public interface ReactiveHashCommands {
* @param key must not be {@literal null}.
* @param fields must not be {@literal null}.
* @param expireAt must not be {@literal null}.
* @return a {@link Flux} emitting the expiration results one by one, {@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;
* @return a {@link Flux} emitting the expiration results one by one, {@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;
* @see <a href="https://redis.io/commands/hexpireat">Redis Documentation: HEXPIREAT</a>
* @since 3.5
*/
default Flux<Long> hExpireAt(ByteBuffer key, Instant expireAt, List<ByteBuffer> fields) {
Assert.notNull(expireAt, "Duration must not be null");
return hExpireAt(Flux.just(ExpireAt.expireAt(fields, expireAt).from(key))).mapNotNull(NumericResponse::getOutput);
return expireHashField(Flux.just(ExpireCommand.expireAt(fields, expireAt, TimeUnit.SECONDS).from(key)))
.mapNotNull(NumericResponse::getOutput);
}
/**
* Expire a {@link List} of {@literal field} in a given {@link Instant} of time, indicated as an absolute
* <a href="https://en.wikipedia.org/wiki/Unix_time">Unix timestamp</a> in seconds since Unix epoch
*
* @param commands must not be {@literal null}.
* @return a {@link Flux} emitting the expiration results one by one, {@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;
* @since 3.5
* @see <a href="https://redis.io/commands/hexpireat">Redis Documentation: HEXPIREAT</a>
*/
Flux<NumericResponse<ExpireAt, Long>> hExpireAt(Publisher<ExpireAt> commands);
/**
* Expire a given {@literal field} in a given {@link Instant} of time, indicated as an absolute
* <a href="https://en.wikipedia.org/wiki/Unix_time">Unix timestamp</a> in milliseconds since Unix epoch
@@ -1114,10 +1048,10 @@ public interface ReactiveHashCommands {
* @param key must not be {@literal null}.
* @param field must not be {@literal null}.
* @param expireAt must not be {@literal null}.
* @return a {@link Mono} emitting the expiration result - {@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;
* @return a {@link Mono} emitting the expiration result - {@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;
* @see <a href="https://redis.io/commands/hpexpireat">Redis Documentation: HPEXPIREAT</a>
* @since 3.5
*/
@@ -1134,47 +1068,32 @@ public interface ReactiveHashCommands {
* @param key must not be {@literal null}.
* @param fields must not be {@literal null}.
* @param expireAt must not be {@literal null}.
* @return a {@link Flux} emitting the expiration results one by one, {@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;
* @return a {@link Flux} emitting the expiration results one by one, {@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;
* @see <a href="https://redis.io/commands/hpexpireat">Redis Documentation: HPEXPIREAT</a>
* @since 3.5
*/
default Flux<Long> hpExpireAt(ByteBuffer key, Instant expireAt, List<ByteBuffer> fields) {
Assert.notNull(expireAt, "Duration must not be null");
return hpExpireAt(Flux.just(ExpireAt.expireAt(fields, expireAt).from(key))).mapNotNull(NumericResponse::getOutput);
return expireHashField(Flux.just(ExpireCommand.expireAt(fields, expireAt, TimeUnit.MILLISECONDS).from(key)))
.mapNotNull(NumericResponse::getOutput);
}
/**
* Expire a {@link List} of {@literal field} in a given {@link Instant} of time, indicated as an absolute
* <a href="https://en.wikipedia.org/wiki/Unix_time">Unix timestamp</a> in milliseconds since Unix epoch
*
* @param commands must not be {@literal null}.
* @return a {@link Flux} emitting the expiration results one by one, {@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;
* @since 3.5
* @see <a href="https://redis.io/commands/hpexpireat">Redis Documentation: HPEXPIREAT</a>
*/
Flux<NumericResponse<ExpireAt, Long>> hpExpireAt(Publisher<ExpireAt> commands);
/**
* Persist a given {@literal field} removing any associated expiration, measured as absolute
* <a href="https://en.wikipedia.org/wiki/Unix_time">Unix timestamp</a> in seconds since Unix epoch
*
* @param key must not be {@literal null}.
* @param field must not be {@literal null}.
* @return a {@link Mono} emitting the persist result - {@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;
*
* @return a {@link Mono} emitting the persist result - {@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;
* @see <a href="https://redis.io/commands/hpersist">Redis Documentation: HPERSIST</a>
* @since 3.5
*/
default Mono<Long> hPersist(ByteBuffer key, ByteBuffer field) {
return hPersist(key, Collections.singletonList(field)).singleOrEmpty();
}
@@ -1183,14 +1102,13 @@ public interface ReactiveHashCommands {
*
* @param key must not be {@literal null}.
* @param fields must not be {@literal null}.
* @return a {@link Flux} emitting the persisting results one by one - {@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;
*
* @return a {@link Flux} emitting the persisting results one by one - {@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;
* @see <a href="https://redis.io/commands/hpersist">Redis Documentation: HPERSIST</a>
* @since 3.5
*/
default Flux<Long> hPersist(ByteBuffer key, List<ByteBuffer> fields) {
return hPersist(Flux.just(new HashFieldsCommand(key, fields))).mapNotNull(NumericResponse::getOutput);
}
@@ -1198,9 +1116,9 @@ public interface ReactiveHashCommands {
* Persist a given {@link List} of {@literal field} removing any associated expiration.
*
* @param commands must not be {@literal null}.
* @return a {@link Flux} emitting the persisting results one by one - {@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;
* * @since 3.5
* @return a {@link Flux} emitting the persisting results one by one - {@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; * @since 3.5
* @see <a href="https://redis.io/commands/hpersist">Redis Documentation: HPERSIST</a>
*/
Flux<NumericResponse<HashFieldsCommand, Long>> hPersist(Publisher<HashFieldsCommand> commands);
@@ -1210,9 +1128,9 @@ public interface ReactiveHashCommands {
*
* @param key must not be {@literal null}.
* @param field must not be {@literal null}.
* @return a {@link Mono} emitting the TTL result - 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;
* @return a {@link Mono} emitting the TTL result - 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;
* @see <a href="https://redis.io/commands/httl">Redis Documentation: HTTL</a>
* @since 3.5
*/
@@ -1226,9 +1144,9 @@ public interface ReactiveHashCommands {
*
* @param key must not be {@literal null}.
* @param fields must not be {@literal null}.
* @return a {@link Flux} emitting the TTL results one by one - 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;
* @return a {@link Flux} emitting the TTL results one by one - 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;
* @see <a href="https://redis.io/commands/httl">Redis Documentation: HTTL</a>
* @since 3.5
*/
@@ -1241,23 +1159,22 @@ public interface ReactiveHashCommands {
* Returns the time-to-live of all the given {@literal field} in the {@link List} in seconds.
*
* @param commands must not be {@literal null}.
* @return a {@link Flux} emitting the persisting results one by one - 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;
* @return a {@link Flux} emitting the persisting results one by one - 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;
* @since 3.5
* @see <a href="https://redis.io/commands/httl">Redis Documentation: HTTL</a>
*/
Flux<NumericResponse<HashFieldsCommand, Long>> hTtl(Publisher<HashFieldsCommand> commands);
/**
* Returns the time-to-live of a given {@literal field} in milliseconds.
*
* @param key must not be {@literal null}.
* @param field must not be {@literal null}.
* @return a {@link Mono} emitting the TTL result - 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;
* @return a {@link Mono} emitting the TTL result - 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;
* @see <a href="https://redis.io/commands/hpttl">Redis Documentation: HPTTL</a>
* @since 3.5
*/
@@ -1272,8 +1189,8 @@ public interface ReactiveHashCommands {
* @param key must not be {@literal null}.
* @param fields must not be {@literal null}.
* @return a {@link Flux} emitting the TTL results one by one - 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;
* 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;
* @see <a href="https://redis.io/commands/hpttl">Redis Documentation: HPTTL</a>
* @since 3.5
*/
@@ -1286,9 +1203,9 @@ public interface ReactiveHashCommands {
* Returns the time-to-live of all the given {@literal field} in the {@link List} in milliseconds.
*
* @param commands must not be {@literal null}.
* @return a {@link Flux} emitting the persisting results one by one - 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;
* @return a {@link Flux} emitting the persisting results one by one - 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;
* @since 3.5
* @see <a href="https://redis.io/commands/hpttl">Redis Documentation: HPTTL</a>
*/

View File

@@ -15,11 +15,13 @@
*/
package org.springframework.data.redis.connection;
import java.time.Duration;
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.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.lang.Nullable;
@@ -252,38 +254,83 @@ 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);
}
@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);
/**
* Set time to live for given {@code field} in seconds.
* Set time to live for given {@code fields}.
*
* @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 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.
* @see <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
* @since 3.4
* @since 3.5
*/
@Nullable
List<Long> hExpire(byte[] key, long seconds, byte[]... fields);
default List<Long> hExpire(byte[] key, Duration ttl, byte[]... fields) {
return hExpire(key, ttl.toSeconds(), fields);
}
/**
* Set time to live for given {@code field} in milliseconds.
* 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 key will be expired in milliseconds, 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.
* @see <a href="https://redis.io/docs/latest/commands/hpexpire/">Redis Documentation: HPEXPIRE</a>
* @since 3.4
* @since 3.5
*/
@Nullable
List<Long> hpExpire(byte[] key, long millis, 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 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
default List<Long> hpExpire(byte[] key, Duration ttl, byte[]... fields) {
return hpExpire(key, ttl.toMillis(), fields);
}
/**
* Set the expiration for given {@code field} as a {@literal UNIX} timestamp.
*
@@ -295,7 +342,7 @@ public interface RedisHashCommands {
* 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(byte[] key, long unixTime, byte[]... fields);
@@ -311,7 +358,7 @@ public interface RedisHashCommands {
* 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(byte[] key, long unixTimeInMillis, byte[]... fields);
@@ -325,27 +372,27 @@ public interface RedisHashCommands {
* {@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(byte[] key, byte[]... fields);
/**
* Get the time to live for {@code field} in seconds.
* Get the time to live for {@code fields} in seconds.
*
* @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.
* 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.4
* @since 3.5
*/
@Nullable
List<Long> hTtl(byte[] key, byte[]... fields);
/**
* Get the time to live for {@code field} in and convert it to the given {@link TimeUnit}.
* Get the time to live for {@code fields} in and convert it to the given {@link TimeUnit}.
*
* @param key must not be {@literal null}.
* @param timeUnit must not be {@literal null}.
@@ -354,8 +401,24 @@ public interface RedisHashCommands {
* 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.4
* @since 3.5
*/
@Nullable
// TODO: this is complete nonsense as it would jeopardize negative values
// TODO: this should be a List<Map.Entry<byte, Expiration>>
List<Long> hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields);
/**
* Get the time to live for {@code fields} in milliseconds.
*
* @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.
* @see <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HTTL</a>
* @since 3.5
*/
@Nullable
List<Long> hpTtl(byte[] key, byte[]... fields);
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.redis.connection;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -191,6 +192,20 @@ public interface RedisKeyCommands {
@Nullable
Boolean expire(byte[] key, long seconds);
/**
* Set time to live for given {@code key} using {@link Duration#toSeconds() seconds} precision.
*
* @param key must not be {@literal null}.
* @param duration
* @return {@literal null} when used in pipeline / transaction.
* @see <a href="https://redis.io/commands/expire">Redis Documentation: EXPIRE</a>
* @since 3.5
*/
@Nullable
default Boolean expire(byte[] key, Duration duration) {
return expire(key, duration.toSeconds());
}
/**
* Set time to live for given {@code key} in milliseconds.
*
@@ -202,6 +217,20 @@ public interface RedisKeyCommands {
@Nullable
Boolean pExpire(byte[] key, long millis);
/**
* Set time to live for given {@code key} using {@link Duration#toMillis() milliseconds} precision.
*
* @param key must not be {@literal null}.
* @param duration
* @return {@literal null} when used in pipeline / transaction.
* @see <a href="https://redis.io/commands/pexpire">Redis Documentation: PEXPIRE</a>
* @since 3.5
*/
@Nullable
default Boolean pExpire(byte[] key, Duration duration) {
return pExpire(key, duration.toMillis());
}
/**
* Set the expiration for given {@code key} as a {@literal UNIX} timestamp.
*
@@ -213,6 +242,21 @@ public interface RedisKeyCommands {
@Nullable
Boolean expireAt(byte[] key, long unixTime);
/**
* Set the expiration for given {@code key} as a {@literal UNIX} timestamp in {@link Instant#getEpochSecond() seconds}
* precision.
*
* @param key must not be {@literal null}.
* @param unixTime
* @return {@literal null} when used in pipeline / transaction.
* @see <a href="https://redis.io/commands/expireat">Redis Documentation: EXPIREAT</a>
* @since 3.5
*/
@Nullable
default Boolean expireAt(byte[] key, Instant unixTime) {
return expireAt(key, unixTime.getEpochSecond());
}
/**
* Set the expiration for given {@code key} as a {@literal UNIX} timestamp in milliseconds.
*
@@ -224,6 +268,21 @@ public interface RedisKeyCommands {
@Nullable
Boolean pExpireAt(byte[] key, long unixTimeInMillis);
/**
* Set the expiration for given {@code key} as a {@literal UNIX} timestamp in {@link Instant#toEpochMilli()
* milliseconds} precision.
*
* @param key must not be {@literal null}.
* @param unixTime
* @return {@literal null} when used in pipeline / transaction.
* @see <a href="https://redis.io/commands/pexpireat">Redis Documentation: PEXPIREAT</a>
* @since 3.5
*/
@Nullable
default Boolean pExpireAt(byte[] key, Instant unixTime) {
return pExpireAt(key, unixTime.toEpochMilli());
}
/**
* Remove the expiration from given {@code key}.
*

View File

@@ -2333,6 +2333,7 @@ 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.
*
@@ -2412,7 +2413,7 @@ public interface StringRedisConnection extends RedisConnection {
List<Long> hPersist(String key, String... fields);
/**
* Get the time to live for {@code field} in seconds.
* Get the time to live for {@code fields} in seconds.
*
* @param key must not be {@literal null}.
* @param fields must not be {@literal null}.
@@ -2420,13 +2421,13 @@ public interface StringRedisConnection extends RedisConnection {
* 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.4
* @since 3.5
*/
@Nullable
List<Long> hTtl(String key, String... fields);
/**
* Get the time to live for {@code field} in and convert it to the given {@link TimeUnit}.
* Get the time to live for {@code fields} in and convert it to the given {@link TimeUnit}.
*
* @param key must not be {@literal null}.
* @param timeUnit must not be {@literal null}.
@@ -2435,11 +2436,25 @@ public interface StringRedisConnection extends RedisConnection {
* 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.4
* @since 3.5
*/
@Nullable
List<Long> hTtl(String key, TimeUnit timeUnit, String... fields);
/**
* Get the time to live for {@code fields} in seconds.
*
* @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.
* @see <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HTTL</a>
* @since 3.5
*/
@Nullable
List<Long> hpTtl(String key, String... fields);
// -------------------------------------------------------------------------
// Methods dealing with HyperLogLog
// -------------------------------------------------------------------------

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.redis.connection.jedis;
import redis.clients.jedis.args.ExpiryOption;
import redis.clients.jedis.params.ScanParams;
import redis.clients.jedis.resps.ScanResult;
@@ -26,13 +27,16 @@ import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.Hash.FieldExpirationOptions;
import org.springframework.data.redis.connection.RedisHashCommands;
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.
@@ -281,16 +285,54 @@ class JedisClusterHashCommands implements RedisHashCommands {
ScanParams params = JedisConverters.toScanParams(options);
ScanResult<Entry<byte[], byte[]>> result = connection.getCluster().hscan(key,
JedisConverters.toBytes(cursorId),
ScanResult<Entry<byte[], byte[]>> result = connection.getCluster().hscan(key, JedisConverters.toBytes(cursorId),
params);
return new ScanIteration<>(CursorId.of(result.getCursor()), result.getResult());
}
}.open();
}
@Nullable
@Override
public List<Long> expireHashField(byte[] key, 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);
}
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");
@@ -368,8 +410,19 @@ class JedisClusterHashCommands implements RedisHashCommands {
try {
return connection.getCluster().httl(key, fields).stream()
.map(it -> it != null ? timeUnit.convert(it, TimeUnit.SECONDS) : null)
.toList();
.map(it -> it != null ? timeUnit.convert(it, TimeUnit.SECONDS) : null).toList();
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
@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");
try {
return connection.getCluster().hpttl(key, fields);
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.redis.connection.jedis;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.args.ExpiryOption;
import redis.clients.jedis.commands.PipelineBinaryCommands;
import redis.clients.jedis.params.ScanParams;
import redis.clients.jedis.resps.ScanResult;
@@ -28,6 +29,7 @@ import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.redis.connection.Hash.FieldExpirationOptions;
import org.springframework.data.redis.connection.RedisHashCommands;
import org.springframework.data.redis.connection.convert.Converters;
import org.springframework.data.redis.core.Cursor;
@@ -37,6 +39,7 @@ 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.
@@ -152,7 +155,8 @@ class JedisHashCommands implements RedisHashCommands {
List<Entry<byte[], byte[]>> convertedMapEntryList = new ArrayList<>(mapEntryList.size());
mapEntryList.forEach(entry -> convertedMapEntryList.add(Converters.entryOf(entry.getKey(), entry.getValue())));
mapEntryList
.forEach(entry -> convertedMapEntryList.add(Converters.entryOf(entry.getKey(), entry.getValue())));
return convertedMapEntryList;
@@ -239,8 +243,8 @@ class JedisHashCommands implements RedisHashCommands {
ScanParams params = JedisConverters.toScanParams(options);
ScanResult<Entry<byte[], byte[]>> result = connection.getJedis().hscan(key,
JedisConverters.toBytes(cursorId), params);
ScanResult<Entry<byte[], byte[]>> result = connection.getJedis().hscan(key, JedisConverters.toBytes(cursorId),
params);
return new ScanIteration<>(CursorId.of(result.getCursor()), result.getResult());
}
@@ -262,6 +266,46 @@ class JedisHashCommands implements RedisHashCommands {
return connection.invoke().just(Jedis::hpexpire, PipelineBinaryCommands::hpexpire, key, millis, fields);
}
@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);
}
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);
}
@Override
public List<Long> hExpireAt(byte[] key, long unixTime, byte[]... fields) {
return connection.invoke().just(Jedis::hexpireAt, PipelineBinaryCommands::hexpireAt, key, unixTime, fields);
@@ -269,7 +313,8 @@ class JedisHashCommands implements RedisHashCommands {
@Override
public List<Long> hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) {
return connection.invoke().just(Jedis::hpexpireAt, PipelineBinaryCommands::hpexpireAt, key, unixTimeInMillis, fields);
return connection.invoke().just(Jedis::hpexpireAt, PipelineBinaryCommands::hpexpireAt, key, unixTimeInMillis,
fields);
}
@Override
@@ -288,6 +333,11 @@ class JedisHashCommands implements RedisHashCommands {
.toList(Converters.secondsToTimeUnit(timeUnit));
}
@Override
public List<Long> hpTtl(byte[] key, byte[]... fields) {
return connection.invoke().just(Jedis::hpttl, PipelineBinaryCommands::hpttl, key, fields);
}
@Nullable
@Override
public Long hStrLen(byte[] key, byte[] field) {

View File

@@ -15,10 +15,12 @@
*/
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.ExpireArgs;
import io.lettuce.core.KeyValue;
import io.lettuce.core.MapScanCursor;
import io.lettuce.core.ScanArgs;
import io.lettuce.core.api.async.RedisHashAsyncCommands;
import io.lettuce.core.protocol.CommandArgs;
import java.util.List;
import java.util.Map;
@@ -27,6 +29,7 @@ import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.redis.connection.Hash.FieldExpirationOptions;
import org.springframework.data.redis.connection.RedisHashCommands;
import org.springframework.data.redis.connection.convert.Converters;
import org.springframework.data.redis.core.Cursor;
@@ -36,6 +39,7 @@ 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;
/**
* @author Christoph Strobl
@@ -210,6 +214,46 @@ class LettuceHashCommands implements RedisHashCommands {
return hScan(key, CursorId.initial(), options);
}
@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)
.toList();
}
@Override
public List<Long> hExpire(byte[] key, long seconds, byte[]... fields) {
return connection.invoke().fromMany(RedisHashAsyncCommands::hexpire, key, seconds, fields).toList();
@@ -246,6 +290,11 @@ class LettuceHashCommands implements RedisHashCommands {
.toList(Converters.secondsToTimeUnit(timeUnit));
}
@Override
public List<Long> hpTtl(byte[] key, byte[]... fields) {
return connection.invoke().fromMany(RedisHashAsyncCommands::hpttl, key, fields).toList();
}
/**
* @param key
* @param cursorId

View File

@@ -15,8 +15,10 @@
*/
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.ExpireArgs;
import io.lettuce.core.KeyValue;
import io.lettuce.core.ScanStream;
import io.lettuce.core.protocol.CommandArgs;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -25,10 +27,11 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import org.springframework.data.redis.connection.Hash.FieldExpirationOptions;
import org.springframework.data.redis.connection.ReactiveHashCommands;
import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse;
import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse;
@@ -38,6 +41,7 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiVa
import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse;
import org.springframework.data.redis.connection.convert.Converters;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* @author Christoph Strobl
@@ -265,49 +269,48 @@ class LettuceReactiveHashCommands implements ReactiveHashCommands {
}
@Override
public Flux<NumericResponse<Expire, Long>> hExpire(Publisher<Expire> commands) {
public Flux<NumericResponse<ExpireCommand, Long>> expireHashField(Publisher<ExpireCommand> commands) {
return connection.execute(cmd -> Flux.from(commands).concatMap(command -> {
Assert.notNull(command.getKey(), "Key must not be null");
Assert.notNull(command.getFields(), "Fields must not be null");
return cmd.hexpire(command.getKey(), command.getTtl().toSeconds(), command.getFields().toArray(ByteBuffer[]::new))
.map(value -> new NumericResponse<>(command, value));
}));
}
ByteBuffer[] fields = command.getFields().toArray(ByteBuffer[]::new);
@Override
public Flux<NumericResponse<Expire, Long>> hpExpire(Publisher<Expire> commands) {
return connection.execute(cmd -> Flux.from(commands).concatMap(command -> {
if (command.getExpiration().isPersistent()) {
return cmd.hpersist(command.getKey(), fields).map(value -> new NumericResponse<>(command, value));
}
Assert.notNull(command.getKey(), "Key must not be null");
Assert.notNull(command.getFields(), "Fields must not be null");
ExpireArgs args = new ExpireArgs() {
return cmd.hpexpire(command.getKey(), command.getTtl().toMillis(), command.getFields().toArray(ByteBuffer[]::new))
.map(value -> new NumericResponse<>(command, value));
}));
}
@Override
public <K, V> void build(CommandArgs<K, V> args) {
super.build(args);
if (ObjectUtils.nullSafeEquals(command.getOptions(), FieldExpirationOptions.none())) {
return;
}
@Override
public Flux<NumericResponse<ExpireAt, Long>> hExpireAt(Publisher<ExpireAt> commands) {
return connection.execute(cmd -> Flux.from(commands).concatMap(command -> {
args.add(command.getOptions().getCondition().name());
}
};
Assert.notNull(command.getKey(), "Key must not be null");
Assert.notNull(command.getFields(), "Fields must not be null");
if (command.getExpiration().isUnixTimestamp()) {
return cmd.hexpireat(command.getKey(), command.getExpireAt().getEpochSecond(), command.getFields().toArray(ByteBuffer[]::new))
.map(value -> new NumericResponse<>(command, value));
}));
}
if (command.getExpiration().getTimeUnit().equals(TimeUnit.MILLISECONDS)) {
return cmd
.hpexpireat(command.getKey(), command.getExpiration().getExpirationTimeInMilliseconds(), args, fields)
.map(value -> new NumericResponse<>(command, value));
}
return cmd.hexpireat(command.getKey(), command.getExpiration().getExpirationTimeInSeconds(), args, fields)
.map(value -> new NumericResponse<>(command, value));
}
@Override
public Flux<NumericResponse<ExpireAt, Long>> hpExpireAt(Publisher<ExpireAt> commands) {
return connection.execute(cmd -> Flux.from(commands).concatMap(command -> {
if (command.getExpiration().getTimeUnit().equals(TimeUnit.MILLISECONDS)) {
return cmd.hpexpire(command.getKey(), command.getExpiration().getExpirationTimeInMilliseconds(), args, fields)
.map(value -> new NumericResponse<>(command, value));
}
Assert.notNull(command.getKey(), "Key must not be null");
Assert.notNull(command.getFields(), "Fields must not be null");
return cmd.hpexpireat(command.getKey(), command.getExpireAt().toEpochMilli(), command.getFields().toArray(ByteBuffer[]::new))
return cmd.hexpire(command.getKey(), command.getExpiration().getExpirationTimeInSeconds(), args, fields)
.map(value -> new NumericResponse<>(command, value));
}));
}

View File

@@ -23,6 +23,8 @@ 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;
/**
@@ -157,6 +159,12 @@ 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).
*
@@ -171,7 +179,7 @@ public interface BoundHashOperations<H, HK, HV> extends BoundKeyOperations<H> {
* @since 3.5
*/
@Nullable
List<Long> expire(Duration timeout, Collection<HK> hashKeys);
ExpireChanges<HK> expire(Duration timeout, Collection<HK> hashKeys);
/**
* Set the expiration for given {@code hashKey} (aka field) as a {@literal date} timestamp.
@@ -187,7 +195,7 @@ public interface BoundHashOperations<H, HK, HV> extends BoundKeyOperations<H> {
* @since 3.5
*/
@Nullable
List<Long> expireAt(Instant expireAt, Collection<HK> hashKeys);
ExpireChanges<HK> expireAt(Instant expireAt, Collection<HK> hashKeys);
/**
* Remove the expiration from given {@code hashKey} (aka field).
@@ -200,7 +208,7 @@ public interface BoundHashOperations<H, HK, HV> extends BoundKeyOperations<H> {
* @since 3.5
*/
@Nullable
List<Long> persist(Collection<HK> hashKeys);
ExpireChanges<HK> persist(Collection<HK> hashKeys);
/**
* Get the time to live for {@code hashKey} (aka field) in seconds.
@@ -213,7 +221,7 @@ public interface BoundHashOperations<H, HK, HV> extends BoundKeyOperations<H> {
* @since 3.5
*/
@Nullable
List<Long> getExpire(Collection<HK> hashKeys);
Expirations<HK> getExpire(Collection<HK> hashKeys);
/**
* Get the time to live for {@code hashKey} (aka field) and convert it to the given {@link TimeUnit}.
@@ -227,7 +235,7 @@ public interface BoundHashOperations<H, HK, HV> extends BoundKeyOperations<H> {
* @since 3.5
*/
@Nullable
List<Long> getExpire(TimeUnit timeUnit, Collection<HK> hashKeys);
Expirations<HK> getExpire(TimeUnit timeUnit, Collection<HK> hashKeys);
/**
* Get size of hash at the bound key.

View File

@@ -27,7 +27,10 @@ import java.util.Set;
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.lang.Nullable;
import org.springframework.util.Assert;
@@ -215,44 +218,88 @@ class DefaultHashOperations<K, HK, HV> extends AbstractOperations<K, Object> imp
}
@Override
public List<Long> expire(K key, Duration duration, Collection<HK> hashKeys) {
byte[] rawKey = rawKey(key);
byte[][] rawHashKeys = rawHashKeys(hashKeys.toArray());
long rawTimeout = duration.toMillis();
public ExpireChanges<HK> expire(K key, Duration duration, Collection<HK> hashKeys) {
return execute(connection -> connection.hpExpire(rawKey, rawTimeout, rawHashKeys));
List<HK> orderedKeys = List.copyOf(hashKeys);
byte[] rawKey = rawKey(key);
byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray());
boolean splitSecond = 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);
});
return raw != null ? ExpireChanges.of(orderedKeys, raw) : null;
}
@Override
public List<Long> expireAt(K key, Instant instant, Collection<HK> hashKeys) {
byte[] rawKey = rawKey(key);
byte[][] rawHashKeys = rawHashKeys(hashKeys.toArray());
public ExpireChanges<HK> expireAt(K key, Instant instant, Collection<HK> hashKeys) {
return execute(connection -> connection.hpExpireAt(rawKey, instant.toEpochMilli(), rawHashKeys));
List<HK> orderedKeys = List.copyOf(hashKeys);
byte[] rawKey = rawKey(key);
byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray());
Long millis = instant.toEpochMilli();
List<Long> raw = execute(connection -> TimeoutUtils.containsSplitSecond(millis)
? connection.hashCommands().hpExpireAt(rawKey, millis, rawHashKeys)
: connection.hashCommands().hExpireAt(rawKey, instant.getEpochSecond(), rawHashKeys));
return raw != null ? ExpireChanges.of(orderedKeys, raw) : null;
}
@Override
public List<Long> persist(K key, Collection<HK> hashKeys) {
byte[] rawKey = rawKey(key);
byte[][] rawHashKeys = rawHashKeys(hashKeys.toArray());
public ExpireChanges<HK> expire(K key, Expiration expiration, FieldExpirationOptions options, Collection<HK> hashKeys) {
return execute(connection -> connection.hPersist(rawKey, rawHashKeys));
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));
return raw != null ? ExpireChanges.of(orderedKeys, raw) : null;
}
@Override
public List<Long> getExpire(K key, Collection<HK> hashKeys) {
byte[] rawKey = rawKey(key);
byte[][] rawHashKeys = rawHashKeys(hashKeys.toArray());
public ExpireChanges<HK> persist(K key, Collection<HK> hashKeys) {
return execute(connection -> connection.hTtl(rawKey, rawHashKeys));
List<HK> orderedKeys = List.copyOf(hashKeys);
byte[] rawKey = rawKey(key);
byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray());
List<Long> raw = execute(connection -> connection.hashCommands().hPersist(rawKey, rawHashKeys));
return raw != null ? ExpireChanges.of(orderedKeys, raw) : null;
}
@Override
public List<Long> getExpire(K key, TimeUnit timeUnit, Collection<HK> hashKeys) {
byte[] rawKey = rawKey(key);
byte[][] rawHashKeys = rawHashKeys(hashKeys.toArray());
public Expirations<HK> getExpire(K key, TimeUnit timeUnit, Collection<HK> hashKeys) {
return execute(connection -> connection.hTtl(rawKey, timeUnit, rawHashKeys));
if(timeUnit.compareTo(TimeUnit.MILLISECONDS) < 0) {
throw new IllegalArgumentException("%s precision is not supported must be >= MILLISECONDS".formatted(timeUnit));
}
List<HK> orderedKeys = List.copyOf(hashKeys);
byte[] rawKey = rawKey(key);
byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray());
List<Long> raw = execute(
connection -> TimeUnit.MILLISECONDS.equals(timeUnit) ? connection.hashCommands().hpTtl(rawKey, rawHashKeys)
: connection.hashCommands().hTtl(rawKey, timeUnit, rawHashKeys));
if (raw == null) {
return null;
}
Timeouts timeouts = new Timeouts(TimeUnit.MILLISECONDS.equals(timeUnit) ? timeUnit : TimeUnit.SECONDS, raw);
return Expirations.of(timeUnit, orderedKeys, timeouts);
}
@Override

View File

@@ -15,20 +15,28 @@
*/
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;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
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.ReactiveHashCommands;
import org.springframework.data.redis.connection.convert.Converters;
import org.springframework.data.redis.core.Expirations.Timeouts;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -63,8 +71,7 @@ class DefaultReactiveHashOperations<H, HK, HV> implements ReactiveHashOperations
Assert.noNullElements(hashKeys, "Hash keys must not contain null elements");
return createMono(hashCommands -> Flux.fromArray(hashKeys) //
.map(hashKey -> (HK) hashKey)
.map(this::rawHashKey) //
.map(hashKey -> (HK) hashKey).map(this::rawHashKey) //
.collectList() //
.flatMap(hks -> hashCommands.hDel(rawKey(key), hks)));
}
@@ -86,8 +93,8 @@ class DefaultReactiveHashOperations<H, HK, HV> implements ReactiveHashOperations
Assert.notNull(key, "Key must not be null");
Assert.notNull(hashKey, "Hash key must not be null");
return createMono(hashCommands -> hashCommands.hGet(rawKey(key), rawHashKey((HK) hashKey))
.map(this::readHashValue));
return createMono(
hashCommands -> hashCommands.hGet(rawKey(key), rawHashKey((HK) hashKey)).map(this::readHashValue));
}
@Override
@@ -109,8 +116,8 @@ class DefaultReactiveHashOperations<H, HK, HV> implements ReactiveHashOperations
Assert.notNull(key, "Key must not be null");
Assert.notNull(hashKey, "Hash key must not be null");
return template.doCreateMono(connection -> connection.numberCommands()
.hIncrBy(rawKey(key), rawHashKey(hashKey), delta));
return template
.doCreateMono(connection -> connection.numberCommands().hIncrBy(rawKey(key), rawHashKey(hashKey), delta));
}
@Override
@@ -119,8 +126,8 @@ class DefaultReactiveHashOperations<H, HK, HV> implements ReactiveHashOperations
Assert.notNull(key, "Key must not be null");
Assert.notNull(hashKey, "Hash key must not be null");
return template.doCreateMono(connection -> connection.numberCommands()
.hIncrBy(rawKey(key), rawHashKey(hashKey), delta));
return template
.doCreateMono(connection -> connection.numberCommands().hIncrBy(rawKey(key), rawHashKey(hashKey), delta));
}
@Override
@@ -137,8 +144,7 @@ class DefaultReactiveHashOperations<H, HK, HV> implements ReactiveHashOperations
Assert.notNull(key, "Key must not be null");
return createMono(hashCommands -> hashCommands.hRandFieldWithValues(rawKey(key)))
.map(this::deserializeHashEntry);
return createMono(hashCommands -> hashCommands.hRandFieldWithValues(rawKey(key))).map(this::deserializeHashEntry);
}
@Override
@@ -235,6 +241,78 @@ class DefaultReactiveHashOperations<H, HK, HV> implements ReactiveHashOperations
.map(this::deserializeHashEntry));
}
@Override
public Mono<ExpireChanges<HK>> expire(H key, Duration timeout, Collection<HK> hashKeys) {
return expire(key, Expiration.from(timeout), FieldExpirationOptions.none(), hashKeys);
}
@Override
public Mono<ExpireChanges<HK>> expire(H key, Expiration expiration, FieldExpirationOptions options, Collection<HK> hashKeys) {
List<HK> orderedKeys = List.copyOf(hashKeys);
ByteBuffer rawKey = rawKey(key);
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);
}).collectList();
return raw.map(values -> ExpireChanges.of(orderedKeys, values));
}
@Nullable
@Override
public Mono<ExpireChanges<HK>> expireAt(H key, Instant expireAt, Collection<HK> hashKeys) {
List<HK> orderedKeys = List.copyOf(hashKeys);
ByteBuffer rawKey = rawKey(key);
List<ByteBuffer> rawHashKeys = orderedKeys.stream().map(this::rawHashKey).toList();
Mono<List<Long>> raw = createFlux(connection -> connection.hExpireAt(rawKey, expireAt, rawHashKeys)).collectList();
return raw.map(values -> ExpireChanges.of(orderedKeys, values));
}
@Nullable
@Override
public Mono<ExpireChanges<HK>> persist(H key, Collection<HK> hashKeys) {
List<HK> orderedKeys = List.copyOf(hashKeys);
ByteBuffer rawKey = rawKey(key);
List<ByteBuffer> rawHashKeys = orderedKeys.stream().map(this::rawHashKey).toList();
Mono<List<Long>> raw = createFlux(connection -> connection.hPersist(rawKey, rawHashKeys)).collectList();
return raw.map(values -> ExpireChanges.of(orderedKeys, values));
}
@Nullable
@Override
public Mono<Expirations<HK>> getExpire(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));
}
List<HK> orderedKeys = List.copyOf(hashKeys);
ByteBuffer rawKey = rawKey(key);
List<ByteBuffer> rawHashKeys = orderedKeys.stream().map(this::rawHashKey).toList();
Mono<List<Long>> raw = createFlux(connection -> {
if (TimeUnit.MILLISECONDS.equals(timeUnit)) {
return connection.hpTtl(rawKey, rawHashKeys);
}
return connection.hTtl(rawKey, rawHashKeys);
}).collectList();
return raw.map(values -> {
Timeouts timeouts = new Timeouts(TimeUnit.MILLISECONDS.equals(timeUnit) ? timeUnit : TimeUnit.SECONDS, values);
return Expirations.of(timeUnit, orderedKeys, timeouts);
});
}
@Override
public Mono<Boolean> delete(H key) {

View File

@@ -0,0 +1,303 @@
/*
* 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.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.springframework.lang.Nullable;
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.
* 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 #expiring()} returns the expiring keys along with their {@link Duration time to live}</li>
* </ol>
*
* @author Christoph Strobl
* @since 3.5
*/
public class Expirations<K> { // TODO: should we move this to let's say Hash.class or another place
private final TimeUnit unit;
private final Map<K, Expiration> expirations;
Expirations(TimeUnit unit, Map<K, Expiration> 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}.
* @param timeouts the raw Redis time to live values.
* @return new instance of {@link Expirations}.
* @param <K> the key type used
*/
public static <K> Expirations<K> of(TimeUnit targetUnit, List<K> keys, Timeouts timeouts) {
if (keys.size() != timeouts.size()) {
throw new IllegalArgumentException(
"Keys and Timeouts must be of same size but was %s vs %s".formatted(keys.size(), timeouts.size()));
}
if (keys.size() == 1) {
return new Expirations<>(targetUnit,
Map.of(keys.iterator().next(), Expiration.of(timeouts.raw().iterator().next(), timeouts.timeUnit())));
}
Map<K, Expiration> 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()));
}
return new Expirations<>(targetUnit, target);
}
/**
* @return an ordered set of keys that do not have a time to live.
*/
public Set<K> persistent() {
return filterByState(Expiration.PERSISTENT);
}
/**
* @return an ordered set of keys that do not exists and therefore do not have a time to live.
*/
public Set<K> missing() {
return filterByState(Expiration.MISSING);
}
/**
* @return an ordered set of all {@link Expirations expirations} where the {@link Expiration#value()} is using the
* {@link TimeUnit} defined in {@link #precision()}.
*/
public List<Expiration> expirations() {
return expirations.values().stream().map(it -> it.convert(this.unit)).toList();
}
/**
* @return the {@link TimeUnit} for {@link Expiration expirations} held by this instance.
*/
public TimeUnit precision() {
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
* 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();
}
/**
* @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.
*/
@Nullable
public Expiration expirationOf(K key) {
Expiration expiration = expirations.get(key);
if (expiration == null) {
return null;
}
return expiration.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}
* otherwise.
*/
@Nullable
public Duration ttlOf(K key) {
Expiration expiration = expirationOf(key);
if (expiration == null) {
return null;
}
return toDuration(expiration);
}
private Set<K> filterByState(Expiration 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) {
if (expiration.sourceUnit == null) {
return null;
}
return Duration.of(expiration.raw(), expiration.sourceUnit.toChronoUnit());
}
public record Timeouts(TimeUnit timeUnit, List<Long> raw) {
Long get(int index) {
return raw.get(index);
}
public int size() {
return raw.size();
}
}
/**
* Expiration holds time to live {@link #raw()} values as returned by a Redis Client. {@link #value()} serves the
* actual timeout in the given temporal context converting the {@link #raw()} value into a target {@link TimeUnit}.
* Dedicated methods such as {@link #isPersistent()} allow interpretation of the raw result. {@link #MISSING} and
* {@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?
private final long raw;
@Nullable TimeUnit sourceUnit;
@Nullable TimeUnit targetUnit;
public Expiration(long value) {
this(value, null);
}
public Expiration(long value, @Nullable TimeUnit sourceUnit) {
this(value, sourceUnit, null);
}
public Expiration(long value, @Nullable TimeUnit sourceUnit, @Nullable TimeUnit targetUnit) {
this.raw = value;
this.sourceUnit = sourceUnit;
this.targetUnit = targetUnit;
}
/**
* The raw source value as returned by the Redis Client.
*
* @return the raw data
*/
public long raw() {
return raw;
}
/**
* @return the {@link #raw()} value converted into the {@link #convert(TimeUnit) requested} target {@link TimeUnit}.
*/
public long value() {
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()
* value}, or the same instance raw value cannot or must not be converted.
*/
public Expiration convert(TimeUnit timeUnit) {
if (sourceUnit == null || ObjectUtils.nullSafeEquals(sourceUnit, timeUnit)) {
return this;
}
return new Expiration(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 {@literal true} if key does not exists 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);
};
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Expiration that)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(this.sourceUnit, that.sourceUnit)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(this.targetUnit, that.targetUnit)) {
return false;
}
return this.raw == that.raw;
}
@Override
public int hashCode() {
return Objects.hash(raw);
}
}
}

View File

@@ -0,0 +1,195 @@
/*
* 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.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.springframework.util.CollectionUtils;
/**
* Value Object linking a number of keys to their {@link ExpiryChangeState} retaining the order of the original source.
* Dedicated higher level methods interpret raw values retrieved from a Redis Client.
* <ol>
* <li>{@link #ok()} returns keys for which the time to live has been set</li>
* <li>{@link #expired()} returns keys that have been expired</li>
* <li>{@link #missed()} returns keys for which the time to live could not be set because they do not exist</li>
* <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
*/
public class ExpireChanges<K> {
private final Map<K, ExpiryChangeState> changes;
ExpireChanges(Map<K, ExpiryChangeState> changes) {
this.changes = changes;
}
/**
* 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
* {@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) {
if (keys.size() == 1) {
return new ExpireChanges<>(Map.of(keys.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)));
}
return new ExpireChanges<>(target);
}
/**
* @return an ordered {@link List} of the status changes.
*/
public List<ExpiryChangeState> stateChanges() {
return List.copyOf(changes.values());
}
/**
* @return the status change for the given {@literal key}, or {@literal null} if {@link ExpiryChangeState} does not
* contain an entry for it.
*/
public ExpiryChangeState stateOf(K key) {
return changes.get(key);
}
/**
* @return {@literal true} if all changes are {@link ExpiryChangeState#OK}.
*/
public boolean allOk() {
return allMach(ExpiryChangeState.OK::equals);
}
/**
* @return {@literal true} if all changes are either ok {@link ExpiryChangeState#OK} or
* {@link ExpiryChangeState#EXPIRED}.
*/
public boolean allChanged() {
return allMach(it -> ExpiryChangeState.OK.equals(it) || ExpiryChangeState.EXPIRED.equals(it));
}
/**
* @return an ordered list of if all changes are {@link ExpiryChangeState#OK}.
*/
public Set<K> ok() {
return filterByState(ExpiryChangeState.OK);
}
/**
* @return an ordered list of if all changes are {@link ExpiryChangeState#EXPIRED}.
*/
public Set<K> expired() {
return filterByState(ExpiryChangeState.EXPIRED);
}
/**
* @return an ordered list of if all changes are {@link ExpiryChangeState#DOES_NOT_EXIST}.
*/
public Set<K> missed() {
return filterByState(ExpiryChangeState.DOES_NOT_EXIST);
}
/**
* @return an ordered list of if all changes are {@link ExpiryChangeState#CONDITION_NOT_MET}.
*/
public Set<K> skipped() {
return filterByState(ExpiryChangeState.CONDITION_NOT_MET);
}
public boolean allMach(Predicate<ExpiryChangeState> predicate) {
return changes.values().stream().allMatch(predicate);
}
private Set<K> filterByState(ExpiryChangeState filter) {
return changes.entrySet().stream().filter(entry -> entry.getValue().equals(filter)).map(Map.Entry::getKey)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
private static ExpiryChangeState stateFromValue(Number value) {
return ExpiryChangeState.of(value);
}
public record ExpiryChangeState(long value) {
public static final ExpiryChangeState DOES_NOT_EXIST = new ExpiryChangeState(-2L);
public static final ExpiryChangeState CONDITION_NOT_MET = new ExpiryChangeState(0L);
public static final ExpiryChangeState OK = new ExpiryChangeState(1L);
public static final ExpiryChangeState EXPIRED = new ExpiryChangeState(2L);
static ExpiryChangeState of(Number value) {
return switch (value.intValue()) {
case -2 -> DOES_NOT_EXIST;
case 0 -> CONDITION_NOT_MET;
case 1 -> OK;
case 2 -> EXPIRED;
default -> new ExpiryChangeState(value.longValue());
};
}
public boolean isOk() {
return OK.equals(this);
}
public boolean isExpired() {
return EXPIRED.equals(this);
}
public boolean isMissing() {
return DOES_NOT_EXIST.equals(this);
}
public boolean isSkipped() {
return CONDITION_NOT_MET.equals(this);
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof ExpiryChangeState that)) {
return false;
}
return this.value == that.value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}
}

View File

@@ -23,6 +23,8 @@ 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;
/**
@@ -240,7 +242,7 @@ public interface HashOperations<H, HK, HV> {
* @since 3.5
*/
@Nullable
List<Long> expire(H key, Duration timeout, Collection<HK> hashKeys);
ExpireChanges<HK> expire(H key, Duration timeout, Collection<HK> hashKeys);
/**
* Set the expiration for given {@code hashKey} (aka field) as a {@literal date} timestamp.
@@ -257,7 +259,9 @@ public interface HashOperations<H, HK, HV> {
* @since 3.5
*/
@Nullable
List<Long> expireAt(H key, Instant expireAt, Collection<HK> hashKeys);
ExpireChanges<HK> expireAt(H key, Instant expireAt, Collection<HK> hashKeys);
ExpireChanges<HK> expire(H key, Expiration expiration, FieldExpirationOptions options, Collection<HK> hashKeys);
/**
* Remove the expiration from given {@code hashKey} (aka field).
@@ -271,7 +275,7 @@ public interface HashOperations<H, HK, HV> {
* @since 3.5
*/
@Nullable
List<Long> persist(H key, Collection<HK> hashKeys);
ExpireChanges<HK> persist(H key, Collection<HK> hashKeys);
/**
* Get the time to live for {@code hashKey} (aka field) in seconds.
@@ -285,7 +289,9 @@ public interface HashOperations<H, HK, HV> {
* @since 3.5
*/
@Nullable
List<Long> getExpire(H key, Collection<HK> hashKeys);
default Expirations<HK> getExpire(H key, Collection<HK> hashKeys) {
return getExpire(key, TimeUnit.SECONDS, hashKeys);
}
/**
* Get the time to live for {@code hashKey} (aka field) and convert it to the given {@link TimeUnit}.
@@ -300,7 +306,8 @@ public interface HashOperations<H, HK, HV> {
* @since 3.5
*/
@Nullable
List<Long> getExpire(H key, TimeUnit timeUnit, Collection<HK> hashKeys);
Expirations<HK> getExpire(H key, TimeUnit timeUnit, Collection<HK> hashKeys);
/**
* @return never {@literal null}.
*/

View File

@@ -15,13 +15,20 @@
*/
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;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.springframework.lang.Nullable;
/**
* Reactive Redis operations for Hash Commands.
@@ -230,10 +237,80 @@ public interface ReactiveHashOperations<H, HK, HV> {
*/
Flux<Map.Entry<HK, HV>> scan(H key, ScanOptions options);
Mono<ExpireChanges<HK>> expire(H key, Duration timeout, Collection<HK> hashKeys);
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.
*
* @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.
* @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
Mono<ExpireChanges<HK>> expireAt(H key, Instant expireAt, Collection<HK> hashKeys);
/**
* Remove the expiration from given {@code hashKey} (aka field).
*
* @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.
* @see <a href="https://redis.io/docs/latest/commands/hpersist/">Redis Documentation: HPERSIST</a>
* @since 3.5
*/
@Nullable
Mono<ExpireChanges<HK>> persist(H key, Collection<HK> hashKeys);
/**
* Get the time to live for {@code hashKey} (aka field) 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.
* @see <a href="https://redis.io/docs/latest/commands/httl/">Redis Documentation: HTTL</a>
* @since 3.5
*/
@Nullable
default Mono<Expirations<HK>> getExpire(H key, Collection<HK> hashKeys) {
return getExpire(key, TimeUnit.SECONDS, hashKeys);
}
/**
* Get the time to live for {@code hashKey} (aka field) 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.
* @see <a href="https://redis.io/docs/latest/commands/httl/">Redis Documentation: HTTL</a>
* @since 3.5
*/
@Nullable
Mono<Expirations<HK>> getExpire(H key, TimeUnit timeUnit, Collection<HK> hashKeys);
/**
* Removes the given {@literal key}.
*
* @param key must not be {@literal null}.
*/
Mono<Boolean> delete(H key);
}

View File

@@ -35,7 +35,11 @@ public abstract class TimeoutUtils {
* @since 2.1
*/
public static boolean hasMillis(Duration duration) {
return duration.toMillis() % 1000 != 0;
return containsSplitSecond(duration.toMillis());
}
public static boolean containsSplitSecond(long millis) {
return millis % 1000 != 0;
}
/**

View File

@@ -19,6 +19,7 @@ import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.springframework.data.redis.core.TimeoutUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -105,8 +106,8 @@ public class Expiration {
Assert.notNull(duration, "Duration must not be null");
return duration.isZero() ? Expiration.persistent()
: duration.toMillis() % 1000 == 0 ? new Expiration(duration.getSeconds(), TimeUnit.SECONDS)
: new Expiration(duration.toMillis(), TimeUnit.MILLISECONDS);
: TimeoutUtils.hasMillis(duration) ? new Expiration(duration.toMillis(), TimeUnit.MILLISECONDS)
: new Expiration(duration.getSeconds(), TimeUnit.SECONDS);
}
/**

View File

@@ -20,13 +20,14 @@ import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
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.BoundHashOperations;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisOperations;
@@ -327,27 +328,27 @@ public class DefaultRedisMap<K, V> implements RedisMap<K, V> {
}
@Override
public List<Long> expire(Duration timeout, Collection<K> hashKeys) {
public ExpireChanges<K> expire(Duration timeout, Collection<K> hashKeys) {
return Objects.requireNonNull(hashOps.expire(timeout, hashKeys));
}
@Override
public List<Long> expireAt(Instant expireAt, Collection<K> hashKeys) {
public ExpireChanges<K> expireAt(Instant expireAt, Collection<K> hashKeys) {
return Objects.requireNonNull(hashOps.expireAt(expireAt, hashKeys));
}
@Override
public List<Long> persist(Collection<K> hashKeys) {
public ExpireChanges<K> persist(Collection<K> hashKeys) {
return Objects.requireNonNull(hashOps.persist(hashKeys));
}
@Override
public List<Long> getExpire(Collection<K> hashKeys) {
public Expirations<K> getExpire(Collection<K> hashKeys) {
return Objects.requireNonNull(hashOps.getExpire(hashKeys));
}
@Override
public List<Long> getExpire(TimeUnit timeUnit, Collection<K> hashKeys) {
public Expirations<K> getExpire(TimeUnit timeUnit, Collection<K> hashKeys) {
return Objects.requireNonNull(hashOps.getExpire(timeUnit, hashKeys));
}

View File

@@ -19,11 +19,12 @@ import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
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.lang.Nullable;
/**
@@ -91,7 +92,7 @@ public interface RedisMap<K, V> extends RedisStore, ConcurrentMap<K, V> {
* @see <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
List<Long> expire(Duration timeout, Collection<K> hashKeys);
ExpireChanges<K> expire(Duration timeout, Collection<K> hashKeys);
/**
* Set the expiration for given hash {@code key} as a {@literal date} timestamp.
@@ -106,7 +107,7 @@ public interface RedisMap<K, V> extends RedisStore, ConcurrentMap<K, V> {
* @see <a href="https://redis.io/docs/latest/commands/hexpireat/">Redis Documentation: HEXPIRE</a>
* @since 3.5
*/
List<Long> expireAt(Instant expireAt, Collection<K> hashKeys);
ExpireChanges<K> expireAt(Instant expireAt, Collection<K> hashKeys);
/**
* Remove the expiration from given hash {@code key}.
@@ -118,7 +119,7 @@ public interface RedisMap<K, V> extends RedisStore, ConcurrentMap<K, V> {
* @see <a href="https://redis.io/docs/latest/commands/hpersist/">Redis Documentation: HPERSIST</a>
* @since 3.5
*/
List<Long> persist(Collection<K> hashKeys);
ExpireChanges<K> persist(Collection<K> hashKeys);
/**
* Get the time to live for hash {@code key} in seconds.
@@ -130,7 +131,7 @@ public interface RedisMap<K, V> extends RedisStore, ConcurrentMap<K, V> {
* @see <a href="https://redis.io/docs/latest/commands/httl/">Redis Documentation: HTTL</a>
* @since 3.5
*/
List<Long> getExpire(Collection<K> hashKeys);
Expirations<K> getExpire(Collection<K> hashKeys);
/**
* Get the time to live for hash {@code key} and convert it to the given {@link TimeUnit}.
@@ -143,5 +144,5 @@ public interface RedisMap<K, V> extends RedisStore, ConcurrentMap<K, V> {
* @see <a href="https://redis.io/docs/latest/commands/httl/">Redis Documentation: HTTL</a>
* @since 3.5
*/
List<Long> getExpire(TimeUnit timeUnit, Collection<K> hashKeys);
Expirations<K> getExpire(TimeUnit timeUnit, Collection<K> hashKeys);
}

View File

@@ -24,6 +24,8 @@ import java.util.Map.Entry;
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.BoundHashOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.lang.Nullable;
@@ -300,33 +302,38 @@ public class RedisProperties extends Properties implements RedisMap<Object, Obje
}
@Override
public List<Long> expire(Duration timeout, Collection<Object> hashKeys) {
public ExpireChanges<Object> expire(Duration timeout, Collection<Object> hashKeys) {
Collection<String> keys = hashKeys.stream().map(key -> (String) key).toList();
return Objects.requireNonNull(hashOps.expire(timeout, keys));
return (ExpireChanges) hashOps.expire(timeout, keys);
}
@Override
public List<Long> expireAt(Instant expireAt, Collection<Object> hashKeys) {
public ExpireChanges<Object> expireAt(Instant expireAt, Collection<Object> hashKeys) {
Collection<String> keys = hashKeys.stream().map(key -> (String) key).toList();
return Objects.requireNonNull(hashOps.expireAt(expireAt, keys));
return (ExpireChanges) hashOps.expireAt(expireAt, keys);
}
@Override
public List<Long> persist(Collection<Object> hashKeys) {
public ExpireChanges<Object> persist(Collection<Object> hashKeys) {
Collection<String> keys = hashKeys.stream().map(key -> (String) key).toList();
return Objects.requireNonNull(hashOps.persist(keys));
return (ExpireChanges) hashOps.persist(keys);
}
@Override
public List<Long> getExpire(Collection<Object> hashKeys) {
public Expirations<Object> getExpire(Collection<Object> hashKeys) {
Collection<String> keys = hashKeys.stream().map(key -> (String) key).toList();
return Objects.requireNonNull(hashOps.getExpire(keys));
return (Expirations) hashOps.getExpire(keys);
}
@Override
public List<Long> getExpire(TimeUnit timeUnit, Collection<Object> hashKeys) {
public Expirations<Object> getExpire(TimeUnit timeUnit, Collection<Object> hashKeys) {
Collection<String> keys = hashKeys.stream().map(key -> (String) key).toList();
return Objects.requireNonNull(hashOps.getExpire(timeUnit, keys));
return (Expirations) hashOps.getExpire(timeUnit, keys);
}
}

View File

@@ -3436,6 +3436,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"));
@@ -3449,6 +3450,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireReturnsMinusTwoWhenFieldDoesNotExist() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
actual.add(connection.hExpire("hash-hexpire", 5L, "missking-field"));
actual.add(connection.hExpire("missing-key", 5L, "key-2"));
@@ -3459,6 +3461,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireReturnsTwoWhenZeroProvided() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
actual.add(connection.hExpire("hash-hexpire", 0, "key-2"));
@@ -3468,9 +3471,10 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HPEXPIRE")
public void hpExpireReturnsSuccessAndSetsTTL() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
actual.add(connection.hpExpire("hash-hexpire", 5000L, "key-2"));
actual.add(connection.hTtl("hash-hexpire", TimeUnit.MILLISECONDS,"key-2"));
actual.add(connection.hpTtl("hash-hexpire", "key-2"));
List<Object> results = getResults();
assertThat(results.get(0)).isEqualTo(Boolean.TRUE);
@@ -3481,6 +3485,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HPEXPIRE")
public void hpExpireReturnsMinusTwoWhenFieldDoesNotExist() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
actual.add(connection.hpExpire("hash-hexpire", 5L, "missing-field"));
actual.add(connection.hpExpire("missing-key", 5L, "key-2"));
@@ -3491,6 +3496,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HPEXPIRE")
public void hpExpireReturnsTwoWhenZeroProvided() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
actual.add(connection.hpExpire("hash-hexpire", 0, "key-2"));
@@ -3500,6 +3506,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HEXPIREAT")
public void hExpireAtReturnsSuccessAndSetsTTL() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
long inFiveSeconds = Instant.now().plusSeconds(5L).getEpochSecond();
@@ -3515,6 +3522,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HEXPIREAT")
public void hExpireAtReturnsMinusTwoWhenFieldDoesNotExist() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
long inFiveSeconds = Instant.now().plusSeconds(5L).getEpochSecond();
@@ -3527,6 +3535,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HEXPIREAT")
public void hExpireAtReturnsTwoWhenZeroProvided() {
long fiveSecondsAgo = Instant.now().minusSeconds(5L).getEpochSecond();
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
@@ -3538,6 +3547,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HEXPIREAT")
public void hpExpireAtReturnsSuccessAndSetsTTL() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
long inFiveSeconds = Instant.now().plusSeconds(5L).toEpochMilli();
@@ -3553,6 +3563,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HEXPIREAT")
public void hpExpireAtReturnsMinusTwoWhenFieldDoesNotExist() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
long inFiveSeconds = Instant.now().plusSeconds(5L).toEpochMilli();
@@ -3565,6 +3576,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HPEXPIREAT")
public void hpExpireAdReturnsTwoWhenZeroProvided() {
long fiveSecondsAgo = Instant.now().minusSeconds(5L).getEpochSecond();
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
@@ -3576,6 +3588,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HPERSIST")
public void hPersistReturnsSuccessAndPersistsField() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
actual.add(connection.hExpire("hash-hexpire", 5L, "key-2"));
actual.add(connection.hPersist("hash-hexpire", "key-2"));
@@ -3587,6 +3600,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HPERSIST")
public void hPersistReturnsMinusOneWhenFieldDoesNotHaveExpiration() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
actual.add(connection.hPersist("hash-hexpire", "key-2"));
@@ -3596,6 +3610,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HPERSIST")
public void hPersistReturnsMinusTwoWhenFieldOrKeyMissing() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
actual.add(connection.hPersist("hash-hexpire", "missing-field"));
actual.add(connection.hPersist("missing-key", "key-2"));
@@ -3606,15 +3621,27 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
@EnabledOnCommand("HTTL")
public void hTtlReturnsMinusOneWhenFieldHasNoExpiration() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
actual.add(connection.hTtl("hash-hexpire", "key-2"));
verifyResults(Arrays.asList(Boolean.TRUE, List.of(-1L)));
}
@Test
@EnabledOnCommand("HTTL")
public void hTtlReturnsMinusIndependendOfTimeUnitOneWhenFieldHasNoExpiration() {
actual.add(connection.hSet("hash-hexpire", "key-2", "value-2"));
actual.add(connection.hTtl("hash-hexpire", TimeUnit.HOURS, "key-2"));
verifyResults(Arrays.asList(Boolean.TRUE, List.of(-1L)));
}
@Test
@EnabledOnCommand("HTTL")
public void hTtlReturnsMinusTwoWhenFieldOrKeyMissing() {
actual.add(connection.hTtl("hash-hexpire", "missing-field"));
actual.add(connection.hTtl("missing-key", "key-2"));

View File

@@ -1041,6 +1041,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireReturnsSuccessAndSetsTTL() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
@@ -1050,6 +1051,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireReturnsMinusTwoWhenFieldDoesNotExist() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
// missing field
@@ -1059,6 +1061,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireReturnsTwoWhenZeroProvided() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
@@ -1066,6 +1069,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hpExpireReturnsSuccessAndSetsTTL() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
@@ -1075,6 +1079,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hpExpireReturnsMinusTwoWhenFieldDoesNotExist() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
// missing field
@@ -1084,6 +1089,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hpExpireReturnsTwoWhenZeroProvided() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
@@ -1091,6 +1097,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireAtReturnsSuccessAndSetsTTL() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
long inFiveSeconds = Instant.now().plusSeconds(5L).getEpochSecond();
@@ -1099,6 +1106,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireAtReturnsMinusTwoWhenFieldDoesNotExist() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
long inFiveSeconds = Instant.now().plusSeconds(5L).getEpochSecond();
@@ -1110,6 +1118,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireAdReturnsTwoWhenZeroProvided() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
@@ -1117,6 +1126,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hpExpireAtReturnsSuccessAndSetsTTL() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
long inFiveSeconds = Instant.now().plusSeconds(5L).toEpochMilli();
@@ -1126,6 +1136,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hpExpireAtReturnsMinusTwoWhenFieldDoesNotExist() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
long inFiveSeconds = Instant.now().plusSeconds(5L).toEpochMilli();
@@ -1137,6 +1148,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hpExpireAdReturnsTwoWhenZeroProvided() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
@@ -1144,6 +1156,7 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hPersistReturnsSuccessAndPersistsField() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
assertThat(clusterConnection.hashCommands().hExpire(KEY_1_BYTES, 5L, KEY_2_BYTES)).contains(1L);
@@ -1152,12 +1165,14 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hPersistReturnsMinusOneWhenFieldDoesNotHaveExpiration() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
assertThat(clusterConnection.hashCommands().hPersist(KEY_1_BYTES, KEY_2_BYTES)).contains(-1L);
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hPersistReturnsMinusTwoWhenFieldOrKeyMissing() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
@@ -1167,14 +1182,15 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hTtlReturnsMinusOneWhenFieldHasNoExpiration() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, KEY_2_BYTES)).contains(-1L);
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hTtlReturnsMinusTwoWhenFieldOrKeyMissing() {
assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, KEY_1_BYTES)).contains(-2L);

View File

@@ -1098,7 +1098,9 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireReturnsSuccessAndSetsTTL() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
assertThat(clusterConnection.hashCommands().hExpire(KEY_1_BYTES, 5L, KEY_2_BYTES)).contains(1L);
@@ -1106,7 +1108,9 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireReturnsMinusTwoWhenFieldDoesNotExist() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
// missing field
assertThat(clusterConnection.hashCommands().hExpire(KEY_1_BYTES, 5L, KEY_1_BYTES)).contains(-2L);
@@ -1115,6 +1119,7 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireReturnsTwoWhenZeroProvided() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
@@ -1122,16 +1127,19 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hpExpireReturnsSuccessAndSetsTTL() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
assertThat(clusterConnection.hashCommands().hpExpire(KEY_1_BYTES, 5000L, KEY_2_BYTES)).contains(1L);
assertThat(clusterConnection.hTtl(KEY_1_BYTES, TimeUnit.MILLISECONDS,KEY_2_BYTES))
assertThat(clusterConnection.hTtl(KEY_1_BYTES, TimeUnit.MILLISECONDS, KEY_2_BYTES))
.allSatisfy(val -> assertThat(val).isBetween(0L, 5000L));
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hpExpireReturnsMinusTwoWhenFieldDoesNotExist() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
// missing field
assertThat(clusterConnection.hashCommands().hpExpire(KEY_1_BYTES, 5L, KEY_1_BYTES)).contains(-2L);
@@ -1140,14 +1148,18 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hpExpireReturnsTwoWhenZeroProvided() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
assertThat(clusterConnection.hashCommands().hpExpire(KEY_1_BYTES, 0L, KEY_2_BYTES)).contains(2L);
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireAtReturnsSuccessAndSetsTTL() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
long inFiveSeconds = Instant.now().plusSeconds(5L).getEpochSecond();
assertThat(clusterConnection.hashCommands().hExpireAt(KEY_1_BYTES, inFiveSeconds, KEY_2_BYTES)).contains(1L);
@@ -1155,17 +1167,21 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireAtReturnsMinusTwoWhenFieldDoesNotExist() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
long inFiveSeconds = Instant.now().plusSeconds(5L).getEpochSecond();
// missing field
assertThat(clusterConnection.hashCommands().hExpireAt(KEY_1_BYTES, inFiveSeconds, KEY_1_BYTES)).contains(-2L);
// missing key
assertThat(clusterConnection.hashCommands().hExpireAt(KEY_2_BYTES, inFiveSeconds, KEY_2_BYTES)).contains(-2L);
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hExpireAdReturnsTwoWhenZeroProvided() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
@@ -1173,16 +1189,20 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hpExpireAtReturnsSuccessAndSetsTTL() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
long inFiveSeconds = Instant.now().plusSeconds(5L).toEpochMilli();
assertThat(clusterConnection.hashCommands().hpExpireAt(KEY_1_BYTES, inFiveSeconds, KEY_2_BYTES)).contains(1L);
assertThat(clusterConnection.hTtl(KEY_1_BYTES, TimeUnit.MILLISECONDS, KEY_2_BYTES))
.allSatisfy(val -> assertThat(val).isBetween(0L, 5000L));
assertThat(clusterConnection.hpTtl(KEY_1_BYTES, KEY_2_BYTES))
.allSatisfy(val -> assertThat(val).isGreaterThan(1000L).isLessThanOrEqualTo(5000L));
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hpExpireAtReturnsMinusTwoWhenFieldDoesNotExist() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
long inFiveSeconds = Instant.now().plusSeconds(5L).toEpochMilli();
@@ -1193,14 +1213,18 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hpExpireAdReturnsTwoWhenZeroProvided() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
assertThat(clusterConnection.hashCommands().hpExpireAt(KEY_1_BYTES, 0L, KEY_2_BYTES)).contains(2L);
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hPersistReturnsSuccessAndPersistsField() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
assertThat(clusterConnection.hashCommands().hExpire(KEY_1_BYTES, 5L, KEY_2_BYTES)).contains(1L);
assertThat(clusterConnection.hashCommands().hPersist(KEY_1_BYTES, KEY_2_BYTES)).contains(1L);
@@ -1208,12 +1232,15 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hPersistReturnsMinusOneWhenFieldDoesNotHaveExpiration() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
assertThat(clusterConnection.hashCommands().hPersist(KEY_1_BYTES, KEY_2_BYTES)).contains(-1L);
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hPersistReturnsMinusTwoWhenFieldOrKeyMissing() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
@@ -1223,19 +1250,21 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests {
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hTtlReturnsMinusOneWhenFieldHasNoExpiration() {
nativeConnection.hset(KEY_1, KEY_2, VALUE_3);
assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, KEY_2_BYTES)).contains(-1L);
assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, TimeUnit.HOURS, KEY_2_BYTES)).contains(-1L);
}
@Test
@EnabledOnCommand("HEXPIRE")
public void hTtlReturnsMinusTwoWhenFieldOrKeyMissing() {
assertThat(clusterConnection.hashCommands().hTtl(KEY_1_BYTES, KEY_1_BYTES)).contains(-2L);
assertThat(clusterConnection.hashCommands().hTtl(KEY_3_BYTES,KEY_2_BYTES)).contains(-2L);
}
@Test // DATAREDIS-315

View File

@@ -17,6 +17,7 @@ package org.springframework.data.redis.connection.lettuce;
import static org.assertj.core.api.Assertions.*;
import org.springframework.data.redis.test.condition.EnabledOnCommand;
import reactor.test.StepVerifier;
import java.nio.ByteBuffer;
@@ -293,6 +294,7 @@ public class LettuceReactiveHashCommandsIntegrationTests extends LettuceReactive
}
@ParameterizedRedisTest
@EnabledOnCommand("HEXPIRE")
void hExpireShouldHandleMultipleParametersCorrectly() {
assertThat(nativeCommands.hset(KEY_1, FIELD_1, VALUE_1)).isTrue();
assertThat(nativeCommands.hset(KEY_1, FIELD_2, VALUE_2)).isTrue();
@@ -312,6 +314,7 @@ public class LettuceReactiveHashCommandsIntegrationTests extends LettuceReactive
}
@ParameterizedRedisTest
@EnabledOnCommand("HEXPIRE")
void hExpireAtShouldHandleMultipleParametersCorrectly() {
assertThat(nativeCommands.hset(KEY_1, FIELD_1, VALUE_1)).isTrue();
assertThat(nativeCommands.hset(KEY_1, FIELD_2, VALUE_2)).isTrue();
@@ -330,6 +333,7 @@ public class LettuceReactiveHashCommandsIntegrationTests extends LettuceReactive
}
@ParameterizedRedisTest
@EnabledOnCommand("HEXPIRE")
void hPersistShouldPersistFields() {
assertThat(nativeCommands.hset(KEY_1, FIELD_1, VALUE_1)).isTrue();
assertThat(nativeCommands.hset(KEY_1, FIELD_2, VALUE_2)).isTrue();

View File

@@ -15,8 +15,9 @@
*/
package org.springframework.data.redis.core;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assumptions.*;
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 java.io.IOException;
import java.time.Duration;
@@ -27,13 +28,18 @@ import java.util.List;
import java.util.Map;
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.test.condition.EnabledOnCommand;
import org.springframework.data.redis.test.extension.RedisStanalone;
import org.springframework.data.redis.test.extension.parametrized.MethodSource;
import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest;
@@ -136,7 +142,6 @@ public class DefaultHashOperationsIntegrationTests<K, HK, HV> {
hashOps.put(key, key1, val1);
hashOps.put(key, key2, val2);
long count = 0;
try (Cursor<Map.Entry<HK, HV>> it = hashOps.scan(key, ScanOptions.scanOptions().count(1).build())) {
@@ -208,6 +213,7 @@ public class DefaultHashOperationsIntegrationTests<K, HK, HV> {
assertThat(values).hasSize(2).containsEntry(key1, val1).containsEntry(key2, val2);
}
@EnabledOnCommand("HEXPIRE")
@ParameterizedRedisTest
void testExpireAndGetExpireMillis() {
@@ -220,13 +226,20 @@ public class DefaultHashOperationsIntegrationTests<K, HK, HV> {
hashOps.put(key, key2, val2);
assertThat(redisTemplate.opsForHash().expire(key, Duration.ofMillis(500), List.of(key1)))
.containsExactly(1L);
.satisfies(ExpireChanges::allOk);
assertThat(redisTemplate.opsForHash().getExpire(key, List.of(key1)))
.allSatisfy(it -> assertThat(it).isBetween(0L, 500L));
assertThat(redisTemplate.opsForHash().getExpire(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)
.isBetween(0L, 1L);
assertThat(expirations.ttlOf(key1)).isBetween(Duration.ZERO, Duration.ofSeconds(1));
});
}
@ParameterizedRedisTest
@EnabledOnCommand("HEXPIRE")
void testExpireAndGetExpireSeconds() {
K key = keyFactory.instance();
@@ -238,13 +251,26 @@ public class DefaultHashOperationsIntegrationTests<K, HK, HV> {
hashOps.put(key, key2, val2);
assertThat(redisTemplate.opsForHash().expire(key, Duration.ofSeconds(5), List.of(key1, key2)))
.containsExactly(1L, 1L);
.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(redisTemplate.opsForHash().getExpire(key, TimeUnit.SECONDS, List.of(key1, key2)))
.allSatisfy(it -> assertThat(it).isBetween(0L, 5L));
.satisfies(expirations -> {
assertThat(expirations.missing()).isEmpty();
assertThat(expirations.precision()).isEqualTo(TimeUnit.SECONDS);
assertThat(expirations.expirationOf(key1)).extracting(Expiration::raw, InstanceOfAssertFactories.LONG)
.isBetween(0L, 5L);
assertThat(expirations.ttlOf(key1)).isBetween(Duration.ofSeconds(1), Duration.ofSeconds(5));
});
}
@ParameterizedRedisTest
@EnabledOnCommand("HEXPIRE")
void testExpireAtAndGetExpireMillis() {
K key = keyFactory.instance();
@@ -256,13 +282,69 @@ public class DefaultHashOperationsIntegrationTests<K, HK, HV> {
hashOps.put(key, key2, val2);
assertThat(redisTemplate.opsForHash().expireAt(key, Instant.now().plusMillis(500), List.of(key1, key2)))
.containsExactly(1L, 1L);
.satisfies(ExpireChanges::allOk);
assertThat(redisTemplate.opsForHash().getExpire(key, List.of(key1, key2)))
.allSatisfy(it -> assertThat(it).isBetween(0L, 500L));
assertThat(redisTemplate.opsForHash().getExpire(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)
.isBetween(0L, 500L);
assertThat(expirations.ttlOf(key1)).isBetween(Duration.ZERO, Duration.ofMillis(500));
});
}
@ParameterizedRedisTest
void expireThrowsErrorOfNanoPrecision() {
K key = keyFactory.instance();
HK key1 = hashKeyFactory.instance();
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> redisTemplate.opsForHash().getExpire(key, TimeUnit.NANOSECONDS, List.of(key1)));
}
@ParameterizedRedisTest
void testExpireWithOptionsNone() {
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);
ExpireChanges<Object> expire = redisTemplate.opsForHash().expire(key, org.springframework.data.redis.core.types.Expiration.seconds(20), FieldExpirationOptions.none(), List.of(key1));
assertThat(expire.allOk()).isTrue();
}
@ParameterizedRedisTest
@EnabledOnCommand("HEXPIRE")
void testExpireWithOptions() {
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);
redisTemplate.opsForHash().expire(key, org.springframework.data.redis.core.types.Expiration.seconds(20), FieldExpirationOptions.none(), List.of(key1));
redisTemplate.opsForHash().expire(key, org.springframework.data.redis.core.types.Expiration.seconds(60), FieldExpirationOptions.none(), List.of(key2));
ExpireChanges<Object> changes = redisTemplate.opsForHash().expire(key, org.springframework.data.redis.core.types.Expiration.seconds(30), FieldExpirationOptions.builder().gt().build(), List.of(key1, key2));
assertThat(changes.ok()).containsExactly(key1);
assertThat(changes.skipped()).containsExactly(key2);
}
@ParameterizedRedisTest
@EnabledOnCommand("HEXPIRE")
void testPersistAndGetExpireMillis() {
K key = keyFactory.instance();
@@ -273,13 +355,14 @@ public class DefaultHashOperationsIntegrationTests<K, HK, HV> {
hashOps.put(key, key1, val1);
hashOps.put(key, key2, val2);
assertThat(redisTemplate.opsForHash().expireAt(key, Instant.now().plusMillis(500), List.of(key1, key2)))
.containsExactly(1L, 1L);
assertThat(redisTemplate.opsForHash().expireAt(key, Instant.now().plusMillis(800), List.of(key1, key2)))
.satisfies(ExpireChanges::allOk);
assertThat(redisTemplate.opsForHash().persist(key, List.of(key1, key2)))
.allSatisfy(it -> assertThat(it).isEqualTo(1L));
assertThat(redisTemplate.opsForHash().persist(key, List.of(key2))).satisfies(ExpireChanges::allOk);
assertThat(redisTemplate.opsForHash().getExpire(key, List.of(key1, key2)))
.allSatisfy(it -> assertThat(it).isEqualTo(-1L));
assertThat(redisTemplate.opsForHash().getExpire(key, List.of(key1, key2))).satisfies(expirations -> {
assertThat(expirations.expirationOf(key1).isPersistent()).isFalse();
assertThat(expirations.expirationOf(key2).isPersistent()).isTrue();
});
}
}

View File

@@ -15,28 +15,32 @@
*/
package org.springframework.data.redis.core;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assumptions.*;
import static org.junit.jupiter.api.condition.OS.*;
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 org.junit.jupiter.api.condition.DisabledOnOs;
import org.springframework.data.redis.connection.convert.Converters;
import org.springframework.data.redis.connection.Hash.FieldExpirationOptions;
import reactor.test.StepVerifier;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
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.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.convert.Converters;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@@ -501,6 +505,141 @@ public class DefaultReactiveHashOperationsIntegrationTests<K, HK, HV> {
.verifyComplete();
}
@EnabledOnCommand("HEXPIRE")
@ParameterizedRedisTest
void testExpireAndGetExpireMillis() {
K key = keyFactory.instance();
HK key1 = hashKeyFactory.instance();
HV val1 = hashValueFactory.instance();
HK key2 = hashKeyFactory.instance();
HV val2 = hashValueFactory.instance();
putAll(key, key1, val1, key2, val2);
hashOperations.expire(key, Duration.ofMillis(1500), List.of(key1)) //
.as(StepVerifier::create)//
.assertNext(changes -> {
assertThat(changes.allOk()).isTrue();
}).verifyComplete();
hashOperations.getExpire(key, List.of(key1)) //
.as(StepVerifier::create) //
.assertNext(it -> {
assertThat(it.expirationOf(key1).raw()).isBetween(0L, 2L);
}).verifyComplete();
}
@ParameterizedRedisTest
@EnabledOnCommand("HEXPIRE")
void testExpireWithOptions() {
K key = keyFactory.instance();
HK key1 = hashKeyFactory.instance();
HV val1 = hashValueFactory.instance();
HK key2 = hashKeyFactory.instance();
HV val2 = hashValueFactory.instance();
putAll(key, key1, val1, key2, val2);
hashOperations.expire(key, org.springframework.data.redis.core.types.Expiration.seconds(20), FieldExpirationOptions.none(), List.of(key1)).as(StepVerifier::create)//
.assertNext(changes -> {
assertThat(changes.allOk()).isTrue();
}).verifyComplete();
hashOperations.expire(key, org.springframework.data.redis.core.types.Expiration.seconds(60), FieldExpirationOptions.none(), List.of(key2)).as(StepVerifier::create)//
.assertNext(changes -> {
assertThat(changes.allOk()).isTrue();
}).verifyComplete();
hashOperations.expire(key, org.springframework.data.redis.core.types.Expiration.seconds(30), FieldExpirationOptions.builder().gt().build(), List.of(key1, key2)).as(StepVerifier::create)//
.assertNext(changes -> {
assertThat(changes.ok()).containsExactly(key1);
assertThat(changes.skipped()).containsExactly(key2);
}).verifyComplete();
}
@ParameterizedRedisTest
@EnabledOnCommand("HEXPIRE")
void testExpireAndGetExpireSeconds() {
K key = keyFactory.instance();
HK key1 = hashKeyFactory.instance();
HV val1 = hashValueFactory.instance();
HK key2 = hashKeyFactory.instance();
HV val2 = hashValueFactory.instance();
putAll(key, key1, val1, key2, val2);
hashOperations.expire(key, Duration.ofSeconds(5), List.of(key1, key2)) //
.as(StepVerifier::create)//
.assertNext(changes -> {
assertThat(changes.allOk()).isTrue();
}).verifyComplete();
hashOperations.getExpire(key, TimeUnit.SECONDS, List.of(key1, key2)) //
.as(StepVerifier::create) //
.assertNext(it -> {
assertThat(it.expirationOf(key1).raw()).isBetween(0L, 5L);
assertThat(it.expirationOf(key2).raw()).isBetween(0L, 5L);
}).verifyComplete();
}
@ParameterizedRedisTest
@EnabledOnCommand("HEXPIRE")
void testExpireAtAndGetExpireMillis() {
K key = keyFactory.instance();
HK key1 = hashKeyFactory.instance();
HV val1 = hashValueFactory.instance();
HK key2 = hashKeyFactory.instance();
HV val2 = hashValueFactory.instance();
putAll(key, key1, val1, key2, val2);
redisTemplate.opsForHash().expireAt(key, Instant.now().plusMillis(1500), List.of(key1, key2))
.as(StepVerifier::create)//
.assertNext(changes -> {
assertThat(changes.allOk()).isTrue();
}).verifyComplete();
redisTemplate.opsForHash().getExpire(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);
}).verifyComplete();
}
@ParameterizedRedisTest
@EnabledOnCommand("HEXPIRE")
void testPersistAndGetExpireMillis() {
K key = keyFactory.instance();
HK key1 = hashKeyFactory.instance();
HV val1 = hashValueFactory.instance();
HK key2 = hashKeyFactory.instance();
HV val2 = hashValueFactory.instance();
putAll(key, key1, val1, key2, val2);
redisTemplate.opsForHash().expireAt(key, Instant.now().plusMillis(1500), List.of(key1, key2))
.as(StepVerifier::create)//
.assertNext(changes -> {
assertThat(changes.allOk()).isTrue();
}).verifyComplete();
redisTemplate.opsForHash().persist(key, List.of(key1, key2)).as(StepVerifier::create)//
.assertNext(changes -> {
assertThat(changes.allOk()).isTrue();
}).verifyComplete();
redisTemplate.opsForHash().getExpire(key, List.of(key1, key2)).as(StepVerifier::create)//
.assertNext(expirations -> {
assertThat(expirations.persistent()).contains(key1, key2);
}).verifyComplete();
}
@ParameterizedRedisTest // DATAREDIS-602
void delete() {

View File

@@ -0,0 +1,94 @@
/*
* 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 static org.assertj.core.api.Assertions.assertThat;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
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;
/**
* @author Christoph Strobl
* @since 2025/02
*/
class ExpirationsUnitTest {
static final String KEY_1 = "key-1";
static final String KEY_2 = "key-2";
static final String KEY_3 = "key-3";
@ParameterizedTest
@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(expiration.raw()).isEqualTo(120L);
assertThat(expiration.value()).isEqualTo(targetUnit.convert(120, TimeUnit.SECONDS));
});
}
@Test
void expirationsCategorizesElements() {
Expirations<String> exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L)));
assertThat(exp.persistent()).containsExactly(KEY_2);
assertThat(exp.missing()).containsExactly(KEY_1);
assertThat(exp.expiring()).containsExactly(Map.entry(KEY_3, Duration.ofMinutes(2)));
}
@Test
void returnsNullForMissingElements() {
Expirations<String> exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L)));
assertThat(exp.expirationOf("missing")).isNull();
assertThat(exp.ttlOf("missing")).isNull();
}
@Test
void ttlReturnsDurationForEntriesWithTimeout() {
Expirations<String> exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L)));
assertThat(exp.ttlOf(KEY_3)).isEqualTo(Duration.ofMinutes(2));
}
@Test
void ttlReturnsNullForPersistentAndMissingEntries() {
Expirations<String> exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L)));
assertThat(exp.ttlOf(KEY_1)).isNull();
assertThat(exp.ttlOf(KEY_2)).isNull();
}
static Expirations<String> createExpirations(Timeouts timeouts) {
List<String> keys = IntStream.range(1, timeouts.raw().size() + 1).mapToObj("key-%s"::formatted).toList();
return Expirations.of(timeouts.timeUnit(), keys, timeouts);
}
}

View File

@@ -15,8 +15,9 @@
*/
package org.springframework.data.redis.support.collections;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assumptions.*;
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 java.io.IOException;
import java.text.DecimalFormat;
@@ -41,6 +42,7 @@ 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.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisOperations;
@@ -195,31 +197,41 @@ public abstract class AbstractRedisMapIntegrationTests<K, V> {
}
@ParameterizedRedisTest
@EnabledOnCommand("HEXPIRE")
void testExpire() {
K k1 = getKey();
V v1 = getValue();
assertThat(map.put(k1, v1)).isEqualTo(null);
Collection<K> keys = Collections.singletonList(k1);
assertThat(map.expire(Duration.ofSeconds(5), keys)).contains(1L);
assertThat(map.getExpire(keys)).allSatisfy(expiration -> assertThat(expiration).isBetween(1L, 5L));
assertThat(map.getExpire(TimeUnit.MILLISECONDS, keys))
.allSatisfy(expiration -> assertThat(expiration).isBetween(1000L, 5000L));
assertThat(map.persist(keys)).contains(1L);
assertThat(map.expire(Duration.ofSeconds(5), keys)).satisfies(ExpireChanges::allOk);
assertThat(map.getExpire(keys)).satisfies(expiration -> {
assertThat(expiration.expirationOf(k1).raw()).isBetween(1L, 5L);
});
assertThat(map.getExpire(TimeUnit.MILLISECONDS, keys)).satisfies(expiration -> {
assertThat(expiration.expirationOf(k1).raw()).isBetween(1000L, 5000L);
});
assertThat(map.persist(keys)).satisfies(ExpireChanges::allOk);
}
@ParameterizedRedisTest
@EnabledOnCommand("HEXPIRE")
void testExpireAt() {
K k1 = getKey();
V v1 = getValue();
assertThat(map.put(k1, v1)).isEqualTo(null);
Collection<K> keys = Collections.singletonList(k1);
assertThat(map.expireAt(Instant.now().plusSeconds(5), keys)).contains(1L);
assertThat(map.getExpire(keys)).allSatisfy(expiration -> assertThat(expiration).isBetween(1L, 5L));
assertThat(map.getExpire(TimeUnit.MILLISECONDS, keys))
.allSatisfy(expiration -> assertThat(expiration).isBetween(1000L, 5000L));
assertThat(map.persist(keys)).contains(1L);
assertThat(map.expireAt(Instant.now().plusSeconds(5), keys)).satisfies(ExpireChanges::allOk);
assertThat(map.getExpire(keys)).satisfies(expiration -> {
assertThat(expiration.expirationOf(k1).raw()).isBetween(1L, 5L);
});
assertThat(map.getExpire(TimeUnit.MILLISECONDS, keys)).satisfies(expiration -> {
assertThat(expiration.expirationOf(k1).raw()).isBetween(1000L, 5000L);
});
assertThat(map.persist(keys)).satisfies(ExpireChanges::allOk);
}
@ParameterizedRedisTest