Introduce BoundKeyExpirationOperations.
And rename entry points for Hash Field expiration from expiration to hashFieldExpiration. Original Pull Request: #3115
This commit is contained in:
committed by
Christoph Strobl
parent
b8d289208e
commit
e4009f47c4
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2025 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.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.data.redis.connection.ExpirationOptions;
|
||||
import org.springframework.data.redis.core.types.Expiration;
|
||||
import org.springframework.data.redis.core.types.Expirations;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Key Expiration operations bound to a key.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.5
|
||||
*/
|
||||
public interface BoundKeyExpirationOperations {
|
||||
|
||||
/**
|
||||
* Apply {@link Expiration} to the bound key without any additional constraints.
|
||||
*
|
||||
* @param expiration the expiration definition.
|
||||
* @return changes to the key. {@literal null} when used in pipeline / transaction.
|
||||
*/
|
||||
default ExpireChanges.ExpiryChangeState expire(Expiration expiration) {
|
||||
return expire(expiration, ExpirationOptions.none());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply {@link Expiration} to the bound key given {@link ExpirationOptions expiration options}.
|
||||
*
|
||||
* @param expiration the expiration definition.
|
||||
* @param options expiration options.
|
||||
* @return changes to the key. {@literal null} when used in pipeline / transaction.
|
||||
*/
|
||||
@Nullable
|
||||
ExpireChanges.ExpiryChangeState expire(Expiration expiration, ExpirationOptions options);
|
||||
|
||||
/**
|
||||
* Set time to live for the bound key.
|
||||
*
|
||||
* @param timeout the amount of time after which the key will be expired, must not be {@literal null}.
|
||||
* @return changes to the key. {@literal null} when used in pipeline / transaction.
|
||||
* @throws IllegalArgumentException if the timeout is {@literal null}.
|
||||
* @see <a href="https://redis.io/docs/latest/commands/expire/">Redis Documentation: EXPIRE</a>
|
||||
* @since 3.5
|
||||
*/
|
||||
@Nullable
|
||||
ExpireChanges.ExpiryChangeState expire(Duration timeout);
|
||||
|
||||
/**
|
||||
* Set the expiration for the bound key as a {@literal date} timestamp.
|
||||
*
|
||||
* @param expireAt must not be {@literal null}.
|
||||
* @return changes to the key. {@literal null} when used in pipeline / transaction.
|
||||
* @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}.
|
||||
* @see <a href="https://redis.io/docs/latest/commands/expireat/">Redis Documentation: EXPIRE</a>
|
||||
* @since 3.5
|
||||
*/
|
||||
@Nullable
|
||||
ExpireChanges.ExpiryChangeState expireAt(Instant expireAt);
|
||||
|
||||
/**
|
||||
* Remove the expiration from the bound key.
|
||||
*
|
||||
* @return changes to the key. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="https://redis.io/docs/latest/commands/persist/">Redis Documentation: PERSIST</a>
|
||||
* @since 3.5
|
||||
*/
|
||||
@Nullable
|
||||
ExpireChanges.ExpiryChangeState persist();
|
||||
|
||||
/**
|
||||
* Get the time to live for the bound key in seconds.
|
||||
*
|
||||
* @return the actual expirations in seconds for the key. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="https://redis.io/docs/latest/commands/ttl/">Redis Documentation: TTL</a>
|
||||
* @since 3.5
|
||||
*/
|
||||
@Nullable
|
||||
Expirations.TimeToLive getTimeToLive();
|
||||
|
||||
/**
|
||||
* Get the time to live for the bound key and convert it to the given {@link TimeUnit}.
|
||||
*
|
||||
* @param timeUnit must not be {@literal null}.
|
||||
* @return the actual expirations for the key in the given time unit. {@literal null} when used in pipeline /
|
||||
* transaction.
|
||||
* @see <a href="https://redis.io/docs/latest/commands/ttl/">Redis Documentation: TTL</a>
|
||||
* @since 3.5
|
||||
*/
|
||||
@Nullable
|
||||
Expirations.TimeToLive getTimeToLive(TimeUnit timeUnit);
|
||||
|
||||
}
|
||||
@@ -51,6 +51,16 @@ public interface BoundKeyOperations<K> {
|
||||
@Nullable
|
||||
DataType getType();
|
||||
|
||||
/**
|
||||
* Returns a bound operations object to perform expiration operations on the bound key.
|
||||
*
|
||||
* @return the bound operations object to perform operations on the hash field expiration.
|
||||
* @since 3.5
|
||||
*/
|
||||
default BoundKeyExpirationOperations expiration() {
|
||||
return new DefaultBoundKeyExpirationOperations<>(getOperations(), getKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expiration of this key.
|
||||
*
|
||||
@@ -127,4 +137,10 @@ public interface BoundKeyOperations<K> {
|
||||
* @param newKey new key. Must not be {@literal null}.
|
||||
*/
|
||||
void rename(K newKey);
|
||||
|
||||
/**
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
RedisOperations<K, ?> getOperations();
|
||||
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ class BoundOperationsProxyFactory {
|
||||
delegate.rename(invocation.getArguments()[0]);
|
||||
yield null;
|
||||
}
|
||||
case "getOperations" -> delegate.getOps();
|
||||
case "getOperations" -> delegate.getOperations();
|
||||
default ->
|
||||
method.getDeclaringClass() == boundOperationsInterface ? doInvoke(invocation, method, operationsTarget, true)
|
||||
: doInvoke(invocation, method, delegate, false);
|
||||
@@ -234,12 +234,15 @@ class BoundOperationsProxyFactory {
|
||||
key = newKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public RedisOperations<Object, ?> getOps() {
|
||||
@Override
|
||||
public RedisOperations<Object, ?> getOperations() {
|
||||
return ops;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2025 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.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.data.redis.connection.ExpirationOptions;
|
||||
import org.springframework.data.redis.core.types.Expiration;
|
||||
import org.springframework.data.redis.core.types.Expirations;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Default {@link BoundKeyExpirationOperations} implementation.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.5
|
||||
*/
|
||||
class DefaultBoundKeyExpirationOperations<K> implements BoundKeyExpirationOperations {
|
||||
|
||||
private final RedisOperations<K, ?> operations;
|
||||
private final K key;
|
||||
|
||||
public DefaultBoundKeyExpirationOperations(RedisOperations<K, ?> operations, K key) {
|
||||
this.operations = operations;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ExpireChanges.ExpiryChangeState expire(Expiration expiration, ExpirationOptions options) {
|
||||
return operations.expire(key, expiration, options);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ExpireChanges.ExpiryChangeState expire(Duration timeout) {
|
||||
|
||||
Boolean expire = operations.expire(key, timeout);
|
||||
|
||||
return toExpiryChangeState(expire);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ExpireChanges.ExpiryChangeState expireAt(Instant expireAt) {
|
||||
return toExpiryChangeState(operations.expireAt(key, expireAt));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ExpireChanges.ExpiryChangeState persist() {
|
||||
return toExpiryChangeState(operations.persist(key));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Expirations.TimeToLive getTimeToLive() {
|
||||
|
||||
Long expire = operations.getExpire(key);
|
||||
|
||||
return expire == null ? null : Expirations.TimeToLive.of(expire, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Expirations.TimeToLive getTimeToLive(TimeUnit timeUnit) {
|
||||
|
||||
Long expire = operations.getExpire(key, timeUnit);
|
||||
|
||||
return expire == null ? null : Expirations.TimeToLive.of(expire, timeUnit);
|
||||
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ExpireChanges.ExpiryChangeState toExpiryChangeState(@Nullable Boolean result) {
|
||||
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ExpireChanges.ExpiryChangeState.of(result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -386,6 +386,16 @@ public interface RedisOperations<K, V> {
|
||||
@Nullable
|
||||
ExpireChanges.ExpiryChangeState expire(K key, Expiration expiration, ExpirationOptions options);
|
||||
|
||||
/**
|
||||
* Returns a bound operations object to perform expiration operations on the bound key.
|
||||
*
|
||||
* @return the bound operations object to perform operations on the hash field expiration.
|
||||
* @since 3.5
|
||||
*/
|
||||
default BoundKeyExpirationOperations expiration(K key) {
|
||||
return new DefaultBoundKeyExpirationOperations<>(this, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the expiration from given {@code key}.
|
||||
*
|
||||
|
||||
@@ -399,6 +399,11 @@ public class RedisAtomicDouble extends Number implements Serializable, BoundKeyO
|
||||
key = newKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedisOperations<String, ?> getOperations() {
|
||||
return generalOps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int intValue() {
|
||||
return (int) get();
|
||||
@@ -418,4 +423,5 @@ public class RedisAtomicDouble extends Number implements Serializable, BoundKeyO
|
||||
public double doubleValue() {
|
||||
return get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -399,6 +399,11 @@ public class RedisAtomicInteger extends Number implements Serializable, BoundKey
|
||||
key = newKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedisOperations<String, ?> getOperations() {
|
||||
return generalOps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int intValue() {
|
||||
return get();
|
||||
|
||||
@@ -396,6 +396,11 @@ public class RedisAtomicLong extends Number implements Serializable, BoundKeyOpe
|
||||
key = newKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedisOperations<String, ?> getOperations() {
|
||||
return generalOps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int intValue() {
|
||||
return (int) get();
|
||||
@@ -415,4 +420,5 @@ public class RedisAtomicLong extends Number implements Serializable, BoundKeyOpe
|
||||
public double doubleValue() {
|
||||
return get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.springframework.data.redis.ObjectFactory;
|
||||
import org.springframework.data.redis.Person;
|
||||
import org.springframework.data.redis.SettingsUtils;
|
||||
import org.springframework.data.redis.connection.DataType;
|
||||
import org.springframework.data.redis.connection.ExpirationOptions;
|
||||
import org.springframework.data.redis.connection.RedisClusterConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.StringRedisConnection;
|
||||
@@ -42,11 +43,13 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
|
||||
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
|
||||
import org.springframework.data.redis.core.query.SortQueryBuilder;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.data.redis.core.types.Expiration;
|
||||
import org.springframework.data.redis.serializer.GenericToStringSerializer;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.data.redis.test.condition.EnabledIfLongRunningTest;
|
||||
import org.springframework.data.redis.test.condition.EnabledOnCommand;
|
||||
import org.springframework.data.redis.test.extension.LettuceTestClientResources;
|
||||
import org.springframework.data.redis.test.extension.parametrized.MethodSource;
|
||||
import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest;
|
||||
@@ -503,6 +506,46 @@ public class RedisTemplateIntegrationTests<K, V> {
|
||||
assertThat(redisTemplate.getExpire(key1, TimeUnit.MILLISECONDS)).isGreaterThan(0L);
|
||||
}
|
||||
|
||||
@ParameterizedRedisTest // GH-3114
|
||||
@EnabledOnCommand("SPUBLISH") // Redis 7.0
|
||||
void testBoundExpireAndGetExpireSeconds() {
|
||||
|
||||
K key = keyFactory.instance();
|
||||
V value1 = valueFactory.instance();
|
||||
redisTemplate.boundValueOps(key).set(value1);
|
||||
|
||||
BoundKeyExpirationOperations exp = redisTemplate.expiration(key);
|
||||
|
||||
assertThat(exp.expire(Duration.ofSeconds(5))).isEqualTo(ExpireChanges.ExpiryChangeState.OK);
|
||||
|
||||
assertThat(exp.getTimeToLive(TimeUnit.SECONDS)).satisfies(ttl -> {
|
||||
assertThat(ttl.isPersistent()).isFalse();
|
||||
assertThat(ttl.value()).isGreaterThan(1);
|
||||
});
|
||||
}
|
||||
|
||||
@ParameterizedRedisTest // GH-3114
|
||||
@EnabledOnCommand("SPUBLISH") // Redis 7.0
|
||||
void testBoundExpireWithConditionsAndGetExpireSeconds() {
|
||||
|
||||
K key = keyFactory.instance();
|
||||
V value1 = valueFactory.instance();
|
||||
redisTemplate.boundValueOps(key).set(value1);
|
||||
|
||||
BoundKeyExpirationOperations exp = redisTemplate.expiration(key);
|
||||
|
||||
assertThat(exp.expire(Duration.ofSeconds(5))).isEqualTo(ExpireChanges.ExpiryChangeState.OK);
|
||||
assertThat(exp.expire(Expiration.from(Duration.ofSeconds(1)), ExpirationOptions.builder().gt().build()))
|
||||
.isEqualTo(ExpireChanges.ExpiryChangeState.CONDITION_NOT_MET);
|
||||
assertThat(exp.expire(Expiration.from(Duration.ofSeconds(10)), ExpirationOptions.builder().gt().build()))
|
||||
.isEqualTo(ExpireChanges.ExpiryChangeState.OK);
|
||||
|
||||
assertThat(exp.getTimeToLive(TimeUnit.SECONDS)).satisfies(ttl -> {
|
||||
assertThat(ttl.isPersistent()).isFalse();
|
||||
assertThat(ttl.value()).isGreaterThan(5);
|
||||
});
|
||||
}
|
||||
|
||||
@ParameterizedRedisTest
|
||||
void testGetExpireNoTimeUnit() {
|
||||
K key1 = keyFactory.instance();
|
||||
|
||||
Reference in New Issue
Block a user