-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Support TTI expiration in Redis Cache implementation #2649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
We now support time-to-idle (TTI) expiration policies for cache reads. The TTI implementation is achieved with the use of the Redis GETEX command on Cache.get(key) operations as well as consistently using the same TTL configuration for all cache operations when TTI is enabled and TTL expiration has been configured, with the use of a TtlFunction or fixed Duration. Closes #2351 Original pull request: #2643
|
||
Assert.notNull(name, "Name must not be null"); | ||
Assert.notNull(key, "Key must not be null"); | ||
|
||
byte[] result = execute(name, connection -> connection.get(key)); | ||
byte[] result = shouldExpireWithin(ttl) | ||
? execute(name, connection -> connection.getEx(key, Expiration.from(ttl))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we fallback to get
and expire
if server doesn't support getex
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the feedback, @quaff.
I spoke with @mp911de about this earlier today and we decided this feature will only be included from Spring Data Redis 3.2
and later, which is based on the latest Jedis and Lettuce drivers supporting most of the features in the newest Redis server releases.
In addition, it is an opt-in feature. See here and here, defaulting to the previous behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
which is based on the latest Jedis and Lettuce drivers supporting most of the features in the newest Redis server releases.
I mean server not client doesn't support getex
, it's a new command since redis 6.2.0, upgrading server is not convenient as client, many server still using 4.x and 5.x, especially service provided by private cloud.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the server does not support getex
then you cannot use Time to Idle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could fallback to get
and expire
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@quaff - I understood what you meant between client and server versions.
@christophstrobl and I are meeting this week on the topic of caching in SD Redis and using TTI-like expiration. Let me touch base with Christoph on this whether or not we will take into consideration Redis server version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work, I left a few comments we should consider regarding server versions and naming.
@@ -47,9 +47,11 @@ | |||
public class RedisCacheConfiguration { | |||
|
|||
protected static final boolean DEFAULT_CACHE_NULL_VALUES = true; | |||
protected static final boolean DEFAULT_ENABLE_TTI_EXPIRATION = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use TIME_TO_IDLE
instead of TTI
for more expressivity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do.
Ironically, I initially spelled it out in favor of being explicit, but ultimately opted to match our use of the "TTL" abbreviation. It is definitely easy to overlook and possibly confuse "TTI" with "TTL" on a brief glance.
protected static final boolean DEFAULT_USE_PREFIX = true; | ||
protected static final boolean DO_NOT_CACHE_NULL_VALUES = false; | ||
protected static final boolean DO_NOT_USE_PREFIX = false; | ||
protected static final boolean ENABLE_IDLE_TIME_EXPIRATION = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@quaff has a good point regarding server versions. We cannot assume the application runs on the latest Redis version. If we'd enabled this for everyone, then all users with older Redis versions suddenly see failures on their target environments unless they reconfigure the cache.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did similar work before, like this:
private volatile boolean getexSupported = true;
@Override
public Object getWithTti(String key, int timeToIdle, TimeUnit timeUnit) {
BoundValueOperations operations = redisTemplate.boundValueOps(key);
try {
if (getexSupported)
return timeToIdle > 0 ? operations.getAndExpire(timeToIdle, timeUnit) : operations.getAndPersist();
// fallback to get and expire
Object result = operations.get();
if (result != null && timeToIdle > 0)
operations.expire(timeToIdle, timeUnit);
return result;
} catch (RuntimeException e) {
log.error(e.getMessage(), e);
if (e.getCause() instanceof RedisCommandExecutionException) {
getexSupported = false; // server.version < 6.2
return getWithTti(key, timeToIdle, timeUnit);
}
throw e;
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please note, the ENABLE_IDLE_TIME_EXPIRATION
constant was meant to express the preferred use of time-to-idle (TTI) expiration when "explicitly enabled" and is NOT a flag defaulting TTI to true
. The "default" value for TTI is expressed constant DEFAULT_ENABLE_TTI_EXPIRATION
, here.
I prefer to use "named" constants rather than "true
", especially when calling constructors or methods accepting (multiple) boolean-typed arguments in order to make it clear "what" is actually being enabled. For instance.
This similar to the existing constants for not caching null
values (disableCachingNullValues()
; here) or not using a cache key prefix, (disableKeyPrefix()
; here).
However, I realize the constant name (ENABLE_IDLE_TIME_EXPIRATION
) was a poor choice of words and I have changed this to USE_TIME_TO_IDLE_EXPIRATION
. This is now, not unlike the constants for caching null
values or using a cache key prefix.
Keep in mind, this constant (now USE_TIME_TO_IDLE_EXPIRATION
) is only ever used when TTI is enabled by calling the enableTtiExpiration()
method on the RedisCacheConfiguration
instance, here. This makes it very explicit as to what is being set (TTI) and how (use (enable) TTI).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@quaff - FYI, if @christophstrobl and I decide it makes sense to handle Redis server version for a (Redis) Cache.get(key)
, using either GETEX
or a GET
with EXPIRE
(although, I am not certain SD Redis takes Redis server version into consideration anywhere else, though), then I would envision something a bit more robust, such as by (proactively) inspecting the Redis server version on construction of the DefaultRedisCacheWriter
using the INFO
command from RedisServerCommands
given a RedisConnection
(retrieved from the factory).
CacheKeyPrefix.simple(), | ||
SerializationPair.fromSerializer(RedisSerializer.string()), | ||
SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), | ||
conversionService); | ||
} | ||
|
||
private final boolean cacheNullValues; | ||
private final boolean enableTtiExpiration; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's expand on the name to enableTimeToIdle
instead of Tti
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
I also renamed isTtiExpirationEnabled()
to isTimeToIdleEnabled()
and enableTtiExpiration()
to enableTimeToIdle()
.
* expiration are used consistently across the application, then in effect, an application can achieve | ||
* {@literal TTI} expiration-like behavior. | ||
* | ||
* @return this {@link RedisCacheConfiguration}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: @since 3.2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch!
* Defaults to {@literal false}. | ||
* @see <a href="https://redis.io/commands/getex/">GETEX</a> | ||
*/ | ||
public boolean isTtiExpirationEnabled() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: @since 3.2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -0,0 +1,99 @@ | |||
/* | |||
* Copyright 2017-2023 the original author or authors. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Copyright starts at 2023 (inception of the file).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -0,0 +1,56 @@ | |||
/* | |||
* Copyright 2017-2023 the original author or authors. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Copyright starts at 2023.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
* | ||
* @author John Blum | ||
*/ | ||
public class RedisCacheWriterUnitTests { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: JUnit 5 test classes can be package-private.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed
@@ -53,4 +56,38 @@ void fromMinutes() { | |||
assertThat(expiration.getExpirationTime()).isEqualTo(5L * 60); | |||
assertThat(expiration.getTimeUnit()).isEqualTo(TimeUnit.SECONDS); | |||
} | |||
|
|||
@Test | |||
public void equalValuedExpirationsAreEqual() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Test methods can be package-private.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes
import java.util.concurrent.TimeUnit; | ||
|
||
import org.junit.jupiter.api.Test; | ||
|
||
/** | ||
* Unit tests for {@link Expiration}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🥳
@@ -15,10 +15,10 @@ | |||
*/ | |||
package org.springframework.data.redis.cache; | |||
|
|||
import static org.assertj.core.api.Assertions.*; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import changes do not seem in line with the import order defined in spring-data-build
.
I am not sure why, but the GitHub PR did not pick up my latest set of changes, a polishing commit based on the review feedback above. |
See issue #2351.
See original pull request #2643.