diff --git a/pom.xml b/pom.xml index 8657b1992a..ec41e24376 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,13 @@ - + 4.0.0 org.springframework.data spring-data-redis - 2.6.0-SNAPSHOT + 2.6.0-2050-SNAPSHOT Spring Data Redis diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index eacddaa979..eab00c043a 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -7,7 +7,7 @@ This section briefly covers items that are new and noteworthy in the latest rele == New in Spring Data Redis 2.6 * Support for `SubscriptionListener` when using `MessageListener` for subscription confirmation callbacks. `ReactiveRedisMessageListenerContainer` and `ReactiveRedisOperations` provide `receiveLater(…)` and `listenToLater(…)` methods to await until Redis acknowledges the subscription. -* Support Redis 6.2 commands (`LPOP`/`RPOP` with `count`, `COPY`). +* Support Redis 6.2 commands (`LPOP`/`RPOP` with `count`, `COPY`, `GETEX`, `GETDEL`). [[new-in-2.5.0]] == New in Spring Data Redis 2.5 diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java index 6ce7ac173c..a6348abafd 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -410,6 +410,46 @@ public byte[] get(byte[] key) { return convertAndReturn(delegate.get(key), Converters.identityConverter()); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#getDel(byte[]) + */ + @Nullable + @Override + public byte[] getDel(byte[] key) { + return convertAndReturn(delegate.getDel(key), Converters.identityConverter()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#getDel(byte[]) + */ + @Nullable + @Override + public String getDel(String key) { + return convertAndReturn(delegate.getDel(serialize(key)), bytesToString); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#get(byte[], org.springframework.data.redis.core.types.Expiration) + */ + @Nullable + @Override + public byte[] getEx(byte[] key, Expiration expiration) { + return convertAndReturn(delegate.getEx(key, expiration), Converters.identityConverter()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#get(byte[], org.springframework.data.redis.core.types.Expiration) + */ + @Nullable + @Override + public String getEx(String key, Expiration expiration) { + return convertAndReturn(delegate.getEx(serialize(key), expiration), bytesToString); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisStringCommands#getBit(byte[], long) diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java index f26ba14cd3..fad7b59af8 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -276,6 +276,20 @@ default byte[] get(byte[] key) { return stringCommands().get(key); } + /** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */ + @Override + @Deprecated + default byte[] getEx(byte[] key, Expiration expiration) { + return stringCommands().getEx(key, expiration); + } + + /** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */ + @Override + @Deprecated + default byte[] getDel(byte[] key) { + return stringCommands().getDel(key); + } + /** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */ @Override @Deprecated diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java index 575b124185..20304b51b9 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java @@ -218,6 +218,109 @@ default Mono get(ByteBuffer key) { */ Flux> get(Publisher keys); + /** + * Return the value at {@code key} and delete the key. + * + * @param key must not be {@literal null}. + * @return {@link Mono#empty()} in case {@literal key} does not exist. + * @see Redis Documentation: GETDEL + * @since 2.6 + */ + default Mono getDel(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return getDel(Mono.just(new KeyCommand(key))).next().filter(CommandResponse::isPresent) + .map(CommandResponse::getOutput); + } + + /** + * Return the value at {@code key} and delete the key. + * + * @param commands must not be {@literal null}. + * @return {@link Flux} of {@link ByteBufferResponse} holding the {@literal key} to get along with the value + * retrieved. + * @see Redis Documentation: GETDEL + * @since 2.6 + */ + Flux> getDel(Publisher commands); + + /** + * {@link Command} for {@code GETEX}. + * + * @author Mark Paluch + * @since 2.6 + */ + class GetExCommand extends KeyCommand { + + private final Expiration expiration; + + private GetExCommand(@Nullable ByteBuffer key, Expiration expiration) { + + super(key); + + Assert.notNull(expiration, "Expiration must not be null!"); + this.expiration = expiration; + } + + /** + * Creates a new {@link GetExCommand} given a {@code key}. + * + * @param key must not be {@literal null}. + * @return a new {@link GetExCommand} for {@code key}. + */ + public static GetExCommand key(ByteBuffer key) { + return new GetExCommand(key, Expiration.persistent()); + } + + /** + * Applies {@link Expiration}. Constructs a new command instance with all previously configured properties. + * + * @param options must not be {@literal null}. + * @return a new {@link GetExCommand} with {@link Expiration} applied. + */ + public GetExCommand withExpiration(Expiration expiration) { + return new GetExCommand(getKey(), expiration); + } + + /** + * Get the {@link Expiration} to apply. + * + * @return never {@literal null}. + */ + public Expiration getExpiration() { + return expiration; + } + } + + /** + * Return the value at {@code key} and expire the key by applying {@link Expiration}. + * + * @param key must not be {@literal null}. + * @param expiration must not be {@literal null}. + * @return {@link Mono#empty()} in case {@literal key} does not exist. + * @see Redis Documentation: GETEX + * @since 2.6 + */ + default Mono getEx(ByteBuffer key, Expiration expiration) { + + Assert.notNull(key, "Key must not be null!"); + + return getEx(Mono.just(GetExCommand.key(key).withExpiration(expiration))).next().filter(CommandResponse::isPresent) + .map(CommandResponse::getOutput); + } + + /** + * Return the value at {@code key} and expire the key by applying {@link Expiration}. + * + * @param commands must not be {@literal null}. + * @return {@link Flux} of {@link ByteBufferResponse} holding the {@literal key} to get along with the value + * retrieved. + * @see Redis Documentation: GETEX + * @since 2.6 + */ + Flux> getEx(Publisher commands); + /** * Set {@literal value} for {@literal key} and return the existing value. * diff --git a/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java index f174c011ea..febb103879 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.springframework.data.domain.Range; import org.springframework.data.redis.core.types.Expiration; @@ -45,6 +46,33 @@ enum BitOperation { @Nullable byte[] get(byte[] key); + /** + * Return the value at {@code key} and delete the key. + * + * @param key must not be {@literal null}. + * @return {@literal null} when key does not exist or used in pipeline / transaction. + * @see Redis Documentation: GETDEL + * @since 2.6 + */ + @Nullable + byte[] getDel(byte[] key); + + /** + * Return the value at {@code key} and expire the key by applying {@link Expiration}. + *

+ * Use {@link Expiration#seconds(long)} for {@code EX}.
+ * Use {@link Expiration#milliseconds(long)} for {@code PX}.
+ * Use {@link Expiration#unixTimestamp(long, TimeUnit)} for {@code EXAT | PXAT}.
+ * + * @param key must not be {@literal null}. + * @param expiration must not be {@literal null}. + * @return {@literal null} when key does not exist or used in pipeline / transaction. + * @see Redis Documentation: GETEX + * @since 2.6 + */ + @Nullable + byte[] getEx(byte[] key, Expiration expiration); + /** * Set {@code value} of {@code key} and return its old value. * @@ -403,4 +431,5 @@ public static SetOption ifAbsent() { return SET_IF_ABSENT; } } + } diff --git a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index 66c9f27936..d828fbe692 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -397,6 +397,29 @@ interface StringTuple extends Tuple { */ String get(String key); + /** + * Return the value at {@code key} and delete the key. + * + * @param key must not be {@literal null}. + * @return {@literal null} when key does not exist or used in pipeline / transaction. + * @see Redis Documentation: GETDEL + * @since 2.6 + */ + @Nullable + String getDel(String key); + + /** + * Return the value at {@code key} and expire the key by applying {@link Expiration}. + * + * @param key must not be {@literal null}. + * @param expiration must not be {@literal null}. + * @return {@literal null} when key does not exist or used in pipeline / transaction. + * @see Redis Documentation: GETEX + * @since 2.6 + */ + @Nullable + String getEx(String key, Expiration expiration); + /** * Set {@code value} of {@code key} and return its old value. * diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java index db37528473..c986c257f4 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java @@ -35,6 +35,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConverters; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.util.ByteUtils; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -68,6 +69,41 @@ public byte[] get(byte[] key) { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#getDel(byte[]) + */ + @Nullable + @Override + public byte[] getDel(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + try { + return connection.getCluster().getDel(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#getEx(byte[], org.springframework.data.redis.core.types.Expiration) + */ + @Nullable + @Override + public byte[] getEx(byte[] key, Expiration expiration) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(expiration, "Expiration must not be null!"); + + try { + return connection.getCluster().getEx(key, JedisConverters.toGetExParams(expiration)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisStringCommands#getSet(byte[], byte[]) diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java index 1498b3f902..54a129eb5c 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java @@ -23,6 +23,7 @@ import redis.clients.jedis.ScanParams; import redis.clients.jedis.SortingParams; import redis.clients.jedis.params.GeoRadiusParam; +import redis.clients.jedis.params.GetExParams; import redis.clients.jedis.params.SetParams; import redis.clients.jedis.params.ZAddParams; import redis.clients.jedis.util.SafeEncoder; @@ -430,15 +431,48 @@ public static SetParams toSetCommandExPxArgument(Expiration expiration, SetParam return paramsToUse.keepttl(); } - if (!expiration.isPersistent()) { - if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { - return paramsToUse.px(expiration.getExpirationTime()); - } + if (expiration.isPersistent()) { + return paramsToUse; + } + + if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { + return expiration.isUnixTimestamp() ? paramsToUse.pxAt(expiration.getExpirationTime()) : paramsToUse.px(expiration.getExpirationTime()); + } + + return expiration.isUnixTimestamp() ? paramsToUse.exAt(expiration.getConverted(TimeUnit.SECONDS)) : paramsToUse.ex(expiration.getConverted(TimeUnit.SECONDS)); + } + + /** + * Converts a given {@link Expiration} to the according {@code GETEX} command argument depending on + * {@link Expiration#isUnixTimestamp()}. + *

+ *
{@link TimeUnit#MILLISECONDS}
+ *
{@code PX|PXAT}
+ *
{@link TimeUnit#SECONDS}
+ *
{@code EX|EXAT}
+ *
+ * + * @param expiration must not be {@literal null}. + * @return + * @since 2.6 + */ + static GetExParams toGetExParams(Expiration expiration) { + + GetExParams params = new GetExParams(); - return paramsToUse.ex((int) expiration.getExpirationTime()); + if (expiration.isPersistent()) { + return params.persist(); + } + + if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { + if (expiration.isUnixTimestamp()) { + return params.pxAt(expiration.getExpirationTime()); + } + return params.px(expiration.getExpirationTime()); } - return params; + return expiration.isUnixTimestamp() ? params.exAt(expiration.getConverted(TimeUnit.SECONDS)) + : params.ex(expiration.getConverted(TimeUnit.SECONDS)); } /** diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java index e9bf3ffeb4..ad8d06f3ba 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java @@ -57,6 +57,34 @@ public byte[] get(byte[] key) { return connection.invoke().just(BinaryJedis::get, MultiKeyPipelineBase::get, key); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#getDel(byte[]) + */ + @Nullable + @Override + public byte[] getDel(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().just(BinaryJedis::getDel, MultiKeyPipelineBase::getDel, key); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#getEx(byte[], org.springframework.data.redis.core.types.Expiration) + */ + @Nullable + @Override + public byte[] getEx(byte[] key, Expiration expiration) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(expiration, "Expiration must not be null!"); + + return connection.invoke().just(BinaryJedis::getEx, MultiKeyPipelineBase::getEx, key, + JedisConverters.toGetExParams(expiration)); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisStringCommands#getSet(byte[], byte[]) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index b61cb7a9ae..aa86703ee3 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -699,11 +699,19 @@ public static SetArgs toSetArgs(@Nullable Expiration expiration, @Nullable SetOp } else if (!expiration.isPersistent()) { switch (expiration.getTimeUnit()) { - case SECONDS: - args.ex(expiration.getExpirationTime()); + case MILLISECONDS: + if (expiration.isUnixTimestamp()) { + args.pxAt(expiration.getConverted(TimeUnit.MILLISECONDS)); + } else { + args.px(expiration.getConverted(TimeUnit.MILLISECONDS)); + } break; default: - args.px(expiration.getConverted(TimeUnit.MILLISECONDS)); + if (expiration.isUnixTimestamp()) { + args.exAt(expiration.getConverted(TimeUnit.SECONDS)); + } else { + args.ex(expiration.getConverted(TimeUnit.SECONDS)); + } break; } } @@ -725,6 +733,36 @@ public static SetArgs toSetArgs(@Nullable Expiration expiration, @Nullable SetOp return args; } + /** + * Convert {@link Expiration} to {@link GetExArgs}. + * + * @param expiration can be {@literal null}. + * @return + * @since 2.6 + */ + static GetExArgs toGetExArgs(@Nullable Expiration expiration) { + + GetExArgs args = new GetExArgs(); + + if (expiration == null) { + return args; + } + + if (expiration.isPersistent()) { + return args.persist(); + } + + if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { + if (expiration.isUnixTimestamp()) { + return args.pxAt(expiration.getExpirationTime()); + } + return args.px(expiration.getExpirationTime()); + } + + return expiration.isUnixTimestamp() ? args.exAt(expiration.getConverted(TimeUnit.SECONDS)) + : args.ex(expiration.getConverted(TimeUnit.SECONDS)); + } + static Converter, Long> toTimeConverter(TimeUnit timeUnit) { return source -> { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java index ff05a7d85b..abb05e0727 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java @@ -16,6 +16,7 @@ package org.springframework.data.redis.connection.lettuce; import io.lettuce.core.BitFieldArgs; +import io.lettuce.core.GetExArgs; import io.lettuce.core.SetArgs; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -25,6 +26,7 @@ import java.util.stream.Collectors; import org.reactivestreams.Publisher; + import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.ReactiveRedisConnection.AbsentByteBufferResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; @@ -140,6 +142,40 @@ public Flux> get(Publisher commands) })); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#getDel(org.reactivestreams.Publisher) + */ + @Override + public Flux> getDel(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).concatMap((command) -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return cmd.getdel(command.getKey()).map((value) -> new ByteBufferResponse<>(command, value)) + .defaultIfEmpty(new AbsentByteBufferResponse<>(command)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#getDel(org.reactivestreams.Publisher) + */ + @Override + public Flux> getEx(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).concatMap((command) -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + GetExArgs args = LettuceConverters.toGetExArgs(command.getExpiration()); + + return cmd.getex(command.getKey(), args).map((value) -> new ByteBufferResponse<>(command, value)) + .defaultIfEmpty(new AbsentByteBufferResponse<>(command)); + })); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#setNX(org.reactivestreams.Publisher) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java index e9d149027f..52ad1d62e7 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java @@ -55,6 +55,33 @@ public byte[] get(byte[] key) { return connection.invoke().just(RedisStringAsyncCommands::get, key); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#getDel(byte[]) + */ + @Nullable + @Override + public byte[] getDel(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().just(RedisStringAsyncCommands::getdel, key); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#getEx(byte[], org.springframework.data.redis.core.types.Expiration) + */ + @Nullable + @Override + public byte[] getEx(byte[] key, Expiration expiration) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(expiration, "Expiration must not be null!"); + + return connection.invoke().just(RedisStringAsyncCommands::getex, key, LettuceConverters.toGetExArgs(expiration)); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisStringCommands#getSet(byte[], byte[]) diff --git a/src/main/java/org/springframework/data/redis/core/BoundValueOperations.java b/src/main/java/org/springframework/data/redis/core/BoundValueOperations.java index ebf5ecaa76..07195db5a9 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundValueOperations.java @@ -93,7 +93,7 @@ default void set(V value, Duration timeout) { Boolean setIfAbsent(V value, long timeout, TimeUnit unit); /** - * Set bound key to hold the string {@code value} and expiration {@code timeout} if {@code key} is absent. + * Set bound key to hold the string {@code value} and expiration {@code timeout} if the bound key is absent. * * @param value must not be {@literal null}. * @param timeout must not be {@literal null}. @@ -115,7 +115,7 @@ default Boolean setIfAbsent(V value, Duration timeout) { } /** - * Set the bound key to hold the string {@code value} if {@code key} is present. + * Set the bound key to hold the string {@code value} if the bound key is present. * * @param value must not be {@literal null}. * @return command result indicating if the key has been set. @@ -127,7 +127,7 @@ default Boolean setIfAbsent(V value, Duration timeout) { Boolean setIfPresent(V value); /** - * Set the bound key to hold the string {@code value} and expiration {@code timeout} if {@code key} is present. + * Set the bound key to hold the string {@code value} and expiration {@code timeout} if the bound key is present. * * @param value must not be {@literal null}. * @param timeout the key expiration timeout. @@ -141,7 +141,7 @@ default Boolean setIfAbsent(V value, Duration timeout) { Boolean setIfPresent(V value, long timeout, TimeUnit unit); /** - * Set the bound key to hold the string {@code value} and expiration {@code timeout} if {@code key} is present. + * Set the bound key to hold the string {@code value} and expiration {@code timeout} if the bound key is present. * * @param value must not be {@literal null}. * @param timeout must not be {@literal null}. @@ -165,12 +165,57 @@ default Boolean setIfPresent(V value, Duration timeout) { /** * Get the value of the bound key. * - * @return {@literal null} when used in pipeline / transaction. + * @return {@literal null} when key does not exist or used in pipeline / transaction. * @see Redis Documentation: GET */ @Nullable V get(); + /** + * Return the value at the bound key and delete the key. + * + * @param key must not be {@literal null}. + * @return {@literal null} when key does not exist or used in pipeline / transaction. + * @see Redis Documentation: GETDEL + * @since 2.6 + */ + @Nullable + V getAndDelete(); + + /** + * Return the value at the bound key and expire the key by applying {@code timeout}. + * + * @param timeout + * @param unit must not be {@literal null}. + * @return {@literal null} when key does not exist or used in pipeline / transaction. + * @see Redis Documentation: GETEX + * @since 2.6 + */ + @Nullable + V getAndExpire(long timeout, TimeUnit unit); + + /** + * Return the value at the bound key and expire the key by applying {@code timeout}. + * + * @param timeout must not be {@literal null}. + * @return {@literal null} when key does not exist or used in pipeline / transaction. + * @see Redis Documentation: GETEX + * @since 2.6 + */ + @Nullable + V getAndExpire(Duration timeout); + + /** + * Return the value at the bound key and persist the key. This operation removes any TTL that is associated with the + * bound key. + * + * @return {@literal null} when key does not exist or used in pipeline / transaction. + * @see Redis Documentation: GETEX + * @since 2.6 + */ + @Nullable + V getAndPersist(); + /** * Set {@code value} of the bound key and return its old value. * diff --git a/src/main/java/org/springframework/data/redis/core/DefaultBoundValueOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultBoundValueOperations.java index af1e47c163..cd533485c0 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultBoundValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundValueOperations.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.core; +import java.time.Duration; import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.DataType; @@ -50,6 +51,46 @@ public V get() { return ops.get(getKey()); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundValueOperations#get() + */ + @Nullable + @Override + public V getAndDelete() { + return ops.getAndDelete(getKey()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundValueOperations#get(long, java.util.concurrent.TimeUnit) + */ + @Nullable + @Override + public V getAndExpire(long timeout, TimeUnit unit) { + return ops.getAndExpire(getKey(), timeout, unit); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundValueOperations#get(java.time.Duration) + */ + @Nullable + @Override + public V getAndExpire(Duration timeout) { + return ops.getAndExpire(getKey(), timeout); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundValueOperations#getAndPersist() + */ + @Nullable + @Override + public V getAndPersist() { + return ops.getAndPersist(getKey()); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.BoundValueOperations#getAndSet(java.lang.Object) diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java index ac611cb9d6..31c8f0849f 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java @@ -176,6 +176,43 @@ public Mono get(Object key) { .map(this::readValue)); } + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#getAndDelete(java.lang.Object) + */ + @Override + public Mono getAndDelete(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.getDel(rawKey(key)) // + .map(this::readValue)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#getAndExpire(java.lang.Object, java.time.Duration) + */ + @Override + public Mono getAndExpire(K key, Duration timeout) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(timeout, "Timeout must not be null!"); + + return createMono(connection -> connection.getEx(rawKey(key), Expiration.from(timeout)) // + .map(this::readValue)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#getAndPersist(java.lang.Object) + */ + @Override + public Mono getAndPersist(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.getEx(rawKey(key), Expiration.persistent()) // + .map(this::readValue)); + } + /* (non-Javadoc) * @see org.springframework.data.redis.core.ReactiveValueOperations#getAndSet(java.lang.Object, java.lang.Object) */ diff --git a/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java index 0df7a3a62a..1a9eeb03be 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.core; +import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; @@ -59,6 +60,74 @@ protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { }, true); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ValueOperations#getAndDelete(java.lang.Object) + */ + @Nullable + @Override + public V getAndDelete(K key) { + + return execute(new ValueDeserializingRedisCallback(key) { + + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return connection.getDel(rawKey); + } + }, true); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ValueOperations#getAndPersist(java.lang.Object, long, java.util.concurrent.TimeUnit) + */ + @Nullable + @Override + public V getAndExpire(K key, long timeout, TimeUnit unit) { + + return execute(new ValueDeserializingRedisCallback(key) { + + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return connection.getEx(rawKey, Expiration.from(timeout, unit)); + } + }, true); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ValueOperations#getAndPersist(java.lang.Object, java.time.Duration) + */ + @Nullable + @Override + public V getAndExpire(K key, Duration timeout) { + + return execute(new ValueDeserializingRedisCallback(key) { + + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return connection.getEx(rawKey, Expiration.from(timeout)); + } + }, true); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ValueOperations#getAndPersist(java.lang.Object) + */ + @Nullable + @Override + public V getAndPersist(K key) { + + return execute(new ValueDeserializingRedisCallback(key) { + + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return connection.getEx(rawKey, Expiration.persistent()); + } + }, true); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.ValueOperations#getAndSet(java.lang.Object, java.lang.Object) diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java index ee79060842..d0e6d06426 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java @@ -117,6 +117,35 @@ public interface ReactiveValueOperations { */ Mono get(Object key); + /** + * Return the value at {@code key} and delete the key. + * + * @param key must not be {@literal null}. + * @see Redis Documentation: GETDEL + * @since 2.6 + */ + Mono getAndDelete(K key); + + /** + * Return the value at {@code key} and expire the key by applying {@code timeout}. + * + * @param key must not be {@literal null}. + * @param timeout must not be {@literal null}. + * @see Redis Documentation: GETEX + * @since 2.6 + */ + Mono getAndExpire(K key, Duration timeout); + + /** + * Return the value at {@code key} and persist the key. This operation removes any TTL that is associated with + * {@code key}. + * + * @param key must not be {@literal null}. + * @see Redis Documentation: GETEX + * @since 2.6 + */ + Mono getAndPersist(K key); + /** * Set {@code value} of {@code key} and return its old value. * diff --git a/src/main/java/org/springframework/data/redis/core/ValueOperations.java b/src/main/java/org/springframework/data/redis/core/ValueOperations.java index 9912cac07b..ae926debcc 100644 --- a/src/main/java/org/springframework/data/redis/core/ValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ValueOperations.java @@ -198,17 +198,65 @@ default Boolean setIfPresent(K key, V value, Duration timeout) { * Get the value of {@code key}. * * @param key must not be {@literal null}. - * @return {@literal null} when used in pipeline / transaction. + * @return {@literal null} when key does not exist or used in pipeline / transaction. * @see Redis Documentation: GET */ @Nullable V get(Object key); + /** + * Return the value at {@code key} and delete the key. + * + * @param key must not be {@literal null}. + * @return {@literal null} when key does not exist or used in pipeline / transaction. + * @see Redis Documentation: GETDEL + * @since 2.6 + */ + @Nullable + V getAndDelete(K key); + + /** + * Return the value at {@code key} and expire the key by applying {@code timeout}. + * + * @param key must not be {@literal null}. + * @param timeout + * @param unit must not be {@literal null}. + * @return {@literal null} when key does not exist or used in pipeline / transaction. + * @see Redis Documentation: GETEX + * @since 2.6 + */ + @Nullable + V getAndExpire(K key, long timeout, TimeUnit unit); + + /** + * Return the value at {@code key} and expire the key by applying {@code timeout}. + * + * @param key must not be {@literal null}. + * @param timeout must not be {@literal null}. + * @return {@literal null} when key does not exist or used in pipeline / transaction. + * @see Redis Documentation: GETEX + * @since 2.6 + */ + @Nullable + V getAndExpire(K key, Duration timeout); + + /** + * Return the value at {@code key} and persist the key. This operation removes any TTL that is associated with + * {@code key}. + * + * @param key must not be {@literal null}. + * @return {@literal null} when key does not exist or used in pipeline / transaction. + * @see Redis Documentation: GETEX + * @since 2.6 + */ + @Nullable + V getAndPersist(K key); + /** * Set {@code value} of {@code key} and return its old value. * * @param key must not be {@literal null}. - * @return {@literal null} when used in pipeline / transaction. + * @return {@literal null} when key does not exist or used in pipeline / transaction. * @see Redis Documentation: GETSET */ @Nullable diff --git a/src/main/java/org/springframework/data/redis/core/types/Expiration.java b/src/main/java/org/springframework/data/redis/core/types/Expiration.java index ab9068e3a8..cba082ad69 100644 --- a/src/main/java/org/springframework/data/redis/core/types/Expiration.java +++ b/src/main/java/org/springframework/data/redis/core/types/Expiration.java @@ -115,6 +115,17 @@ public static Expiration milliseconds(long expirationTime) { return new Expiration(expirationTime, TimeUnit.MILLISECONDS); } + /** + * Creates new {@link Expiration} with the given {@literal unix timestamp} and {@link TimeUnit}. + * + * @param unixTimestamp the unix timestamp at which the key will expire. + * @param timeUnit must not be {@literal null}. + * @return new instance of {@link Expiration}. + */ + public static Expiration unixTimestamp(long unixTimestamp, TimeUnit timeUnit) { + return new ExpireAt(unixTimestamp, timeUnit); + } + /** * Obtain an {@link Expiration} that indicates to keep the existing one. Eg. when sending a {@code SET} command. *

@@ -197,6 +208,14 @@ public boolean isKeepTtl() { return false; } + /** + * @return {@literal true} if {@link Expiration} is set to a specified Unix time at which the key will expire. + * @since 2.6 + */ + public boolean isUnixTimestamp() { + return false; + } + /** * @author Christoph Strobl * @since 2.4 @@ -214,4 +233,19 @@ public boolean isKeepTtl() { return true; } } + + /** + * @author Christoph Strobl + * @since 2.6 + */ + private static class ExpireAt extends Expiration { + + private ExpireAt(long expirationTime, @Nullable TimeUnit timeUnit) { + super(expirationTime, timeUnit); + } + + public boolean isUnixTimestamp() { + return true; + } + } } diff --git a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java index 23f741034a..cacafc14fe 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -1135,8 +1135,33 @@ void testType() { verifyResults(Arrays.asList(true, DataType.STRING)); } + @Test // GH-2050 + @EnabledOnCommand("GETEX") + void testGetEx() { + + actual.add(connection.set("testGS", "1")); + actual.add(connection.getEx("testGS", Expiration.seconds(10))); + actual.add(connection.ttl("testGS")); + + Long ttl = (Long) getResults().get(2); + + assertThat(ttl).isGreaterThan(1); + } + + @Test // GH-2050 + @EnabledOnCommand("GETDEL") + void testGetDel() { + + actual.add(connection.set("testGS", "1")); + actual.add(connection.getDel("testGS")); + actual.add(connection.exists("testGS")); + + verifyResults(Arrays.asList(true, "1", false)); + } + @Test void testGetSet() { + actual.add(connection.set("testGS", "1")); actual.add(connection.getSet("testGS", "2")); actual.add(connection.get("testGS")); @@ -1145,6 +1170,7 @@ void testGetSet() { @Test void testMSet() { + Map vals = new HashMap<>(); vals.put("color", "orange"); vals.put("size", "1"); @@ -1156,6 +1182,7 @@ void testMSet() { @Test void testMSetNx() { + Map vals = new HashMap<>(); vals.put("height", "5"); vals.put("width", "1"); @@ -1166,6 +1193,7 @@ void testMSetNx() { @Test void testMSetNxFailure() { + actual.add(connection.set("height", "2")); Map vals = new HashMap<>(); vals.put("height", "5"); @@ -1177,6 +1205,7 @@ void testMSetNxFailure() { @Test void testSetNx() { + actual.add(connection.setNX("notaround", "54")); actual.add(connection.get("notaround")); actual.add(connection.setNX("notaround", "55")); diff --git a/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java index f6ab929d2e..90547fb6b2 100644 --- a/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java @@ -186,6 +186,12 @@ public interface ClusterConnectionTests { // DATAREDIS-315 void getRangeShouldReturnValueCorrectly(); + // GH-2050 + void getExShouldWorkCorrectly(); + + // GH-2050 + void getDelShouldWorkCorrectly(); + // DATAREDIS-315 void getSetShouldWorkCorrectly(); diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java index e64c4754fd..1ad156d01d 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java @@ -696,6 +696,26 @@ public void getRangeShouldReturnValueCorrectly() { assertThat(clusterConnection.getRange(KEY_1_BYTES, 0, 2)).isEqualTo(JedisConverters.toBytes("val")); } + @Test // GH-2050 + @EnabledOnCommand("GETEX") + public void getExShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.getEx(KEY_1_BYTES, Expiration.seconds(10))).isEqualTo(VALUE_1_BYTES); + assertThat(clusterConnection.ttl(KEY_1_BYTES)).isGreaterThan(1); + } + + @Test // GH-2050 + @EnabledOnCommand("GETDEL") + public void getDelShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.getDel(KEY_1_BYTES)).isEqualTo(VALUE_1_BYTES); + assertThat(clusterConnection.exists(KEY_1_BYTES)).isFalse(); + } + @Test // DATAREDIS-315 public void getSetShouldWorkCorrectly() { diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java index e16bb58d16..548233fdfc 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.Test; +import redis.clients.jedis.params.GetExParams; import redis.clients.jedis.params.SetParams; import java.util.Arrays; @@ -26,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.RedisServer; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; @@ -211,6 +213,22 @@ void toSetCommandExPxOptionShouldReturnEXforMilliseconds() { assertThat(toString(JedisConverters.toSetCommandExPxArgument(Expiration.milliseconds(100)))).isEqualTo("px 100"); } + @Test // GH-2050 + void convertsExpirationToSetPXAT() { + + assertThat(JedisConverters.toSetCommandExPxArgument(Expiration.unixTimestamp(10, TimeUnit.MILLISECONDS))) + .extracting(SetParams::toString) + .isEqualTo(SetParams.setParams().pxAt(10).toString()); + } + + @Test // GH-2050 + void convertsExpirationToSetEXAT() { + + assertThat(JedisConverters.toSetCommandExPxArgument(Expiration.unixTimestamp(1, TimeUnit.MINUTES))) + .extracting(SetParams::toString) + .isEqualTo(SetParams.setParams().exAt(60).toString()); + } + @Test // DATAREDIS-316, DATAREDIS-749 void toSetCommandNxXxOptionShouldReturnNXforAbsent() { assertThat(toString(JedisConverters.toSetCommandNxXxArgument(SetOption.ifAbsent()))).isEqualTo("nx"); @@ -226,6 +244,54 @@ void toSetCommandNxXxOptionShouldReturnEmptyArrayforUpsert() { assertThat(toString(JedisConverters.toSetCommandNxXxArgument(SetOption.upsert()))).isEqualTo(""); } + @Test // GH-2050 + void convertsExpirationToGetExEX() { + + assertThat(JedisConverters.toGetExParams(Expiration.seconds(10))) + .extracting(GetExParams::toString) + .isEqualTo(new GetExParams().ex(10).toString()); + } + + @Test // GH-2050 + void convertsExpirationWithTimeUnitToGetExEX() { + + assertThat(JedisConverters.toGetExParams(Expiration.from(1, TimeUnit.MINUTES))) + .extracting(GetExParams::toString) + .isEqualTo(new GetExParams().ex(60).toString()); + } + + @Test // GH-2050 + void convertsExpirationToGetExPEX() { + + assertThat(JedisConverters.toGetExParams(Expiration.milliseconds(10))) + .extracting(GetExParams::toString) + .isEqualTo(new GetExParams().px(10).toString()); + } + + @Test // GH-2050 + void convertsExpirationToGetExEXAT() { + + assertThat(JedisConverters.toGetExParams(Expiration.unixTimestamp(10, TimeUnit.SECONDS))) + .extracting(GetExParams::toString) + .isEqualTo(new GetExParams().exAt(10).toString()); + } + + @Test // GH-2050 + void convertsExpirationWithTimeUnitToGetExEXAT() { + + assertThat(JedisConverters.toGetExParams(Expiration.unixTimestamp(1, TimeUnit.MINUTES))) + .extracting(GetExParams::toString) + .isEqualTo(new GetExParams().exAt(60).toString()); + } + + @Test // GH-2050 + void convertsExpirationToGetExPXAT() { + + assertThat(JedisConverters.toGetExParams(Expiration.unixTimestamp(10, TimeUnit.MILLISECONDS))) + .extracting(GetExParams::toString) + .isEqualTo(new GetExParams().pxAt(10).toString()); + } + private void verifyRedisServerInfo(RedisServer server, Map values) { for (Map.Entry entry : values.entrySet()) { diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java index 4b4de458ad..e628e234ed 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java @@ -58,6 +58,7 @@ import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.condition.EnabledOnRedisClusterAvailable; import org.springframework.data.redis.test.extension.LettuceExtension; import org.springframework.data.redis.test.extension.LettuceTestClientResources; @@ -727,6 +728,26 @@ public void getRangeShouldReturnValueCorrectly() { assertThat(clusterConnection.getRange(KEY_1_BYTES, 0, 2)).isEqualTo(LettuceConverters.toBytes("val")); } + @Test // GH-2050 + @EnabledOnCommand("GETEX") + public void getExShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.getEx(KEY_1_BYTES, Expiration.seconds(10))).isEqualTo(VALUE_1_BYTES); + assertThat(clusterConnection.ttl(KEY_1_BYTES)).isGreaterThan(1); + } + + @Test // GH-2050 + @EnabledOnCommand("GETDEL") + public void getDelShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.getDel(KEY_1_BYTES)).isEqualTo(VALUE_1_BYTES); + assertThat(clusterConnection.exists(KEY_1_BYTES)).isFalse(); + } + @Test // DATAREDIS-315 public void getSetShouldWorkCorrectly() { diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceCommandArgsComparator.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceCommandArgsComparator.java new file mode 100644 index 0000000000..5c1e5c028f --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceCommandArgsComparator.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 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.lettuce; + +import io.lettuce.core.CompositeArgument; +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.protocol.CommandArgs; + +import org.assertj.core.api.Assertions; + +/** + * @author Christoph Strobl + */ +public class LettuceCommandArgsComparator { + + static void argsEqual(CompositeArgument args1, CompositeArgument args2) { + + CommandArgs stringStringCommandArgs1 = new CommandArgs<>(StringCodec.UTF8); + args1.build(stringStringCommandArgs1); + + CommandArgs stringStringCommandArgs2 = new CommandArgs<>(StringCodec.UTF8); + args2.build(stringStringCommandArgs2); + + Assertions.assertThat(stringStringCommandArgs1.toCommandString()) + .isEqualTo(stringStringCommandArgs2.toCommandString()); + } + + static LettuceCompositeArgumentAssert assertThatCommandArgument(CompositeArgument command) { + return new LettuceCompositeArgumentAssert() { + + @Override + public LettuceCompositeArgumentAssert isEqualTo(CompositeArgument expected) { + + argsEqual(command, expected); + return this; + } + }; + } + + interface LettuceCompositeArgumentAssert { + LettuceCompositeArgumentAssert isEqualTo(CompositeArgument command); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java index ebc32e6abc..917fd1e80a 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java @@ -17,8 +17,10 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.redis.connection.ClusterTestVariables.*; +import static org.springframework.data.redis.connection.lettuce.LettuceCommandArgsComparator.*; import static org.springframework.test.util.ReflectionTestUtils.*; +import io.lettuce.core.GetExArgs; import io.lettuce.core.Limit; import io.lettuce.core.RedisURI; import io.lettuce.core.SetArgs; @@ -29,16 +31,18 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; - import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisClusterNode.Flag; import org.springframework.data.redis.connection.RedisClusterNode.LinkState; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.connection.RedisZSetCommands; +import org.springframework.data.redis.connection.jedis.JedisConverters; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; +import redis.clients.jedis.params.SetParams; /** * @author Christoph Strobl @@ -134,6 +138,20 @@ void toSetArgsShouldSetExForSeconds() { assertThat((Boolean) getField(args, "xx")).isEqualTo(Boolean.FALSE); } + @Test // GH-2050 + void convertsExpirationToSetPXAT() { + + assertThatCommandArgument(LettuceConverters.toSetArgs(Expiration.unixTimestamp(10, TimeUnit.MILLISECONDS), null)) + .isEqualTo(SetArgs.Builder.pxAt(10)); + } + + @Test // GH-2050 + void convertsExpirationToSetEXAT() { + + assertThatCommandArgument(LettuceConverters.toSetArgs(Expiration.unixTimestamp(1, TimeUnit.MINUTES), null)) + .isEqualTo(SetArgs.Builder.exAt(60)); + } + @Test // DATAREDIS-316 void toSetArgsShouldSetPxForMilliseconds() { @@ -193,4 +211,45 @@ void toLimit() { assertThat(limit.isLimited()).isTrue(); assertThat(limit.getCount()).isEqualTo(5L); } + + @Test // GH-2050 + void convertsExpirationToGetExEX() { + + assertThatCommandArgument(LettuceConverters.toGetExArgs(Expiration.seconds(10))).isEqualTo(new GetExArgs().ex(10)); + } + + @Test // GH-2050 + void convertsExpirationWithTimeUnitToGetExEX() { + + assertThatCommandArgument(LettuceConverters.toGetExArgs(Expiration.from(1, TimeUnit.MINUTES))) + .isEqualTo(new GetExArgs().ex(60)); + } + + @Test // GH-2050 + void convertsExpirationToGetExPEX() { + + assertThatCommandArgument(LettuceConverters.toGetExArgs(Expiration.milliseconds(10))) + .isEqualTo(new GetExArgs().px(10)); + } + + @Test // GH-2050 + void convertsExpirationToGetExEXAT() { + + assertThatCommandArgument(LettuceConverters.toGetExArgs(Expiration.unixTimestamp(10, TimeUnit.SECONDS))) + .isEqualTo(new GetExArgs().exAt(10)); + } + + @Test // GH-2050 + void convertsExpirationWithTimeUnitToGetExEXAT() { + + assertThatCommandArgument(LettuceConverters.toGetExArgs(Expiration.unixTimestamp(1, TimeUnit.MINUTES))) + .isEqualTo(new GetExArgs().exAt(60)); + } + + @Test // GH-2050 + void convertsExpirationToGetExPXAT() { + + assertThatCommandArgument(LettuceConverters.toGetExArgs(Expiration.unixTimestamp(10, TimeUnit.MILLISECONDS))) + .isEqualTo(new GetExArgs().pxAt(10)); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsIntegrationTests.java index 573094da62..b78727220e 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsIntegrationTests.java @@ -51,6 +51,7 @@ import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; import org.springframework.data.redis.test.util.HexStringUtils; import org.springframework.data.redis.util.ByteUtils; @@ -66,6 +67,32 @@ public LettuceReactiveStringCommandsIntegrationTests(Fixture fixture) { super(fixture); } + @ParameterizedRedisTest // GH-2050 + @EnabledOnCommand("GETEX") + void getExShouldWorkCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + connection.stringCommands().getEx(KEY_1_BBUFFER, Expiration.seconds(10)).as(StepVerifier::create) // + .expectNext(VALUE_1_BBUFFER) // + .verifyComplete(); + + assertThat(nativeCommands.ttl(KEY_1)).isGreaterThan(1L); + } + + @ParameterizedRedisTest // GH-2050 + @EnabledOnCommand("GETDEL") + void getDelShouldWorkCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + connection.stringCommands().getDel(KEY_1_BBUFFER).as(StepVerifier::create) // + .expectNext(VALUE_1_BBUFFER) // + .verifyComplete(); + + assertThat(nativeCommands.exists(KEY_1)).isZero(); + } + @ParameterizedRedisTest // DATAREDIS-525 void getSetShouldReturnPreviousValueCorrectly() { diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java index e4f52db9f9..ebfb6b62cc 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java @@ -40,6 +40,7 @@ import org.springframework.data.redis.core.ReactiveOperationsTestParams.Fixture; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; @@ -235,6 +236,50 @@ void get() { valueOperations.get(key).as(StepVerifier::create).expectNext(value).verifyComplete(); } + @ParameterizedRedisTest // GH-2050 + @EnabledOnCommand("GETEX") + void getAndExpire() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + valueOperations.set(key, value).as(StepVerifier::create).expectNext(true).verifyComplete(); + + valueOperations.getAndExpire(key, Duration.ofSeconds(10)).as(StepVerifier::create).expectNext(value) + .verifyComplete(); + + redisTemplate.getExpire(key).as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isGreaterThan(Duration.ZERO)).verifyComplete(); + } + + @ParameterizedRedisTest // GH-2050 + @EnabledOnCommand("GETDEL") + void getAndDelete() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + valueOperations.set(key, value).as(StepVerifier::create).expectNext(true).verifyComplete(); + + valueOperations.getAndDelete(key).as(StepVerifier::create).expectNext(value).verifyComplete(); + + redisTemplate.hasKey(key).as(StepVerifier::create).expectNext(false).verifyComplete(); + } + + @ParameterizedRedisTest // GH-2050 + @EnabledOnCommand("GETEX") + void getAndPersist() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + valueOperations.set(key, value, Duration.ofSeconds(10)).as(StepVerifier::create).expectNext(true).verifyComplete(); + + valueOperations.getAndPersist(key).as(StepVerifier::create).expectNext(value).verifyComplete(); + + redisTemplate.getExpire(key).as(StepVerifier::create).expectNext(Duration.ZERO).verifyComplete(); + } + @ParameterizedRedisTest // DATAREDIS-602 void getAndSet() { diff --git a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java index 71b8c3a3db..42a0ceee34 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java @@ -32,6 +32,7 @@ import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.test.condition.EnabledIfLongRunningTest; +import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; @@ -214,6 +215,46 @@ void testGetSet() { assertThat(valueOps.get(key)).isEqualTo(value); } + @ParameterizedRedisTest // GH-2050 + @EnabledOnCommand("GETEX") + void testGetAndExpire() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + valueOps.set(key, value1); + + assertThat(valueOps.getAndExpire(key, Duration.ofSeconds(10))).isEqualTo(value1); + assertThat(redisTemplate.getExpire(key)).isGreaterThan(1); + } + + @ParameterizedRedisTest // GH-2050 + @EnabledOnCommand("GETEX") + void testGetAndPersist() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + + valueOps.set(key, value1, Duration.ofSeconds(10)); + + assertThat(valueOps.getAndPersist(key)).isEqualTo(value1); + assertThat(redisTemplate.getExpire(key)).isEqualTo(-1); + } + + @ParameterizedRedisTest // GH-2050 + @EnabledOnCommand("GETDEL") + void testGetAndDelete() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + + valueOps.set(key, value1); + + assertThat(valueOps.getAndDelete(key)).isEqualTo(value1); + assertThat(redisTemplate.hasKey(key)).isFalse(); + } + @ParameterizedRedisTest void testGetAndSet() {