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 20, 2024
1 parent f151394 commit 49df4e9
Show file tree
Hide file tree
Showing 26 changed files with 326 additions and 168 deletions.
5 changes: 3 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version: 2.1
parameters:
java_version:
type: integer
default: 17
default: 21

commands:
setup_java:
Expand Down Expand Up @@ -161,7 +161,8 @@ workflows:
- caffeine:strongKeysAndSoftValuesSyncCaffeineTest
- caffeine:weakKeysAndWeakValuesSyncCaffeineTest
- caffeine:weakKeysAndSoftValuesSyncCaffeineTest
- caffeine:lincheckTest
# https://github.com/JetBrains/lincheck/issues/278
# - caffeine:lincheckTest
- caffeine:isolatedTest
- caffeine:junitTest
- simulator:run
2 changes: 2 additions & 0 deletions .github/workflows/dependency-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ jobs:
- name: Run dependency-check
uses: ./.github/actions/run-gradle
continue-on-error: true
env:
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
with:
java: ${{ env.JAVA_VERSION }}
arguments: dependencyCheckAggregate --no-configuration-cache
Expand Down
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
134 changes: 134 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 ben.manes@gmail.com (Ben Manes)
*/
Expand Down Expand Up @@ -76,4 +93,121 @@ 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.accessing((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

0 comments on commit 49df4e9

Please sign in to comment.