From 44a2ca9914f5f85c1c0baa15bbc481bcf43079bf Mon Sep 17 00:00:00 2001 From: Yan Kardziyaka Date: Tue, 14 Nov 2023 03:27:09 +0300 Subject: [PATCH] Support KEEPTTL via ValueOperations. Closes #2084 --- .../core/DefaultReactiveValueOperations.java | 19 ++++ .../redis/core/DefaultValueOperations.java | 20 ++++ .../redis/core/ReactiveValueOperations.java | 20 ++++ .../data/redis/core/ValueOperations.java | 23 +++++ ...activeValueOperationsIntegrationTests.java | 96 +++++++++++++++++++ ...efaultValueOperationsIntegrationTests.java | 64 +++++++++++++ 6 files changed, 242 insertions(+) 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 dd59f5bd18..d25f5bf517 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java @@ -77,6 +77,15 @@ public Mono set(K key, V value, Duration timeout) { stringCommands.set(rawKey(key), rawValue(value), Expiration.from(timeout), SetOption.UPSERT)); } + @Override + public Mono set(K key, V value, boolean keepTtl) { + + Assert.notNull(key, "Key must not be null"); + + Expiration expiration = keepTtl ? Expiration.keepTtl() : Expiration.persistent(); + return createMono(stringCommands -> stringCommands.set(rawKey(key), rawValue(value), expiration, SetOption.UPSERT)); + } + @Override public Mono setIfAbsent(K key, V value) { @@ -115,6 +124,16 @@ public Mono setIfPresent(K key, V value, Duration timeout) { stringCommands.set(rawKey(key), rawValue(value), Expiration.from(timeout), SetOption.SET_IF_PRESENT)); } + @Override + public Mono setIfPresent(K key, V value, boolean keepTtl) { + + Assert.notNull(key, "Key must not be null"); + + Expiration expiration = keepTtl ? Expiration.keepTtl() : Expiration.persistent(); + return createMono( + stringCommands -> stringCommands.set(rawKey(key), rawValue(value), expiration, SetOption.SET_IF_PRESENT)); + } + @Override public Mono multiSet(Map map) { 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 149543027f..7e32ca73b7 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java @@ -281,6 +281,16 @@ private boolean failsafeInvokePsetEx(RedisConnection connection) { }); } + @Override + public void set(K key, V value, boolean keepTtl) { + + byte[] rawKey = rawKey(key); + byte[] rawValue = rawValue(value); + + Expiration expiration = keepTtl ? Expiration.keepTtl() : Expiration.persistent(); + execute(connection -> connection.set(rawKey, rawValue, expiration, SetOption.upsert())); + } + @Override public Boolean setIfAbsent(K key, V value) { @@ -320,6 +330,16 @@ public Boolean setIfPresent(K key, V value, long timeout, TimeUnit unit) { return execute(connection -> connection.set(rawKey, rawValue, expiration, SetOption.ifPresent())); } + @Override + public Boolean setIfPresent(K key, V value, boolean keepTtl) { + + byte[] rawKey = rawKey(key); + byte[] rawValue = rawValue(value); + + Expiration expiration = keepTtl ? Expiration.keepTtl() : Expiration.persistent(); + return execute(connection -> connection.set(rawKey, rawValue, expiration, SetOption.ifPresent())); + } + @Override public void set(K key, V value, long offset) { 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 be8a0c0fbe..9311520283 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java @@ -58,6 +58,16 @@ public interface ReactiveValueOperations { */ Mono set(K key, V value, Duration timeout); + /** + * Set {@code value} for {@code key}. + * + * @param key must not be {@literal null}. + * @param value + * @param keepTtl whether to retain TTL associated with the key. + * @see Redis Documentation: SET + */ + Mono set(K key, V value, boolean keepTtl); + /** * Set {@code key} to hold the string {@code value} if {@code key} is absent. * @@ -98,6 +108,16 @@ public interface ReactiveValueOperations { */ Mono setIfPresent(K key, V value, Duration timeout); + /** + * Set {@code key} to hold the string {@code value} if {@code key} is present. + * + * @param key must not be {@literal null}. + * @param value + * @param keepTtl whether to retain TTL associated with the key. + * @see Redis Documentation: SET + */ + Mono setIfPresent(K key, V value, boolean keepTtl); + /** * Set multiple keys to multiple values using key-value pairs provided in {@code tuple}. * 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 edd350457b..358ce04063 100644 --- a/src/main/java/org/springframework/data/redis/core/ValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ValueOperations.java @@ -76,6 +76,16 @@ default void set(K key, V value, Duration timeout) { } } + /** + * Set {@code value} for {@code key}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param keepTtl whether to retain TTL associated with the key. + * @see Redis Documentation: SET + */ + void set(K key, V value, boolean keepTtl); + /** * Set {@code key} to hold the string {@code value} if {@code key} is absent. * @@ -175,6 +185,19 @@ default Boolean setIfPresent(K key, V value, Duration timeout) { return setIfPresent(key, value, timeout.getSeconds(), TimeUnit.SECONDS); } + /** + * Set {@code key} to hold the string {@code value} if {@code key} is present. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param keepTtl whether to retain TTL associated with the key. + * @return command result indicating if the key has been set. + * @throws IllegalArgumentException if either {@code key} or {@code value} is not present. + * @see Redis Documentation: SET + */ + @Nullable + Boolean setIfPresent(K key, V value, boolean keepTtl); + /** * Set multiple keys to multiple values using key-value pairs provided in {@code tuple}. * 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 b3c8a29a2a..10109965c9 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java @@ -98,6 +98,50 @@ void set() { valueOperations.get(key).as(StepVerifier::create).expectNext(value).verifyComplete(); } + @ParameterizedRedisTest // GH-2084 + void setWithKeepTtl() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + valueOperations.set(key, value1, Duration.ofMillis(5500)).as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + valueOperations.set(key, value2, true).as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + valueOperations.get(key).as(StepVerifier::create) // + .expectNext(value2) // + .verifyComplete(); + redisTemplate.getExpire(key).as(StepVerifier::create) // + .assertNext(actual -> assertThat(actual).isBetween(Duration.ofMillis(1), Duration.ofSeconds(6))) // + .verifyComplete(); + } + + @ParameterizedRedisTest // GH-2084 + void setWithoutKeepTtl() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + valueOperations.set(key, value1, Duration.ofMillis(5500)).as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + valueOperations.set(key, value2, false).as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + valueOperations.get(key).as(StepVerifier::create) // + .expectNext(value2) // + .verifyComplete(); + redisTemplate.getExpire(key).as(StepVerifier::create) // + .assertNext(actual -> assertThat(actual).isZero()) // + .verifyComplete(); + } + @ParameterizedRedisTest // DATAREDIS-602 void setWithExpiry() { @@ -186,6 +230,58 @@ void setIfPresentWithExpiry() { }).verifyComplete(); } + @ParameterizedRedisTest // GH-2084 + void setIfPresentWithKeepTtl() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + valueOperations.setIfPresent(key, value1, true).as(StepVerifier::create) // + .expectNext(false) // + .verifyComplete(); + valueOperations.set(key, value1, Duration.ofMillis(5500)).as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + valueOperations.setIfPresent(key, value2, true).as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + valueOperations.get(key).as(StepVerifier::create) // + .expectNext(value2) // + .verifyComplete(); + redisTemplate.getExpire(key).as(StepVerifier::create) // + .assertNext(actual -> assertThat(actual).isBetween(Duration.ofMillis(1), Duration.ofSeconds(6))) // + .verifyComplete(); + } + + @ParameterizedRedisTest // GH-2084 + void setIfPresentWithoutKeepTtl() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + valueOperations.setIfPresent(key, value1, false).as(StepVerifier::create) // + .expectNext(false) // + .verifyComplete(); + valueOperations.set(key, value1, Duration.ofMillis(5500)).as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + valueOperations.setIfPresent(key, value2, false).as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + valueOperations.get(key).as(StepVerifier::create) // + .expectNext(value2) // + .verifyComplete(); + redisTemplate.getExpire(key).as(StepVerifier::create) // + .assertNext(actual -> assertThat(actual).isZero()) // + .verifyComplete(); + } + @ParameterizedRedisTest // DATAREDIS-602 void multiSet() { 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 93aa564ee7..c71e8f1579 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java @@ -316,6 +316,37 @@ void testSetWithExpirationWithTimeUnitMilliseconds() { await().atMost(Duration.ofMillis(500L)).until(() -> !redisTemplate.hasKey(key)); } + @ParameterizedRedisTest // GH-2084 + void testSetWithKeepTtl() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + valueOps.set(key, value1, Duration.ofMillis(5500)); + valueOps.set(key, value2, true); + + Long expire = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); + + assertThat(valueOps.get(key)).isEqualTo(value2); + assertThat(expire).isLessThan(TimeUnit.SECONDS.toMillis(6)); + assertThat(expire).isGreaterThan(TimeUnit.MILLISECONDS.toMillis(1)); + } + + @ParameterizedRedisTest // GH-2084 + void testSetWithoutKeepTtl() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + valueOps.set(key, value1, Duration.ofMillis(5500)); + valueOps.set(key, value2, false); + + assertThat(valueOps.get(key)).isEqualTo(value2); + assertThat(redisTemplate.getExpire(key)).isEqualTo(-1); + } + @ParameterizedRedisTest void testAppend() { @@ -483,6 +514,39 @@ void testSetIfPresentWithExpirationPX() { assertThat(valueOps.get(key)).isEqualTo(value2); } + @ParameterizedRedisTest // GH-2084 + void testSetIfPresentWithKeepTtl() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + assertThat(valueOps.setIfPresent(key, value1, true)).isFalse(); + valueOps.set(key, value1, Duration.ofMillis(5500)); + + Long expire = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); + + assertThat(valueOps.setIfPresent(key, value2, true)).isTrue(); + assertThat(valueOps.get(key)).isEqualTo(value2); + assertThat(expire).isLessThan(TimeUnit.SECONDS.toMillis(6)); + assertThat(expire).isGreaterThan(TimeUnit.MILLISECONDS.toMillis(1)); + } + + @ParameterizedRedisTest // GH-2084 + void testSetIfPresentWithoutKeepTtl() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + assertThat(valueOps.setIfPresent(key, value1, false)).isFalse(); + valueOps.set(key, value1, Duration.ofMillis(5500)); + + assertThat(valueOps.setIfPresent(key, value2, false)).isTrue(); + assertThat(valueOps.get(key)).isEqualTo(value2); + assertThat(redisTemplate.getExpire(key)).isEqualTo(-1); + } + @ParameterizedRedisTest void testSize() {