Introduce BoundKeyExpirationOperations.

And rename entry points for Hash Field expiration from expiration to hashFieldExpiration.

Original Pull Request: #3115
This commit is contained in:
Mark Paluch
2025-03-05 16:51:40 +01:00
committed by Christoph Strobl
parent b8d289208e
commit e4009f47c4
9 changed files with 301 additions and 2 deletions

View File

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

View File

@@ -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();
}

View File

@@ -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;
}
}
}

View File

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

View File

@@ -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}.
*

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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();