Skip to content

Commit

Permalink
Add Expiry static factory methods (fixes #1499)
Browse files Browse the repository at this point in the history
  • Loading branch information
ben-manes committed Feb 19, 2024
1 parent f151394 commit d5c677f
Show file tree
Hide file tree
Showing 21 changed files with 317 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import static com.github.benmanes.caffeine.cache.Caffeine.calculateHashMapCapacity;
import static com.github.benmanes.caffeine.cache.Caffeine.ceilingPowerOfTwo;
import static com.github.benmanes.caffeine.cache.Caffeine.requireArgument;
import static com.github.benmanes.caffeine.cache.Caffeine.saturatedToNanos;
import static com.github.benmanes.caffeine.cache.Caffeine.toNanosSaturated;
import static com.github.benmanes.caffeine.cache.LocalLoadingCache.newBulkMappingFunction;
import static com.github.benmanes.caffeine.cache.LocalLoadingCache.newMappingFunction;
import static com.github.benmanes.caffeine.cache.Node.PROBATION;
Expand Down Expand Up @@ -4338,7 +4338,7 @@ final class BoundedVarExpiration implements VarExpiration<K, V> {
requireNonNull(remappingFunction);
requireArgument(!duration.isNegative(), "duration cannot be negative: %s", duration);
var expiry = new FixedExpireAfterWrite<K, V>(
saturatedToNanos(duration), TimeUnit.NANOSECONDS);
toNanosSaturated(duration), TimeUnit.NANOSECONDS);

return cache.isAsync
? computeAsync(key, remappingFunction, expiry)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@
public final class Caffeine<K, V> {
static final Supplier<StatsCounter> ENABLED_STATS_COUNTER_SUPPLIER = ConcurrentStatsCounter::new;
static final Logger logger = System.getLogger(Caffeine.class.getName());
static final Duration MIN_DURATION = Duration.ofNanos(Long.MIN_VALUE);
static final Duration MAX_DURATION = Duration.ofNanos(Long.MAX_VALUE);
static final double DEFAULT_LOAD_FACTOR = 0.75;

enum Strength { WEAK, SOFT }
Expand Down Expand Up @@ -604,16 +606,16 @@ public Caffeine<K, V> softValues() {
* described in the class javadoc. A {@link #scheduler(Scheduler)} may be configured for a prompt
* removal of expired entries.
*
* @param duration the length of time after an entry is created that it should be automatically
* removed
* @param duration the length of time after an entry is created or updated before it should be
* automatically removed
* @return this {@code Caffeine} instance (for chaining)
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the time to live or variable expiration was already set
* @throws ArithmeticException for durations greater than +/- approximately 292 years
*/
@CanIgnoreReturnValue
public Caffeine<K, V> expireAfterWrite(Duration duration) {
return expireAfterWrite(saturatedToNanos(duration), TimeUnit.NANOSECONDS);
return expireAfterWrite(toNanosSaturated(duration), TimeUnit.NANOSECONDS);
}

/**
Expand All @@ -628,8 +630,8 @@ public Caffeine<K, V> expireAfterWrite(Duration duration) {
* If you can represent the duration as a {@link java.time.Duration} (which should be preferred
* when feasible), use {@link #expireAfterWrite(Duration)} instead.
*
* @param duration the length of time after an entry is created that it should be automatically
* removed
* @param duration the length of time after an entry is created or updated before it should be
* automatically removed
* @param unit the unit that {@code duration} is expressed in
* @return this {@code Caffeine} instance (for chaining)
* @throws IllegalArgumentException if {@code duration} is negative
Expand Down Expand Up @@ -665,7 +667,7 @@ boolean expiresAfterWrite() {
* described in the class javadoc. A {@link #scheduler(Scheduler)} may be configured for a prompt
* removal of expired entries.
*
* @param duration the length of time after an entry is last accessed that it should be
* @param duration the length of time after an entry is last accessed before it should be
* automatically removed
* @return this {@code Caffeine} instance (for chaining)
* @throws IllegalArgumentException if {@code duration} is negative
Expand All @@ -674,7 +676,7 @@ boolean expiresAfterWrite() {
*/
@CanIgnoreReturnValue
public Caffeine<K, V> expireAfterAccess(Duration duration) {
return expireAfterAccess(saturatedToNanos(duration), TimeUnit.NANOSECONDS);
return expireAfterAccess(toNanosSaturated(duration), TimeUnit.NANOSECONDS);
}

/**
Expand All @@ -692,7 +694,7 @@ public Caffeine<K, V> expireAfterAccess(Duration duration) {
* If you can represent the duration as a {@link java.time.Duration} (which should be preferred
* when feasible), use {@link #expireAfterAccess(Duration)} instead.
*
* @param duration the length of time after an entry is last accessed that it should be
* @param duration the length of time after an entry is last accessed before it should be
* automatically removed
* @param unit the unit that {@code duration} is expressed in
* @return this {@code Caffeine} instance (for chaining)
Expand Down Expand Up @@ -793,7 +795,7 @@ boolean expiresVariable() {
*/
@CanIgnoreReturnValue
public Caffeine<K, V> refreshAfterWrite(Duration duration) {
return refreshAfterWrite(saturatedToNanos(duration), TimeUnit.NANOSECONDS);
return refreshAfterWrite(toNanosSaturated(duration), TimeUnit.NANOSECONDS);
}

/**
Expand Down Expand Up @@ -1186,14 +1188,10 @@ void requireWeightWithWeigher() {
* {@link Long#MAX_VALUE} or {@link Long#MIN_VALUE}. This behavior can be useful when decomposing
* a duration in order to call a legacy API which requires a {@code long, TimeUnit} pair.
*/
static long saturatedToNanos(Duration duration) {
// Using a try/catch seems lazy, but the catch block will rarely get invoked (except for
// durations longer than approximately +/- 292 years).
try {
return duration.toNanos();
} catch (ArithmeticException tooBig) {
return duration.isNegative() ? Long.MIN_VALUE : Long.MAX_VALUE;
}
static long toNanosSaturated(Duration duration) {
return duration.isNegative()
? (duration.compareTo(MIN_DURATION) <= 0) ? Long.MIN_VALUE : duration.toNanos()
: (duration.compareTo(MAX_DURATION) >= 0) ? Long.MAX_VALUE : duration.toNanos();
}

/**
Expand Down
137 changes: 137 additions & 0 deletions caffeine/src/main/java/com/github/benmanes/caffeine/cache/Expiry.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,28 @@
*/
package com.github.benmanes.caffeine.cache;

import static com.github.benmanes.caffeine.cache.Caffeine.toNanosSaturated;
import static java.util.Objects.requireNonNull;

import java.io.Serializable;
import java.time.Duration;
import java.util.function.BiFunction;

import org.checkerframework.checker.index.qual.NonNegative;

import com.google.errorprone.annotations.CanIgnoreReturnValue;

/**
* Calculates when cache entries expire. A single expiration time is retained so that the lifetime
* of an entry may be extended or reduced by subsequent evaluations.
* <p>
* Usage example:
* <pre>{@code
* LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
* .expireAfter(Expiry.creating((Key key, Graph graph) ->
* Duration.between(Instant.now(), graph.createdOn().plusHours(5))))
* .build(key -> createExpensiveGraph(key));
* }</pre>
*
* @author [email protected] (Ben Manes)
*/
Expand Down Expand Up @@ -76,4 +93,124 @@ public interface Expiry<K, V> {
* @return the length of time before the entry expires, in nanoseconds
*/
long expireAfterRead(K key, V value, long currentTime, @NonNegative long currentDuration);

/**
* Returns an {@code Expiry} that specifies that the entry should be automatically removed from
* the cache once the duration has elapsed after the entry's creation. The expiration time is
* not modified when the entry is updated or read.
*
* <pre>{@code
* Expiry<Key, Graph> expiry = Expiry.creating((key, graph) ->
* Duration.between(Instant.now(), graph.createdOn().plusHours(5)));
* }</pre>
*
* @param function the function used to calculate the length of time after an entry is created
* before it should be automatically removed
* @return an {@code Expiry} instance with the specified expiry function
*/
static <K, V> Expiry<K, V> creating(BiFunction<K, V, Duration> function) {
return new ExpiryAfterCreate<>(function);
}

/**
* Returns an {@code Expiry} that specifies that the entry should be automatically removed from
* the cache once the duration has elapsed after the entry's creation or replacement of its value.
* The expiration time is not modified when the entry is read.
*
* <pre>{@code
* Expiry<Key, Graph> expiry = Expiry.writing((key, graph) ->
* Duration.between(Instant.now(), graph.modifiedOn().plusHours(5)));
* }</pre>
*
* @param function the function used to calculate the length of time after an entry is created
* or updated that it should be automatically removed
* @return an {@code Expiry} instance with the specified expiry function
*/
static <K, V> Expiry<K, V> writing(BiFunction<K, V, Duration> function) {
return new ExpiryAfterWrite<>(function);
}

/**
* Returns an {@code Expiry} that specifies that the entry should be automatically removed from
* the cache once the duration has elapsed after the entry's creation, replacement of its value,
* or after it was last read.
*
* <pre>{@code
* Expiry<Key, Graph> expiry = Expiry.writing((key, graph) ->
* graph.isDirected() ? Duration.ofHours(1) : Duration.ofHours(3));
* }</pre>
*
* @param function the function used to calculate the length of time after an entry last accessed
* that it should be automatically removed
* @return an {@code Expiry} instance with the specified expiry function
*/
static <K, V> Expiry<K, V> accessing(BiFunction<K, V, Duration> function) {
return new ExpiryAfterAccess<>(function);
}
}

final class ExpiryAfterCreate<K, V> implements Expiry<K, V>, Serializable {
private static final long serialVersionUID = 1L;

@SuppressWarnings("serial")
final BiFunction<K, V, Duration> function;

public ExpiryAfterCreate(BiFunction<K, V, Duration> calculator) {
this.function = requireNonNull(calculator);
}
@Override public long expireAfterCreate(K key, V value, long currentTime) {
return toNanosSaturated(function.apply(key, value));
}
@CanIgnoreReturnValue
@Override public long expireAfterUpdate(K key, V value,
long currentTime, long currentDuration) {
return currentDuration;
}
@CanIgnoreReturnValue
@Override public long expireAfterRead(K key, V value, long currentTime, long currentDuration) {
return currentDuration;
}
}

final class ExpiryAfterWrite<K, V> implements Expiry<K, V>, Serializable {
private static final long serialVersionUID = 1L;

@SuppressWarnings("serial")
final BiFunction<K, V, Duration> function;

public ExpiryAfterWrite(BiFunction<K, V, Duration> calculator) {
this.function = requireNonNull(calculator);
}
@Override public long expireAfterCreate(K key, V value, long currentTime) {
return toNanosSaturated(function.apply(key, value));
}
@Override public long expireAfterUpdate(K key, V value,
long currentTime, long currentDuration) {
return toNanosSaturated(function.apply(key, value));
}
@CanIgnoreReturnValue
@Override public long expireAfterRead(K key, V value, long currentTime, long currentDuration) {
return currentDuration;
}
}

final class ExpiryAfterAccess<K, V> implements Expiry<K, V>, Serializable {
private static final long serialVersionUID = 1L;

@SuppressWarnings("serial")
final BiFunction<K, V, Duration> function;

public ExpiryAfterAccess(BiFunction<K, V, Duration> calculator) {
this.function = requireNonNull(calculator);
}
@Override public long expireAfterCreate(K key, V value, long currentTime) {
return toNanosSaturated(function.apply(key, value));
}
@Override public long expireAfterUpdate(K key, V value,
long currentTime, long currentDuration) {
return toNanosSaturated(function.apply(key, value));
}
@Override public long expireAfterRead(K key, V value, long currentTime, long currentDuration) {
return toNanosSaturated(function.apply(key, value));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
package com.github.benmanes.caffeine.cache;

import static com.github.benmanes.caffeine.cache.Caffeine.saturatedToNanos;
import static com.github.benmanes.caffeine.cache.Caffeine.toNanosSaturated;

import java.time.Duration;
import java.util.ConcurrentModificationException;
Expand Down Expand Up @@ -414,7 +414,7 @@ default Duration getExpiresAfter() {
* @throws NullPointerException if the duration is null
*/
default void setExpiresAfter(Duration duration) {
setExpiresAfter(saturatedToNanos(duration), TimeUnit.NANOSECONDS);
setExpiresAfter(toNanosSaturated(duration), TimeUnit.NANOSECONDS);
}

/**
Expand Down Expand Up @@ -555,7 +555,7 @@ default Optional<Duration> getExpiresAfter(K key) {
* @throws NullPointerException if the specified key or duration is null
*/
default void setExpiresAfter(K key, Duration duration) {
setExpiresAfter(key, saturatedToNanos(duration), TimeUnit.NANOSECONDS);
setExpiresAfter(key, toNanosSaturated(duration), TimeUnit.NANOSECONDS);
}

/**
Expand Down Expand Up @@ -590,7 +590,7 @@ default void setExpiresAfter(K key, Duration duration) {
* @throws NullPointerException if the specified key, value, or duration is null
*/
default @Nullable V putIfAbsent(K key, V value, Duration duration) {
return putIfAbsent(key, value, saturatedToNanos(duration), TimeUnit.NANOSECONDS);
return putIfAbsent(key, value, toNanosSaturated(duration), TimeUnit.NANOSECONDS);
}

/**
Expand Down Expand Up @@ -625,7 +625,7 @@ default void setExpiresAfter(K key, Duration duration) {
* @throws NullPointerException if the specified key, value, or duration is null
*/
default @Nullable V put(K key, V value, Duration duration) {
return put(key, value, saturatedToNanos(duration), TimeUnit.NANOSECONDS);
return put(key, value, toNanosSaturated(duration), TimeUnit.NANOSECONDS);
}

/**
Expand Down Expand Up @@ -827,7 +827,7 @@ default Duration getRefreshesAfter() {
* @throws NullPointerException if the duration is null
*/
default void setRefreshesAfter(Duration duration) {
setRefreshesAfter(saturatedToNanos(duration), TimeUnit.NANOSECONDS);
setRefreshesAfter(toNanosSaturated(duration), TimeUnit.NANOSECONDS);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ public void clear_pendingWrites_weakKeys(
ref.enqueue();
}
GcFinalization.awaitFullGc();
collected[0] = (invocation.getArgument(2, RemovalCause.class) == COLLECTED);
collected[0] = (invocation.<RemovalCause>getArgument(2) == COLLECTED);
}
return null;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package com.github.benmanes.caffeine.cache;

import static com.github.benmanes.caffeine.cache.Caffeine.UNSET_INT;
import static com.github.benmanes.caffeine.cache.CaffeineSpec.parse;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

Expand Down Expand Up @@ -484,6 +483,10 @@ public void testCaffeineFrom_string() {
assertCaffeineEquivalence(expected, fromString);
}

private static CaffeineSpec parse(String specification) {
return CaffeineSpec.parse(specification);
}

private static void assertCaffeineEquivalence(Caffeine<?, ?> a, Caffeine<?, ?> b) {
assertEquals("expireAfterAccessNanos", a.expireAfterAccessNanos, b.expireAfterAccessNanos);
assertEquals("expireAfterWriteNanos", a.expireAfterWriteNanos, b.expireAfterWriteNanos);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public void schedule(Cache<Int, Int> cache, CacheContext context) {
expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE})
public void schedule_immediate(Cache<Int, Int> cache, CacheContext context) {
doAnswer(invocation -> {
invocation.getArgument(1, Runnable.class).run();
invocation.<Runnable>getArgument(1).run();
return new CompletableFuture<>();
}).when(context.scheduler()).schedule(any(), any(), anyLong(), any());

Expand All @@ -192,8 +192,8 @@ public void schedule_delay(Cache<Int, Duration> cache, CacheContext context) {
var delay = ArgumentCaptor.forClass(long.class);
var task = ArgumentCaptor.forClass(Runnable.class);
Answer<Void> onRemoval = invocation -> {
var key = invocation.getArgument(0, Int.class);
var value = invocation.getArgument(1, Duration.class);
Int key = invocation.getArgument(0);
Duration value = invocation.getArgument(1);
actualExpirationPeriods.put(key, Duration.ofNanos(context.ticker().read()).minus(value));
return null;
};
Expand Down
Loading

0 comments on commit d5c677f

Please sign in to comment.