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)); + } }