diff --git a/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java b/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java index 4ad5201ee..bd72aac17 100644 --- a/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java +++ b/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java @@ -15,15 +15,26 @@ */ package org.springframework.data.redis.core; +import java.time.Duration; import java.util.concurrent.TimeUnit; /** * Helper class featuring methods for calculating Redis timeouts * * @author Jennifer Hickey + * @author Mark Paluch */ abstract public class TimeoutUtils { + /** + * @param duration the actual {@link Duration} to inspect. + * @return {@literal true} if the {@link Duration} contains millisecond information. + * @since 2.1 + */ + static boolean hasMillis(Duration duration) { + return duration.toMillis() % 1000 != 0; + } + /** * Converts the given timeout to seconds. *

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 24f049dba..5f045cfff 100644 --- a/src/main/java/org/springframework/data/redis/core/ValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ValueOperations.java @@ -15,12 +15,14 @@ */ package org.springframework.data.redis.core; +import java.time.Duration; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Redis operations for simple (or in Redis terminology 'string') values. @@ -52,6 +54,26 @@ public interface ValueOperations { */ void set(K key, V value, long timeout, TimeUnit unit); + /** + * Set the {@code value} and expiration {@code timeout} for {@code key}. + * + * @param key must not be {@literal null}. + * @param value + * @param timeout must not be {@literal null}. + * @since 2.1 + * @see Redis Documentation: SETEX + */ + default void set(K key, V value, Duration timeout) { + + Assert.notNull(timeout, "Timeout must not be null!"); + + if (TimeoutUtils.hasMillis(timeout)) { + set(key, value, timeout.toMillis(), TimeUnit.MILLISECONDS); + } else { + set(key, value, timeout.getSeconds(), TimeUnit.SECONDS); + } + } + /** * Set {@code key} to hold the string {@code value} if {@code key} is absent. * @@ -77,6 +99,28 @@ public interface ValueOperations { @Nullable Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit); + /** + * Set {@code key} to hold the string {@code value} and expiration {@code timeout} if {@code key} is absent. + * + * @param key must not be {@literal null}. + * @param value + * @param timeout must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.1 + * @see Redis Documentation: SETEX + */ + @Nullable + default Boolean setIfAbsent(K key, V value, Duration timeout) { + + Assert.notNull(timeout, "Timeout must not be null!"); + + if (TimeoutUtils.hasMillis(timeout)) { + return setIfAbsent(key, value, timeout.toMillis(), TimeUnit.MILLISECONDS); + } + + return setIfAbsent(key, value, timeout.getSeconds(), TimeUnit.SECONDS); + } + /** * Set {@code key} to hold the string {@code value} if {@code key} is present. * @@ -105,6 +149,28 @@ public interface ValueOperations { @Nullable Boolean setIfPresent(K key, V value, long timeout, TimeUnit unit); + /** + * Set {@code key} to hold the string {@code value} and expiration {@code timeout} if {@code key} is present. + * + * @param key must not be {@literal null}. + * @param value + * @param timeout must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.1 + * @see Redis Documentation: SETEX + */ + @Nullable + default Boolean setIfPresent(K key, V value, Duration timeout) { + + Assert.notNull(timeout, "Timeout must not be null!"); + + if (TimeoutUtils.hasMillis(timeout)) { + return setIfPresent(key, value, timeout.toMillis(), TimeUnit.MILLISECONDS); + } + + return setIfPresent(key, value, timeout.getSeconds(), TimeUnit.SECONDS); + } + /** * 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/DefaultValueOperationsTests.java b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsTests.java index fef2f75b6..3db37062e 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsTests.java @@ -22,6 +22,7 @@ import static org.springframework.data.redis.SpinBarrier.*; import static org.springframework.data.redis.matcher.RedisTestMatchers.*; import java.text.DecimalFormat; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -252,14 +253,43 @@ public class DefaultValueOperationsTests { @Test public void testSetWithExpiration() { - assumeTrue(RedisTestProfileValueSource.matches("runLongTests", "true")); + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + valueOps.set(key, value, 5, TimeUnit.SECONDS); + + Long expire = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); + + assertThat(expire, is(lessThan(TimeUnit.SECONDS.toMillis(6)))); + assertThat(expire, is(greaterThan(TimeUnit.MILLISECONDS.toMillis(1)))); + } + + @Test // DATAREDIS-815 + public void testSetWithExpirationEX() { K key = keyFactory.instance(); V value = valueFactory.instance(); - valueOps.set(key, value, 1, TimeUnit.SECONDS); + valueOps.set(key, value, Duration.ofSeconds(5)); - waitFor(() -> (!redisTemplate.hasKey(key)), 1000); + Long expire = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); + + assertThat(expire, is(lessThan(TimeUnit.SECONDS.toMillis(6)))); + assertThat(expire, is(greaterThan(TimeUnit.MILLISECONDS.toMillis(1)))); + } + + @Test // DATAREDIS-815 + public void testSetWithExpirationPX() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + valueOps.set(key, value, Duration.ofMillis(5500)); + + Long expire = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); + + assertThat(expire, is(lessThan(TimeUnit.SECONDS.toMillis(6)))); + assertThat(expire, is(greaterThan(TimeUnit.MILLISECONDS.toMillis(1)))); } @Test // DATAREDIS-271 @@ -344,6 +374,38 @@ public class DefaultValueOperationsTests { assertThat(expire, is(greaterThan(TimeUnit.MILLISECONDS.toMillis(1)))); } + @Test // DATAREDIS-815 + public void testSetIfAbsentWithExpirationEX() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + assertTrue(valueOps.setIfAbsent(key, value1, Duration.ofSeconds(5))); + assertFalse(valueOps.setIfAbsent(key, value2, Duration.ofSeconds(5))); + + Long expire = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); + + assertThat(expire, is(lessThan(TimeUnit.SECONDS.toMillis(6)))); + assertThat(expire, is(greaterThan(TimeUnit.MILLISECONDS.toMillis(1)))); + } + + @Test // DATAREDIS-815 + public void testSetIfAbsentWithExpirationPX() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + assertTrue(valueOps.setIfAbsent(key, value1, Duration.ofMillis(5500))); + assertFalse(valueOps.setIfAbsent(key, value2, Duration.ofMillis(5500))); + + Long expire = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); + + assertThat(expire, is(lessThan(TimeUnit.SECONDS.toMillis(6)))); + assertThat(expire, is(greaterThan(TimeUnit.MILLISECONDS.toMillis(1)))); + } + @Test // DATAREDIS-786 public void setIfPresentReturnsTrueWhenKeyExists() { @@ -380,6 +442,42 @@ public class DefaultValueOperationsTests { assertThat(valueOps.get(key), is(value2)); } + @Test // DATAREDIS-815 + public void testSetIfPresentWithExpirationEX() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + assertFalse(valueOps.setIfPresent(key, value1, Duration.ofSeconds(5))); + valueOps.set(key, value1); + + assertTrue(valueOps.setIfPresent(key, value2, Duration.ofSeconds(5))); + + Long expire = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); + assertThat(expire, is(lessThan(TimeUnit.SECONDS.toMillis(6)))); + assertThat(expire, is(greaterThan(TimeUnit.MILLISECONDS.toMillis(1)))); + assertThat(valueOps.get(key), is(value2)); + } + + @Test // DATAREDIS-815 + public void testSetIfPresentWithExpirationPX() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + assertFalse(valueOps.setIfPresent(key, value1, Duration.ofMillis(5500))); + valueOps.set(key, value1); + + assertTrue(valueOps.setIfPresent(key, value2, Duration.ofMillis(5500))); + + Long expire = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); + assertThat(expire, is(lessThan(TimeUnit.SECONDS.toMillis(6)))); + assertThat(expire, is(greaterThan(TimeUnit.MILLISECONDS.toMillis(1)))); + assertThat(valueOps.get(key), is(value2)); + } + @Test public void testSize() {