I'm using Caffeine v2.8.5 and I want to create a cache with a variable expiry based on:
- the creation/update of the value and
- the last access (read) of this value.
Whatever comes first should trigger the removal of that entry.
The cache will be part of a three-layered resolution of values:
- The key is present in the Caffeine cache
- use this value
- refresh access/read expiry
- The key is present in the Redis database
- use this value
- store this value in the Caffeine cache with the remaining TTL (Time to live) of the Redis key
- The key was neither present in the internal cache nor Redis
- request the value from an external REST API
- store this value in the Redis database with a fixed expiration of 30 days
- store this value in the Caffeine cache with a fixed expiration of 30 days
Redis is used as a global cache, so that multiple applications/instances can share the cached data, but this resolution happens so often, that it cannot be used for every request, so another caching layer is necessary.
The requested data has varying TTLs, based on the time of request. So while the expiry time may be fixed when we request the REST API and that expiry is set in Redis, the time will be dynamic in Caffeine, as the expiry is based on the remaining TTL of the Redis Key.
Cases (2) and (3) are already solved within my CacheLoader for the Caffeine cache (I use the cache in read-through mode). To control the expiration I already found out, that I'll have to make use of the advanced Expiry API and I've also looked into similar issues like (Specify expiry for an Entry) and (Expire cached values after creation time). So I came up with a wrapper object for my keys like this:
import lombok.Value;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Instant;
@Value
public class ExpiringValue<ValueType> {
@Nullable
private final ValueType value;
@NotNull
private final Instant validUntil;
}
and an Expiry like this:
import com.github.benmanes.caffeine.cache.Expiry;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.time.Instant;
public final class ValueBasedExpiry<KeyType, ValueType extends ExpiringValue<?>> implements Expiry<KeyType, ValueType> {
@Override
public long expireAfterCreate(
@NotNull final KeyType key,
@NotNull final ValueType value,
final long currentTime
) {
return Duration.between(Instant.now(), value.getValidUntil()).toNanos();
}
@Override
public long expireAfterUpdate(
@NotNull final KeyType key,
@NotNull final ValueType value,
final long currentTime,
final long currentDuration
) {
return currentDuration;
}
@Override
public long expireAfterRead(
@NotNull final KeyType key,
@NotNull final ValueType value,
final long currentTime,
final long currentDuration
) {
return currentDuration;
}
}
What's different in my use case is, that I'd like to have a second expiry criterion based on the last access of the value. So I'd like to remove the entry early, if it has not been requested for an hour. And if it is frequently accessed, it will be eventually removed after the TTL reaches zero.
How would I implement this second criterion? I don't know how I would get the last time that an entry was accessed. The interface does not seem to provide such a value. I also looked into this question. Is it correct that the methods will be called/re-evaluated periodically, based on the scheduler bucket, that the entry has been sorted into?
expireAfterRead
that stores the last access as another timestamp onExpiringValue
and returns the minimum duration betweenvalidUntil
andlastAccessDuration
. Then the entry has a 1 hr TTL by access time, up until it becomes invalid due to Redis TTL. Or am I missing something? – Ben ManesExpiringValue
withinexpireAfterRead
to maintain your own state for calculations. The cache only stores one timestamp, which the 3 methods assist in calculating. Since you know the max valid time, maybe you can simply calculate the window asMath.min(TimeUnit.HOURS.toNanos(1), Duration.between(Instant.now(), value.getValidUntil()).toNanos())
? – Ben Manes