diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java index 22e37cbb81..3dd867609b 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java @@ -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; @@ -4338,7 +4338,7 @@ final class BoundedVarExpiration implements VarExpiration { requireNonNull(remappingFunction); requireArgument(!duration.isNegative(), "duration cannot be negative: %s", duration); var expiry = new FixedExpireAfterWrite( - saturatedToNanos(duration), TimeUnit.NANOSECONDS); + toNanosSaturated(duration), TimeUnit.NANOSECONDS); return cache.isAsync ? computeAsync(key, remappingFunction, expiry) diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java index 5311f7c681..bada3969e4 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java @@ -140,6 +140,8 @@ public final class Caffeine { static final Supplier 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 } @@ -604,8 +606,8 @@ public Caffeine 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 @@ -613,7 +615,7 @@ public Caffeine softValues() { */ @CanIgnoreReturnValue public Caffeine expireAfterWrite(Duration duration) { - return expireAfterWrite(saturatedToNanos(duration), TimeUnit.NANOSECONDS); + return expireAfterWrite(toNanosSaturated(duration), TimeUnit.NANOSECONDS); } /** @@ -628,8 +630,8 @@ public Caffeine 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 @@ -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 @@ -674,7 +676,7 @@ boolean expiresAfterWrite() { */ @CanIgnoreReturnValue public Caffeine expireAfterAccess(Duration duration) { - return expireAfterAccess(saturatedToNanos(duration), TimeUnit.NANOSECONDS); + return expireAfterAccess(toNanosSaturated(duration), TimeUnit.NANOSECONDS); } /** @@ -692,7 +694,7 @@ public Caffeine 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) @@ -793,7 +795,7 @@ boolean expiresVariable() { */ @CanIgnoreReturnValue public Caffeine refreshAfterWrite(Duration duration) { - return refreshAfterWrite(saturatedToNanos(duration), TimeUnit.NANOSECONDS); + return refreshAfterWrite(toNanosSaturated(duration), TimeUnit.NANOSECONDS); } /** @@ -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(); } /** diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Expiry.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Expiry.java index 36158da70a..25f254165b 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Expiry.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Expiry.java @@ -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. + *

+ * Usage example: + *

{@code
+ *   LoadingCache cache = Caffeine.newBuilder()
+ *       .expireAfter(Expiry.creating((Key key, Graph graph) ->
+ *           Duration.between(Instant.now(), graph.createdOn().plusHours(5))))
+ *       .build(key -> createExpensiveGraph(key));
+ * }
* * @author ben.manes@gmail.com (Ben Manes) */ @@ -76,4 +93,121 @@ public interface Expiry { * @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. + * + *
{@code
+   * Expiry expiry = Expiry.creating((key, graph) ->
+   *     Duration.between(Instant.now(), graph.createdOn().plusHours(5)));
+   * }
+ * + * @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 Expiry creating(BiFunction 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. + * + *
{@code
+   * Expiry expiry = Expiry.writing((key, graph) ->
+   *     Duration.between(Instant.now(), graph.modifiedOn().plusHours(5)));
+   * }
+ * + * @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 Expiry writing(BiFunction 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. + * + *
{@code
+   * Expiry expiry = Expiry.accessing((key, graph) ->
+   *     graph.isDirected() ? Duration.ofHours(1) : Duration.ofHours(3));
+   * }
+ * + * @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 Expiry accessing(BiFunction function) { + return new ExpiryAfterAccess<>(function); + } +} + +final class ExpiryAfterCreate implements Expiry, Serializable { + private static final long serialVersionUID = 1L; + + @SuppressWarnings("serial") + final BiFunction function; + + public ExpiryAfterCreate(BiFunction 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 implements Expiry, Serializable { + private static final long serialVersionUID = 1L; + + @SuppressWarnings("serial") + final BiFunction function; + + public ExpiryAfterWrite(BiFunction 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 implements Expiry, Serializable { + private static final long serialVersionUID = 1L; + + @SuppressWarnings("serial") + final BiFunction function; + + public ExpiryAfterAccess(BiFunction 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)); + } } diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Policy.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Policy.java index 70d4827e11..0f78860af0 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Policy.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Policy.java @@ -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; @@ -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); } /** @@ -555,7 +555,7 @@ default Optional 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); } /** @@ -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); } /** @@ -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); } /** @@ -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); } } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java index 0b36c3e39c..60f3e7e595 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java @@ -219,7 +219,7 @@ public void clear_pendingWrites_weakKeys( ref.enqueue(); } GcFinalization.awaitFullGc(); - collected[0] = (invocation.getArgument(2, RemovalCause.class) == COLLECTED); + collected[0] = (invocation.getArgument(2) == COLLECTED); } return null; }; diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CaffeineSpecGuavaTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CaffeineSpecGuavaTest.java index 38d2f1fcfb..7ca8227488 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CaffeineSpecGuavaTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CaffeineSpecGuavaTest.java @@ -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; @@ -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); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java index 0903b28933..aa06159441 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java @@ -172,7 +172,7 @@ public void schedule(Cache cache, CacheContext context) { expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void schedule_immediate(Cache cache, CacheContext context) { doAnswer(invocation -> { - invocation.getArgument(1, Runnable.class).run(); + invocation.getArgument(1).run(); return new CompletableFuture<>(); }).when(context.scheduler()).schedule(any(), any(), anyLong(), any()); @@ -192,8 +192,8 @@ public void schedule_delay(Cache cache, CacheContext context) { var delay = ArgumentCaptor.forClass(long.class); var task = ArgumentCaptor.forClass(Runnable.class); Answer 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; }; diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java index 7b71e74b14..1426cfa705 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java @@ -43,6 +43,7 @@ import static org.mockito.Mockito.when; import static org.slf4j.event.Level.WARN; +import java.io.Serializable; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.ConcurrentModificationException; @@ -75,6 +76,7 @@ import com.github.benmanes.caffeine.testing.ConcurrentTestHarness; import com.github.benmanes.caffeine.testing.Int; import com.google.common.collect.ImmutableList; +import com.google.common.testing.SerializableTester; /** * The test cases for caches that support the variable expiration policy. @@ -1637,6 +1639,121 @@ public void youngestFunc_metadata_expiresInTraversal(CacheContext context, assertThat(entries).hasSize(1); } + /* --------------- Expiry --------------- */ + + @Test + public void expiry_creating_null() { + var expiry = Expiry.creating((key, value) -> null); + assertThrows(NullPointerException.class, () -> expiry.expireAfterCreate(1, 2, 3)); + assertThat(expiry.expireAfterUpdate(1, 2, 3, 99)).isEqualTo(99); + assertThat(expiry.expireAfterRead(1, 2, 3, 99)).isEqualTo(99); + } + + @Test + public void expiry_creating() { + var expiry = Expiry.creating((Integer key, Integer value) -> Duration.ofSeconds(key + value)); + assertThat(expiry.expireAfterCreate(1, 2, 3)).isEqualTo(Duration.ofSeconds(3).toNanos()); + assertThat(expiry.expireAfterUpdate(1, 2, 3, 99)).isEqualTo(99); + assertThat(expiry.expireAfterRead(1, 2, 3, 99)).isEqualTo(99); + } + + @Test + public void expiry_creating_saturating() { + var expiry = Expiry.creating((Long key, Long value) -> Duration.ofNanos(key)); + assertThat(expiry.expireAfterCreate(Long.MIN_VALUE, 2L, 3)).isEqualTo(Long.MIN_VALUE); + assertThat(expiry.expireAfterCreate(Long.MAX_VALUE, 2L, 3)).isEqualTo(Long.MAX_VALUE); + } + + @Test + public void expiry_creating_serialize() { + SerializableBiFunction creator = (key, value) -> Duration.ofNanos(key); + + var expiry = Expiry.creating(creator); + var reserialized = SerializableTester.reserialize(expiry); + assertThat(reserialized.expireAfterCreate(1, 2, 3)).isEqualTo(1L); + assertThat(expiry.expireAfterUpdate(1, 2, 3, 99)).isEqualTo(99); + assertThat(expiry.expireAfterRead(1, 2, 3, 99)).isEqualTo(99); + } + + @Test + public void expiry_writing_null() { + var expiry = Expiry.writing((Integer key, Integer value) -> null); + assertThrows(NullPointerException.class, () -> expiry.expireAfterCreate(1, 2, 3)); + assertThrows(NullPointerException.class, () -> expiry.expireAfterUpdate(1, 2, 3, 99)); + assertThat(expiry.expireAfterRead(1, 2, 3, 99)).isEqualTo(99); + } + + @Test + public void expiry_writing() { + var expiry = Expiry.writing((Integer key, Integer value) -> Duration.ofSeconds(key + value)); + assertThat(expiry.expireAfterCreate(1, 2, 3)).isEqualTo(Duration.ofSeconds(3).toNanos()); + assertThat(expiry.expireAfterUpdate(1, 2, 3, 99)).isEqualTo(Duration.ofSeconds(3).toNanos()); + assertThat(expiry.expireAfterRead(1, 2, 3, 99)).isEqualTo(99); + } + + @Test + public void expiry_writing_saturating() { + var expiry = Expiry.writing((Long key, Long value) -> Duration.ofNanos(key)); + assertThat(expiry.expireAfterCreate(Long.MIN_VALUE, 2L, 3)).isEqualTo(Long.MIN_VALUE); + assertThat(expiry.expireAfterCreate(Long.MAX_VALUE, 2L, 3)).isEqualTo(Long.MAX_VALUE); + assertThat(expiry.expireAfterUpdate(Long.MIN_VALUE, 2L, 3, 4)).isEqualTo(Long.MIN_VALUE); + assertThat(expiry.expireAfterUpdate(Long.MAX_VALUE, 2L, 3, 4)).isEqualTo(Long.MAX_VALUE); + } + + @Test + public void expiry_writing_serialize() { + SerializableBiFunction writer = (key, value) -> Duration.ofSeconds(key + value); + + var expiry = Expiry.writing(writer); + var reserialized = SerializableTester.reserialize(expiry); + assertThat(reserialized.expireAfterCreate(1, 2, 3)).isEqualTo(Duration.ofSeconds(3).toNanos()); + assertThat(reserialized.expireAfterUpdate(1, 2, 3, 99)) + .isEqualTo(Duration.ofSeconds(3).toNanos()); + assertThat(reserialized.expireAfterRead(1, 2, 3, 99)).isEqualTo(99); + } + + @Test + public void expiry_accessing_null() { + var expiry = Expiry.accessing((Integer key, Integer value) -> null); + assertThrows(NullPointerException.class, () -> expiry.expireAfterCreate(1, 2, 3)); + assertThrows(NullPointerException.class, () -> expiry.expireAfterUpdate(1, 2, 3, 99)); + assertThrows(NullPointerException.class, () -> expiry.expireAfterRead(1, 2, 3, 99)); + } + + @Test + public void expiry_accessing() { + var expiry = Expiry.accessing((Integer key, Integer value) -> Duration.ofSeconds(key + value)); + assertThat(expiry.expireAfterCreate(1, 2, 3)).isEqualTo(Duration.ofSeconds(3).toNanos()); + assertThat(expiry.expireAfterUpdate(1, 2, 3, 99)).isEqualTo(Duration.ofSeconds(3).toNanos()); + assertThat(expiry.expireAfterRead(1, 2, 3, 99)).isEqualTo(Duration.ofSeconds(3).toNanos()); + } + + @Test + public void expiry_accessing_saturating() { + var expiry = Expiry.accessing((Long key, Long value) -> Duration.ofNanos(key)); + assertThat(expiry.expireAfterCreate(Long.MIN_VALUE, 2L, 3)).isEqualTo(Long.MIN_VALUE); + assertThat(expiry.expireAfterCreate(Long.MAX_VALUE, 2L, 3)).isEqualTo(Long.MAX_VALUE); + assertThat(expiry.expireAfterUpdate(Long.MIN_VALUE, 2L, 3, 4)).isEqualTo(Long.MIN_VALUE); + assertThat(expiry.expireAfterUpdate(Long.MAX_VALUE, 2L, 3, 4)).isEqualTo(Long.MAX_VALUE); + assertThat(expiry.expireAfterRead(Long.MIN_VALUE, 2L, 3, 4)).isEqualTo(Long.MIN_VALUE); + assertThat(expiry.expireAfterRead(Long.MAX_VALUE, 2L, 3, 4)).isEqualTo(Long.MAX_VALUE); + } + + @Test + public void expiry_accessing_serialize() { + SerializableBiFunction accessor = (key, value) -> Duration.ofSeconds(key + value); + + var expiry = Expiry.accessing(accessor); + var reserialized = SerializableTester.reserialize(expiry); + assertThat(reserialized.expireAfterCreate(1, 2, 3)).isEqualTo(Duration.ofSeconds(3).toNanos()); + assertThat(reserialized.expireAfterUpdate(1, 2, 3, 99)) + .isEqualTo(Duration.ofSeconds(3).toNanos()); + assertThat(reserialized.expireAfterRead(1, 2, 3, 99)) + .isEqualTo(Duration.ofSeconds(3).toNanos()); + } + + interface SerializableBiFunction extends BiFunction, Serializable {}; + static final class ExpirationException extends RuntimeException { private static final long serialVersionUID = 1L; } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/TimerWheelTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/TimerWheelTest.java index c6d1eccb27..01c5476ac0 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/TimerWheelTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/TimerWheelTest.java @@ -403,7 +403,7 @@ public void deschedule_fuzzy(long clock, long nanos, long[] times) { @Test(dataProvider = "clock") public void expire_reschedule(long clock) { when(cache.evictEntry(captor.capture(), any(), anyLong())).thenAnswer(invocation -> { - var timer = (Timer) invocation.getArgument(0); + Timer timer = invocation.getArgument(0); timer.setVariableTime(timerWheel.nanos + 100); return false; }); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/AsyncCacheSubject.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/AsyncCacheSubject.java index 978aab4f2b..02d56c2bb0 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/AsyncCacheSubject.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/AsyncCacheSubject.java @@ -100,7 +100,7 @@ public void containsEntry(Object key, Object value) { /** Fails if the cache does not contain exactly the given set of entries in the given map. */ public void containsExactlyEntriesIn(Map expectedMap) { - if (expectedMap.values().stream().anyMatch(value -> value instanceof Future)) { + if (expectedMap.values().stream().anyMatch(Future.class::isInstance)) { check("cache").that(actual.asMap()).containsExactlyEntriesIn(expectedMap); } else { check("cache").about(cache()) diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java index 0da96feafb..c0ee3b81fe 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java @@ -42,6 +42,7 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; import java.util.function.Supplier; import org.mockito.Mockito; @@ -282,27 +283,24 @@ enum CacheExpiry { } }, CREATE { + @SuppressWarnings("unchecked") @Override public Expiry createExpiry(Expire expiryTime) { - return ExpiryBuilder - .expiringAfterCreate(expiryTime.duration()) - .build(); + return Expiry.creating( + (Serializable & BiFunction) (k, v) -> expiryTime.duration()); } }, WRITE { + @SuppressWarnings("unchecked") @Override public Expiry createExpiry(Expire expiryTime) { - return ExpiryBuilder - .expiringAfterCreate(expiryTime.duration()) - .expiringAfterUpdate(expiryTime.duration()) - .build(); + return Expiry.writing( + (Serializable & BiFunction) (k, v) -> expiryTime.duration()); } }, ACCESS { + @SuppressWarnings("unchecked") @Override public Expiry createExpiry(Expire expiryTime) { - return ExpiryBuilder - .expiringAfterCreate(expiryTime.duration()) - .expiringAfterUpdate(expiryTime.duration()) - .expiringAfterRead(expiryTime.duration()) - .build(); + return Expiry.accessing( + (Serializable & BiFunction) (k, v) -> expiryTime.duration()); } }; diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java index 3ac9d379a8..f98f813116 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java @@ -142,7 +142,7 @@ public void afterInvocation(IInvokedMethod method, ITestResult testResult) { /** Validates the internal state of the cache. */ private void validate(ITestResult testResult) { CacheContext context = Arrays.stream(testResult.getParameters()) - .filter(param -> param instanceof CacheContext) + .filter(CacheContext.class::isInstance) .findFirst().map(param -> (CacheContext) param) .orElse(null); if (context != null) { diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/ExpiryBuilder.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/ExpiryBuilder.java deleted file mode 100644 index 23c206911c..0000000000 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/ExpiryBuilder.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2017 Ben Manes. All Rights Reserved. - * - * 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 - * - * http://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 com.github.benmanes.caffeine.cache.testing; - -import static java.util.Objects.requireNonNull; - -import java.io.Serializable; -import java.time.Duration; - -import com.github.benmanes.caffeine.cache.Expiry; -import com.google.errorprone.annotations.CanIgnoreReturnValue; - -/** - * A builder for unit test convenience. - * - * @author ben.manes@gmail.com (Ben Manes) - */ -public final class ExpiryBuilder { - private final Duration create; - private Duration update; - private Duration read; - - private ExpiryBuilder(Duration create) { - this.create = create; - } - - /** Sets the fixed creation expiration time. */ - public static ExpiryBuilder expiringAfterCreate(Duration duration) { - return new ExpiryBuilder(duration); - } - - /** Sets the fixed update expiration time. */ - @CanIgnoreReturnValue - public ExpiryBuilder expiringAfterUpdate(Duration duration) { - update = duration; - return this; - } - - /** Sets the fixed read expiration time. */ - @CanIgnoreReturnValue - public ExpiryBuilder expiringAfterRead(Duration duration) { - read = duration; - return this; - } - - public Expiry build() { - return new FixedExpiry(create, update, read); - } - - private static final class FixedExpiry implements Expiry, Serializable { - private static final long serialVersionUID = 1L; - - private final Duration create; - private final Duration update; - private final Duration read; - - FixedExpiry(Duration create, Duration update, Duration read) { - this.create = create; - this.update = update; - this.read = read; - } - - @Override - public long expireAfterCreate(K key, V value, long currentTime) { - requireNonNull(key); - requireNonNull(value); - return create.toNanos(); - } - @Override - public long expireAfterUpdate(K key, V value, long currentTime, long currentDuration) { - requireNonNull(key); - requireNonNull(value); - return (update == null) ? currentDuration : update.toNanos(); - } - @Override - public long expireAfterRead(K key, V value, long currentTime, long currentDuration) { - requireNonNull(key); - requireNonNull(value); - return (read == null) ? currentDuration : read.toNanos(); - } - } -} diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/lincheck/AbstractLincheckCacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/lincheck/AbstractLincheckCacheTest.java index 362d7a4888..760afeddc1 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/lincheck/AbstractLincheckCacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/lincheck/AbstractLincheckCacheTest.java @@ -16,7 +16,6 @@ package com.github.benmanes.caffeine.lincheck; import java.util.Map; -import java.util.concurrent.ForkJoinPool; import org.jetbrains.kotlinx.lincheck.LinChecker; import org.jetbrains.kotlinx.lincheck.annotations.Operation; @@ -44,8 +43,7 @@ public abstract class AbstractLincheckCacheTest { private final LoadingCache cache; public AbstractLincheckCacheTest(Caffeine builder) { - cache = builder.executor(Runnable::run).build(key -> -key); - ForkJoinPool.commonPool(); // force eager initialization + cache = builder.build(key -> -key); } /** diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/lincheck/CaffeineLincheckTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/lincheck/CaffeineLincheckTest.java index 96d50ec146..c4aebb7598 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/lincheck/CaffeineLincheckTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/lincheck/CaffeineLincheckTest.java @@ -36,8 +36,10 @@ public Object[] factory() { } public static final class BoundedLincheckTest extends AbstractLincheckCacheTest { + public BoundedLincheckTest() { super(Caffeine.newBuilder() + .executor(Runnable::run) .maximumSize(Long.MAX_VALUE) .expireAfterWrite(Duration.ofNanos(Long.MAX_VALUE))); } diff --git a/examples/coalescing-bulkloader-reactor/gradle/libs.versions.toml b/examples/coalescing-bulkloader-reactor/gradle/libs.versions.toml index cd08ccc524..314a7ecbef 100644 --- a/examples/coalescing-bulkloader-reactor/gradle/libs.versions.toml +++ b/examples/coalescing-bulkloader-reactor/gradle/libs.versions.toml @@ -2,7 +2,7 @@ caffeine = "3.1.8" junit = "5.10.2" reactor = "3.6.2" -truth = "1.4.0" +truth = "1.4.1" versions = "0.51.0" [libraries] diff --git a/examples/graal-native/gradle/libs.versions.toml b/examples/graal-native/gradle/libs.versions.toml index d173df16b8..bfe2166946 100644 --- a/examples/graal-native/gradle/libs.versions.toml +++ b/examples/graal-native/gradle/libs.versions.toml @@ -2,7 +2,7 @@ caffeine = "3.1.8" graal = "0.10.0" junit = "5.10.2" -truth = "1.4.0" +truth = "1.4.1" versions = "0.51.0" [libraries] diff --git a/examples/hibernate/gradle/libs.versions.toml b/examples/hibernate/gradle/libs.versions.toml index dd5ec5eabc..e8b878ab0b 100644 --- a/examples/hibernate/gradle/libs.versions.toml +++ b/examples/hibernate/gradle/libs.versions.toml @@ -1,11 +1,11 @@ [versions] caffeine = "3.1.8" h2 = "2.2.224" -hibernate = "6.4.3.Final" +hibernate = "6.4.4.Final" junit = "5.10.2" log4j2 = "3.0.0-beta1" slf4j = "2.0.7" -truth = "1.4.0" +truth = "1.4.1" versions = "0.51.0" [libraries] diff --git a/examples/resilience-failsafe/gradle/libs.versions.toml b/examples/resilience-failsafe/gradle/libs.versions.toml index 242726e5df..6d5b75e079 100644 --- a/examples/resilience-failsafe/gradle/libs.versions.toml +++ b/examples/resilience-failsafe/gradle/libs.versions.toml @@ -2,7 +2,7 @@ caffeine = "3.1.8" failsafe = "3.3.2" junit = "5.10.2" -truth = "1.4.0" +truth = "1.4.1" versions = "0.51.0" [libraries] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a6f66005a7..b7c1147b32 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ checker-framework = "3.42.0" checkstyle = "10.13.0" coherence = "22.06.2" commons-collections4 = "4.4" -commons-compress = "1.25.0" +commons-compress = "1.26.0" commons-io = "2.15.1" commons-lang3 = "3.14.0" commons-math3 = "3.6.1" @@ -23,12 +23,12 @@ coveralls = "2.12.2" dependency-check = "9.0.9" eclipse-collections = "12.0.0.M3" ehcache3 = "3.10.8" -errorprone-core = "2.24.1" +errorprone-core = "2.25.0" errorprone-plugin = "3.1.0" -errorprone-support = "0.14.0" +errorprone-support = "0.15.0" expiring-map = "0.5.11" fast-filter = "1.0.2" -fastutil = "8.5.12" +fastutil = "8.5.13" felix-framework = "7.0.5" felix-scr = "2.2.10" findsecbugs = "1.12.0" @@ -51,7 +51,7 @@ java-object-layout = "0.17" javapoet = "1.13.0" jcache = "1.1.1" jcommander = "1.82" -jctools = "4.0.2" +jctools = "4.0.3" jfreechart = "1.5.4" jgit = "6.8.0.202311291450-r" jmh-core = "1.37" @@ -64,13 +64,13 @@ junit-testng = "1.0.5" junit4 = "4.13.2" junit5 = "5.10.2" kotlin = "1.9.22" -lincheck = "2.18.1" +lincheck = "2.25" mockito = "5.10.0" nexus-publish = "2.0.0-rc-2" -nullaway-core = "0.10.22" -nullaway-plugin = "1.6.0" +nullaway-core = "0.10.23" +nullaway-plugin = "2.0.0" okhttp-bom = "4.12.0" -okio-bom = "3.7.0" +okio-bom = "3.8.0" osgi-annotations = "1.5.1" osgi-function = "1.2.0" osgi-promise = "1.3.0" @@ -89,7 +89,7 @@ spotbugs-plugin = "6.0.7" stream = "2.9.8" tcache = "2.0.1" testng = "7.9.0" -truth = "1.4.0" +truth = "1.4.1" univocity-parsers = "2.9.1" versions = "0.51.0" xz = "1.9" diff --git a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheManagerImpl.java b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheManagerImpl.java index 0ea06e66b9..cadc9b5b70 100644 --- a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheManagerImpl.java +++ b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheManagerImpl.java @@ -240,7 +240,7 @@ public boolean isClosed() { @Override public T unwrap(Class clazz) { - if (clazz.isAssignableFrom(getClass())) { + if (clazz.isInstance(this)) { return clazz.cast(this); } throw new IllegalArgumentException("Unwapping to " + clazz diff --git a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheProxy.java b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheProxy.java index ea2767d494..28a9357444 100644 --- a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheProxy.java +++ b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheProxy.java @@ -1016,10 +1016,9 @@ public void close() { @Override public T unwrap(Class clazz) { - if (clazz.isAssignableFrom(cache.getClass())) { + if (clazz.isInstance(cache)) { return clazz.cast(cache); - } - if (clazz.isAssignableFrom(getClass())) { + } else if (clazz.isInstance(this)) { return clazz.cast(this); } throw new IllegalArgumentException("Unwrapping to " + clazz