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 e1f39e186..febb10387 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 @@ package org.springframework.data.redis.connection;
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;
@@ -58,10 +59,13 @@ public interface RedisStringCommands {
/**
* 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}.
- * @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
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 a71fdeee0..10f0d21c0 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
@@ -443,12 +443,13 @@ public abstract class JedisConverters extends Converters {
}
/**
- * Converts a given {@link Expiration} to the according {@code GETEX} command argument.
+ * Converts a given {@link Expiration} to the according {@code GETEX} command argument depending on
+ * {@link Expiration#isUnixTimestamp()}.
*
* - {@link TimeUnit#MILLISECONDS}
- * - {@code PX}
+ * - {@code PX|PXAT}
* - {@link TimeUnit#SECONDS}
- * - {@code EX}
+ * - {@code EX|EXAT}
*
*
* @param expiration must not be {@literal null}.
@@ -464,10 +465,14 @@ public abstract class JedisConverters extends Converters {
}
if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) {
+ if (expiration.isUnixTimestamp()) {
+ return params.pxAt(expiration.getExpirationTime());
+ }
return params.px(expiration.getExpirationTime());
}
- return params.ex((int) expiration.getExpirationTime());
+ 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/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java
index b31297d4b..79687c4c4 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
@@ -728,32 +728,31 @@ public abstract class LettuceConverters extends Converters {
/**
* Convert {@link Expiration} to {@link GetExArgs}.
*
- * @param expiration
+ * @param expiration can be {@literal null}.
* @return
* @since 2.6
*/
- static GetExArgs toGetExArgs(Expiration expiration) {
+ static GetExArgs toGetExArgs(@Nullable Expiration expiration) {
GetExArgs args = new GetExArgs();
- if (expiration != null) {
-
- if (expiration.isPersistent()) {
- args.persist();
- } else if (!expiration.isPersistent()) {
-
- switch (expiration.getTimeUnit()) {
- case SECONDS:
- args.ex(expiration.getExpirationTime());
- break;
- default:
- args.px(expiration.getConverted(TimeUnit.MILLISECONDS));
- break;
- }
- }
+ if (expiration == null) {
+ return args;
}
- 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) {
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 ab9068e3a..cba082ad6 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 class Expiration {
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 class Expiration {
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 class Expiration {
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 3fc7adbfb..cacafc14f 100644
--- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java
+++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java
@@ -1138,6 +1138,7 @@ public abstract class AbstractConnectionIntegrationTests {
@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"));
@@ -1150,6 +1151,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test // GH-2050
@EnabledOnCommand("GETDEL")
void testGetDel() {
+
actual.add(connection.set("testGS", "1"));
actual.add(connection.getDel("testGS"));
actual.add(connection.exists("testGS"));
@@ -1159,6 +1161,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
void testGetSet() {
+
actual.add(connection.set("testGS", "1"));
actual.add(connection.getSet("testGS", "2"));
actual.add(connection.get("testGS"));
@@ -1167,6 +1170,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
void testMSet() {
+
Map vals = new HashMap<>();
vals.put("color", "orange");
vals.put("size", "1");
@@ -1178,6 +1182,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
void testMSetNx() {
+
Map vals = new HashMap<>();
vals.put("height", "5");
vals.put("width", "1");
@@ -1188,6 +1193,7 @@ public abstract class AbstractConnectionIntegrationTests {
@Test
void testMSetNxFailure() {
+
actual.add(connection.set("height", "2"));
Map vals = new HashMap<>();
vals.put("height", "5");
@@ -1199,6 +1205,7 @@ public abstract class AbstractConnectionIntegrationTests {
@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/jedis/JedisConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java
index e16bb58d1..4bb426f4a 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 @@ package org.springframework.data.redis.connection.jedis;
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.Date;
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;
@@ -226,6 +228,54 @@ class JedisConvertersUnitTests {
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/LettuceCommandArgsComparator.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceCommandArgsComparator.java
new file mode 100644
index 000000000..5c1e5c028
--- /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 ebc32e6ab..a4354405e 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 @@ package org.springframework.data.redis.connection.lettuce;
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,9 +31,9 @@ import java.util.Arrays;
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;
@@ -193,4 +195,45 @@ class LettuceConvertersUnitTests {
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));
+ }
}