From a95ae264b0efff0519f3a6fab759b18a8fb7909a Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Sun, 21 Feb 2021 11:53:11 -0800 Subject: [PATCH] Added snapshot view of in-flight refresh operations The ongoing refresh operations can now be observed by using Policy.refreshes(). This does not provide a live view due to weak keys needing to be unwrapped from reference key (and possibly discarded if null). Instead an unmodifiable copy is provided for inspection. The futures may be canceled, completed, or waited upon. --- .../caffeine/cache/BoundedLocalCache.java | 25 +++++++++++++++++-- .../benmanes/caffeine/cache/Policy.java | 8 ++++++ .../caffeine/cache/UnboundedLocalCache.java | 9 +++++++ .../benmanes/caffeine/cache/CacheTest.java | 17 +++++++++++++ .../caffeine/cache/LoadingCacheTest.java | 19 ++++++++++++++ .../caffeine/cache/RefreshAfterWriteTest.java | 22 +++++++++++++++- .../cache/testing/GuavaCacheFromContext.java | 3 +++ 7 files changed, 100 insertions(+), 3 deletions(-) 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 63d499374a..45d83607d4 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 @@ -42,6 +42,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -1508,8 +1509,7 @@ public void cleanUp() { } /** - * Performs the maintenance work, blocking until the lock is acquired. Any exception thrown, such - * as by {@link CacheWriter#delete}, is propagated to the caller. + * Performs the maintenance work, blocking until the lock is acquired. * * @param task an additional pending task to run, or {@code null} if not present */ @@ -3485,6 +3485,27 @@ static final class BoundedPolicy implements Policy { } return transformer.apply(node.getValue()); } + @Override public Map> refreshes() { + var refreshes = cache.refreshes; + if ((refreshes == null) || refreshes.isEmpty()) { + return Map.of(); + } if (cache.collectKeys()) { + var inFlight = new IdentityHashMap>(refreshes.size()); + for (var entry : refreshes.entrySet()) { + @SuppressWarnings("unchecked") + var key = ((InternalReference) entry.getKey()).get(); + @SuppressWarnings("unchecked") + var future = (CompletableFuture) entry.getValue(); + if (key != null) { + inFlight.put(key, future); + } + } + return Collections.unmodifiableMap(inFlight); + } + @SuppressWarnings("unchecked") + var castedRefreshes = (Map>) (Object) refreshes; + return Map.copyOf(castedRefreshes); + } @Override public Optional> eviction() { return cache.evicts() ? (eviction == null) ? (eviction = Optional.of(new BoundedEviction())) : eviction 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 cd25fc1e1b..6624107b0d 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 @@ -20,6 +20,7 @@ import java.util.Optional; import java.util.OptionalInt; import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import org.checkerframework.checker.index.qual.NonNegative; @@ -56,6 +57,13 @@ public interface Policy { @Nullable V getIfPresentQuietly(K key); + /** + * Returns an unmodifiable snapshot {@link Map} view of the in-flight refresh operations. + * + * @return a snapshot view of the in-flight refresh operations + */ + Map> refreshes(); + /** * Returns access to perform operations based on the maximum size or maximum weight eviction * policy. If the cache was not constructed with a size-based bound or the implementation does diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java index cfce808cd4..f155dd0da3 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java @@ -921,6 +921,15 @@ static final class UnboundedPolicy implements Policy { @Override public V getIfPresentQuietly(Object key) { return transformer.apply(cache.data.get(key)); } + @Override public Map> refreshes() { + var refreshes = cache.refreshes; + if (refreshes == null) { + return Map.of(); + } + @SuppressWarnings("unchecked") + var castedRefreshes = (Map>) (Object) refreshes; + return Map.copyOf(castedRefreshes); + } @Override public Optional> eviction() { return Optional.empty(); } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CacheTest.java index 91c0df6948..0b7980f95a 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CacheTest.java @@ -22,6 +22,7 @@ import static java.util.stream.Collectors.toMap; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; @@ -727,4 +728,20 @@ public void getIfPresentQuietly_present(Cache cache, CacheCont assertThat(cache.policy().getIfPresentQuietly(context.middleKey()), is(not(nullValue()))); assertThat(cache.policy().getIfPresentQuietly(context.lastKey()), is(not(nullValue()))); } + + /* --------------- Policy: refreshes --------------- */ + + @CacheSpec + @CheckNoStats + @Test(dataProvider = "caches") + public void refreshes_empty(Cache cache, CacheContext context) { + assertThat(cache.policy().refreshes(), is(anEmptyMap())); + } + + @CacheSpec + @CheckNoStats + @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) + public void refreshes_unmodifiable(Cache cache, CacheContext context) { + cache.policy().refreshes().clear(); + } } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LoadingCacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LoadingCacheTest.java index e9c2e8f43c..ce68a8321b 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LoadingCacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LoadingCacheTest.java @@ -23,6 +23,7 @@ import static java.util.stream.Collectors.toMap; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.either; import static org.hamcrest.Matchers.empty; @@ -811,4 +812,22 @@ public void bulk_present() throws Exception { assertThat(loader.loadAll(Set.of(1, 2)), is(Map.of(1, 1, 2, 2))); assertThat(loader.load(1), is(1)); } + + /* --------------- Policy: refreshes --------------- */ + + @Test(dataProvider = "caches") + @CacheSpec(loader = Loader.ASYNC_INCOMPLETE, implementation = Implementation.Caffeine) + public void refreshes(LoadingCache cache, CacheContext context) { + var key1 = Iterables.get(context.absentKeys(), 0); + var key2 = context.original().isEmpty() + ? Iterables.get(context.absentKeys(), 1) + : context.firstKey(); + var future1 = cache.refresh(key1); + var future2 = cache.refresh(key2); + assertThat(cache.policy().refreshes(), is(equalTo(Map.of(key1, future1, key2, future2)))); + + future1.complete(1); + future2.cancel(true); + assertThat(cache.policy().refreshes(), is(anEmptyMap())); + } } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java index fe809a1b09..cfc0253532 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java @@ -21,6 +21,8 @@ import static com.github.benmanes.caffeine.testing.IsFutureValue.futureOf; import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -373,7 +375,25 @@ public void refresh(LoadingCache cache, CacheContext context) future2.cancel(true); } - /* --------------- Policy --------------- */ + /* --------------- Policy: refreshes --------------- */ + + @Test(dataProvider = "caches") + @CacheSpec(loader = Loader.ASYNC_INCOMPLETE, implementation = Implementation.Caffeine, + refreshAfterWrite = Expire.ONE_MINUTE, population = Population.FULL) + public void refreshes(LoadingCache cache, CacheContext context) { + context.ticker().advance(2, TimeUnit.MINUTES); + cache.getIfPresent(context.firstKey()); + assertThat(cache.policy().refreshes(), is(aMapWithSize(1))); + + var future = cache.policy().refreshes().get(context.firstKey()); + assertThat(future, is(not(nullValue()))); + + future.complete(Integer.MAX_VALUE); + assertThat(cache.policy().refreshes(), is(anEmptyMap())); + assertThat(cache.getIfPresent(context.firstKey()), is(Integer.MAX_VALUE)); + } + + /* --------------- Policy: refreshAfterWrite --------------- */ @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/GuavaCacheFromContext.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/GuavaCacheFromContext.java index 61babc75bd..2a530d0bcb 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/GuavaCacheFromContext.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/GuavaCacheFromContext.java @@ -436,6 +436,9 @@ public Policy policy() { @Override public V getIfPresentQuietly(K key) { return cache.asMap().get(key); } + @Override public Map> refreshes() { + return Map.of(); + } @Override public Optional> eviction() { return Optional.empty(); }