From 157a91bbf1c3efcfb5814917952dc6c481ba4675 Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Sat, 20 Feb 2021 21:44:15 -0800 Subject: [PATCH] Add refreshAll(keys) convenience method (bulk not supported) This interface method provides a placeholder for future support of bulk reloading (see #7). That is not implemented. Therefore this method merely calls refresh(key) for each key and composes the result. If and when bulk reloading is supported then this method may be optimized. --- .github/workflows/codeql.yml | 7 +- .../benmanes/caffeine/cache/LoadingCache.java | 19 ++++++ .../caffeine/cache/LocalAsyncCache.java | 6 +- .../cache/LocalAsyncLoadingCache.java | 10 +++ .../caffeine/cache/LocalLoadingCache.java | 10 +++ .../caffeine/cache/LoadingCacheTest.java | 68 +++++++++++++++++-- .../cache/testing/GuavaCacheFromContext.java | 27 ++++++++ checksum.xml | 13 +++- gradle/dependencies.gradle | 8 +-- 9 files changed, 153 insertions(+), 15 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ab2836f393..b087abef69 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,13 +41,18 @@ jobs: - run: git checkout HEAD^2 if: ${{ github.event_name == 'pull_request' }} + - name: Setup Java JDK + uses: actions/setup-java@v1.3.0 + with: + java-version: 11 + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. + # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LoadingCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LoadingCache.java index 0229894320..a0488c84eb 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LoadingCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LoadingCache.java @@ -111,4 +111,23 @@ public interface LoadingCache refresh(K key); + + /** + * Loads a new value for each {@code key}, asynchronously. While the new value is loading the + * previous value (if any) will continue to be returned by {@code get(key)} unless it is evicted. + * If the new value is loaded successfully it will replace the previous value in the cache; if an + * exception is thrown while refreshing the previous value will remain, and the exception will + * be logged (using {@link System.Logger}) and swallowed. If another thread is currently + * loading the value for {@code key}, then does not perform an additional load. + *

+ * Caches loaded by a {@link CacheLoader} will call {@link CacheLoader#reload} if the cache + * currently contains a value for the {@code key}, and {@link CacheLoader#load} otherwise. Loading + * is asynchronous by delegating to the default executor. + * + * @param keys the keys whose associated values are to be returned + * @return the future containing an unmodifiable mapping of keys to values for the specified keys + * that are loading the values + * @throws NullPointerException if the specified collection is null or contains a null element + */ + CompletableFuture> refreshAll(Iterable keys); } diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncCache.java index f618a90044..f9a9fb7e3c 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncCache.java @@ -80,8 +80,8 @@ default CompletableFuture get(K key, BiFunction get(K key, - BiFunction> mappingFunction, boolean recordStats) { + default CompletableFuture get(K key, BiFunction> mappingFunction, boolean recordStats) { long startTime = cache().statsTicker().read(); @SuppressWarnings({"unchecked", "rawtypes"}) CompletableFuture[] result = new CompletableFuture[1]; @@ -151,7 +151,7 @@ default CompletableFuture> getAll(Iterable keys, * combined mapping if successful. If any future fails then it is automatically removed from * the cache if still present. */ - default CompletableFuture> composeResult(Map> futures) { + static CompletableFuture> composeResult(Map> futures) { if (futures.isEmpty()) { return CompletableFuture.completedFuture(Collections.emptyMap()); } diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java index 592856d91e..28e6aaf1c1 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java @@ -15,6 +15,7 @@ */ package com.github.benmanes.caffeine.cache; +import static com.github.benmanes.caffeine.cache.LocalAsyncCache.composeResult; import static java.util.Objects.requireNonNull; import java.lang.System.Logger; @@ -195,6 +196,15 @@ public CompletableFuture refresh(K key) { } } + @Override + public CompletableFuture> refreshAll(Iterable keys) { + Map> result = new LinkedHashMap<>(); + for (K key : keys) { + result.computeIfAbsent(key, this::refresh); + } + return composeResult(result); + } + /** Attempts to avoid a reload if the entry is absent, or a load or reload is in-flight. */ private @Nullable CompletableFuture tryOptimisticRefresh(K key, Object keyReference) { // If a refresh is in-flight, then return it directly. If completed and not yet removed, then diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.java index 3dcd0f05a6..20d49f5f5c 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.java @@ -15,6 +15,7 @@ */ package com.github.benmanes.caffeine.cache; +import static com.github.benmanes.caffeine.cache.LocalAsyncCache.composeResult; import static java.util.Objects.requireNonNull; import java.lang.System.Logger; @@ -175,6 +176,15 @@ default CompletableFuture refresh(K key) { return castedFuture; } + @Override + default CompletableFuture> refreshAll(Iterable keys) { + Map> result = new LinkedHashMap<>(); + for (K key : keys) { + result.computeIfAbsent(key, this::refresh); + } + return composeResult(result); + } + /** Returns a mapping function that adapts to {@link CacheLoader#load}. */ static Function newMappingFunction(CacheLoader cacheLoader) { return key -> { 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 254ec717db..e9c2e8f43c 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 @@ -22,6 +22,7 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.either; import static org.hamcrest.Matchers.empty; @@ -370,14 +371,13 @@ public void refresh_ignored(LoadingCache cache, CacheContext c } @Test(dataProvider = "caches") - @CacheSpec(executor = CacheExecutor.DIRECT, loader = Loader.EXCEPTIONAL, - removalListener = { Listener.DEFAULT, Listener.REJECTING }, - population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) + @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, + loader = Loader.EXCEPTIONAL, removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void refresh_failure(LoadingCache cache, CacheContext context) { // Shouldn't leak exception to caller nor retain the future; should retain the stale entry var future1 = cache.refresh(context.absentKey()); var future2 = cache.refresh(context.firstKey()); - var future3 = cache.refresh(context.firstKey()); + var future3 = cache.refresh(context.lastKey()); assertThat(future2, is(not(sameInstance(future3)))); assertThat(future1.isCompletedExceptionally(), is(true)); assertThat(future2.isCompletedExceptionally(), is(true)); @@ -648,6 +648,66 @@ public void refresh_evicted(CacheContext context) { verifyStats(context, verifier -> verifier.success(1).failures(0)); } + /* --------------- refreshAll --------------- */ + + @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) + @Test(dataProvider = "caches", expectedExceptions = NullPointerException.class) + public void refreshAll_null(LoadingCache cache, CacheContext context) { + cache.refreshAll(null).join(); + } + + @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) + @Test(dataProvider = "caches", expectedExceptions = NullPointerException.class) + public void refreshAll_nullKey(LoadingCache cache, CacheContext context) { + cache.refreshAll(Collections.singletonList(null)).join(); + } + + @Test(dataProvider = "caches") + @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) + public void refreshAll_absent(LoadingCache cache, CacheContext context) { + var result = cache.refreshAll(context.absentKeys()).join(); + int count = context.absentKeys().size(); + assertThat(result, aMapWithSize(count)); + assertThat(cache.asMap(), aMapWithSize(context.original().size() + count)); + } + + @Test(dataProvider = "caches") + @CacheSpec(population = Population.FULL, loader = Loader.IDENTITY, + removalListener = { Listener.DEFAULT, Listener.REJECTING }) + public void refreshAll_present(LoadingCache cache, CacheContext context) { + var result = cache.refreshAll(context.original().keySet()).join(); + int count = context.original().keySet().size(); + assertThat(result, aMapWithSize(count)); + + var expected = context.original().keySet().stream().collect(toMap(identity(), identity())); + assertThat(cache.asMap(), is(equalTo(expected))); + } + + @Test(dataProvider = "caches") + @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, + loader = Loader.EXCEPTIONAL, removalListener = { Listener.DEFAULT, Listener.REJECTING }) + public void refreshAll_failure(LoadingCache cache, CacheContext context) { + var future = cache.refreshAll(List.of( + context.absentKey(), context.firstKey(), context.lastKey())); + assertThat(future.isCompletedExceptionally(), is(true)); + assertThat(cache.estimatedSize(), is(context.initialSize())); + } + + @Test(dataProvider = "caches") + @CacheSpec(loader = Loader.ASYNC_INCOMPLETE, implementation = Implementation.Caffeine, + removalListener = { Listener.DEFAULT, Listener.REJECTING }) + public void refreshAll_cancel(LoadingCache cache, CacheContext context) { + var key = context.original().isEmpty() ? context.absentKey() : context.firstKey(); + var future1 = cache.refresh(key); + var future2 = cache.refreshAll(List.of(key)); + + assertThat(future1.isDone(), is(false)); + future1.cancel(true); + + assertThat(future2.isCompletedExceptionally(), is(true)); + assertThat(cache.asMap(), is(equalTo(context.original()))); + } + /* --------------- CacheLoader --------------- */ @Test(expectedExceptions = UnsupportedOperationException.class) 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 fd8e9659d3..61babc75bd 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 @@ -514,6 +514,33 @@ public CompletableFuture refresh(K key) { error.remove(); return CompletableFuture.failedFuture(e); } + + @Override + public CompletableFuture> refreshAll(Iterable keys) { + Map> result = new LinkedHashMap<>(); + for (K key : keys) { + result.computeIfAbsent(key, this::refresh); + } + return composeResult(result); + } + + CompletableFuture> composeResult(Map> futures) { + if (futures.isEmpty()) { + return CompletableFuture.completedFuture(Collections.emptyMap()); + } + @SuppressWarnings("rawtypes") + CompletableFuture[] array = futures.values().toArray(new CompletableFuture[0]); + return CompletableFuture.allOf(array).thenApply(ignored -> { + Map result = new LinkedHashMap<>(futures.size()); + futures.forEach((key, future) -> { + V value = future.getNow(null); + if (value != null) { + result.put(key, value); + } + }); + return Collections.unmodifiableMap(result); + }); + } } static final class GuavaWeigher implements Weigher, Serializable { diff --git a/checksum.xml b/checksum.xml index fb94d9d04a..30bb7e438b 100644 --- a/checksum.xml +++ b/checksum.xml @@ -184,6 +184,7 @@ + @@ -355,6 +356,9 @@ 80BE5ADF1A1D5F182DA3BC718953D0149673C43844690DEA30FE00D83EA3886517E88473DA48619007932047FD45E461643EC07704B3B7C5B52D798801C6066E + + 97823390838111768E99E68A9EB8E603E528A910850EF58B1A9198828C5DE9C8F4BE53238DBED56C46A2471E5F1871FA4CD1D34F86FA44B4E3643BD3D03901A9 + B3BFAD07E6A3D4D73CBCE802D8614CF4AC84E589166D243D41028DC077F84C027DF4D514F145360405F37DA73A8F2E7B65D90877A9EE1151174D2440530F9051 @@ -364,12 +368,12 @@ 1A47AAF2442159C1CBD22521F31C74B4C71C4168AF5B22D04B4691FDD286E90F02B2792DEDAD3EEEC12B5034ADA1A9EE751C975B5A169AE0B33EE800A8D96E7F - - B57469B9FA2C5D598688FF9C6A3B3283496921B91E269EA6D5A1DA6511125BD2545E2CFA8D3736739787B0BACF69C0AF5C5ECDEA474CB9FD56C96495E654682B - 4C8808B0607564006379FBEB63BCEFC03A0F5FE83F307E455EE66B0B40AC238D14388CEA3C1D883835AF089238F824037A423124348571085C6D5415AB3981CF + + B57469B9FA2C5D598688FF9C6A3B3283496921B91E269EA6D5A1DA6511125BD2545E2CFA8D3736739787B0BACF69C0AF5C5ECDEA474CB9FD56C96495E654682B + E126B7CCF3E42FD1984A0BEEF1004A7269A337C202E59E04E8E2AF714280D2F2D8D2BA5E6F59481B8DCD34AAF35C966A688D0B48EC7E96F102C274DC0D3B381E @@ -436,6 +440,9 @@ A2001C5E2F3D7EB6FFF5DD19E92925114DF28AE0E23357D811E7C82955751220C39AE73BFEB0EA0BC34C3AF95E27A1D39EBB9E7F5F9522F3957D269F72FD920E + + 5A630269AE2E19E48529A5D768DD9C01DCC9A91247A9D24CC777F0D2B79DBA06AC40AEAC4560C57BFCB6BBBD423621313013B742B78C24C8861490C957A4DC92 + BC7BC2A514F8CA104A392ECF8736F4A3D316EE988FA91299D33B0AF46134B38E14E4A5061449D17B2DF7A521643E6C02DFA37CC277ED7CAB7CE83C28C00E9719 diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 66aada028c..f0d59eae9f 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -34,10 +34,10 @@ ext { commonsMath3: '3.6.1', concurrentlinkedhashmap: '1.4.2', config: '1.4.1', - ehcache3: '3.9.0', + ehcache3: '3.9.1', errorprone: '2.5.1', errorproneJavac: '9+181-r4173-1', - elasticSearch: '7.11.0', + elasticSearch: '7.11.1', expiringMap: '0.5.9', fastfilter: '1.0', fastutil: '8.5.2', @@ -82,7 +82,7 @@ ext { ] pluginVersions = [ bnd: '5.2.0', - checkerFramework: '0.5.15', + checkerFramework: '0.5.16', checkstyle: '8.40', coveralls: '2.8.4', errorprone: '1.3.0', @@ -96,7 +96,7 @@ ext { shadow: '6.1.0', sonarqube: '3.1.1', spotbugs: '4.2.0', - spotbugsPlugin: '4.6.0', + spotbugsPlugin: '4.6.1', stats: '0.2.2', versions: '0.36.0', ]