Skip to content

Commit b45b1cc

Browse files
committed
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.
1 parent b792cc3 commit b45b1cc

9 files changed

Lines changed: 151 additions & 23 deletions

File tree

.github/workflows/codeql.yml

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,18 @@ jobs:
3131
steps:
3232
- name: Checkout repository
3333
uses: actions/checkout@v2
34+
- name: Setup Java JDK
35+
uses: actions/setup-java@v1
3436
with:
35-
# We must fetch at least the immediate parents so that if this is
36-
# a pull request then we can checkout the head.
37-
fetch-depth: 2
38-
39-
# If this run was triggered by a pull request event, then checkout
40-
# the head of the pull request instead of the merge commit.
41-
- run: git checkout HEAD^2
42-
if: ${{ github.event_name == 'pull_request' }}
37+
java-version: 11
4338

4439
# Initializes the CodeQL tools for scanning.
4540
- name: Initialize CodeQL
4641
uses: github/codeql-action/init@v1
4742
with:
4843
languages: ${{ matrix.language }}
4944
# If you wish to specify custom queries, you can do so here or in a config file.
50-
# By default, queries listed here will override any specified in a config file.
45+
# By default, queries listed here will override any specified in a config file.
5146
# Prefix the list here with "+" to use these queries and those in the config file.
5247
# queries: ./path/to/local/query, your-org/your-repo/queries@main
5348

caffeine/src/main/java/com/github/benmanes/caffeine/cache/LoadingCache.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,23 @@ public interface LoadingCache<K extends @NonNull Object, V extends @NonNull Obje
111111
* @throws NullPointerException if the specified key is null
112112
*/
113113
CompletableFuture<V> refresh(K key);
114+
115+
/**
116+
* Loads a new value for each {@code key}, asynchronously. While the new value is loading the
117+
* previous value (if any) will continue to be returned by {@code get(key)} unless it is evicted.
118+
* If the new value is loaded successfully it will replace the previous value in the cache; if an
119+
* exception is thrown while refreshing the previous value will remain, <i>and the exception will
120+
* be logged (using {@link System.Logger}) and swallowed</i>. If another thread is currently
121+
* loading the value for {@code key}, then does not perform an additional load.
122+
* <p>
123+
* Caches loaded by a {@link CacheLoader} will call {@link CacheLoader#reload} if the cache
124+
* currently contains a value for the {@code key}, and {@link CacheLoader#load} otherwise. Loading
125+
* is asynchronous by delegating to the default executor.
126+
*
127+
* @param keys the keys whose associated values are to be returned
128+
* @return the future containing an unmodifiable mapping of keys to values for the specified keys
129+
* that are loading the values
130+
* @throws NullPointerException if the specified collection is null or contains a null element
131+
*/
132+
CompletableFuture<Map<K, V>> refreshAll(Iterable<? extends K> keys);
114133
}

caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncCache.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ default CompletableFuture<V> get(K key, BiFunction<? super K, ? super Executor,
8080
}
8181

8282
@SuppressWarnings({"FutureReturnValueIgnored", "NullAway"})
83-
default CompletableFuture<V> get(K key,
84-
BiFunction<? super K, ? super Executor, ? extends CompletableFuture<? extends V>> mappingFunction, boolean recordStats) {
83+
default CompletableFuture<V> get(K key, BiFunction<? super K, ? super Executor,
84+
? extends CompletableFuture<? extends V>> mappingFunction, boolean recordStats) {
8585
long startTime = cache().statsTicker().read();
8686
@SuppressWarnings({"unchecked", "rawtypes"})
8787
CompletableFuture<? extends V>[] result = new CompletableFuture[1];
@@ -151,7 +151,7 @@ default CompletableFuture<Map<K, V>> getAll(Iterable<? extends K> keys,
151151
* combined mapping if successful. If any future fails then it is automatically removed from
152152
* the cache if still present.
153153
*/
154-
default CompletableFuture<Map<K, V>> composeResult(Map<K, CompletableFuture<V>> futures) {
154+
static <K, V> CompletableFuture<Map<K, V>> composeResult(Map<K, CompletableFuture<V>> futures) {
155155
if (futures.isEmpty()) {
156156
return CompletableFuture.completedFuture(Collections.emptyMap());
157157
}

caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.github.benmanes.caffeine.cache;
1717

18+
import static com.github.benmanes.caffeine.cache.LocalAsyncCache.composeResult;
1819
import static java.util.Objects.requireNonNull;
1920

2021
import java.lang.System.Logger;
@@ -195,6 +196,15 @@ public CompletableFuture<V> refresh(K key) {
195196
}
196197
}
197198

199+
@Override
200+
public CompletableFuture<Map<K, V>> refreshAll(Iterable<? extends K> keys) {
201+
Map<K, CompletableFuture<V>> result = new LinkedHashMap<>();
202+
for (K key : keys) {
203+
result.computeIfAbsent(key, this::refresh);
204+
}
205+
return composeResult(result);
206+
}
207+
198208
/** Attempts to avoid a reload if the entry is absent, or a load or reload is in-flight. */
199209
private @Nullable CompletableFuture<V> tryOptimisticRefresh(K key, Object keyReference) {
200210
// If a refresh is in-flight, then return it directly. If completed and not yet removed, then

caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.github.benmanes.caffeine.cache;
1717

18+
import static com.github.benmanes.caffeine.cache.LocalAsyncCache.composeResult;
1819
import static java.util.Objects.requireNonNull;
1920

2021
import java.lang.System.Logger;
@@ -175,6 +176,15 @@ default CompletableFuture<V> refresh(K key) {
175176
return castedFuture;
176177
}
177178

179+
@Override
180+
default CompletableFuture<Map<K, V>> refreshAll(Iterable<? extends K> keys) {
181+
Map<K, CompletableFuture<V>> result = new LinkedHashMap<>();
182+
for (K key : keys) {
183+
result.computeIfAbsent(key, this::refresh);
184+
}
185+
return composeResult(result);
186+
}
187+
178188
/** Returns a mapping function that adapts to {@link CacheLoader#load}. */
179189
static <K, V> Function<K, V> newMappingFunction(CacheLoader<? super K, V> cacheLoader) {
180190
return key -> {

caffeine/src/test/java/com/github/benmanes/caffeine/cache/LoadingCacheTest.java

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static java.util.stream.Collectors.toList;
2323
import static java.util.stream.Collectors.toMap;
2424
import static org.hamcrest.MatcherAssert.assertThat;
25+
import static org.hamcrest.Matchers.aMapWithSize;
2526
import static org.hamcrest.Matchers.containsInAnyOrder;
2627
import static org.hamcrest.Matchers.either;
2728
import static org.hamcrest.Matchers.empty;
@@ -370,14 +371,13 @@ public void refresh_ignored(LoadingCache<Integer, Integer> cache, CacheContext c
370371
}
371372

372373
@Test(dataProvider = "caches")
373-
@CacheSpec(executor = CacheExecutor.DIRECT, loader = Loader.EXCEPTIONAL,
374-
removalListener = { Listener.DEFAULT, Listener.REJECTING },
375-
population = { Population.SINGLETON, Population.PARTIAL, Population.FULL })
374+
@CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL },
375+
loader = Loader.EXCEPTIONAL, removalListener = { Listener.DEFAULT, Listener.REJECTING })
376376
public void refresh_failure(LoadingCache<Integer, Integer> cache, CacheContext context) {
377377
// Shouldn't leak exception to caller nor retain the future; should retain the stale entry
378378
var future1 = cache.refresh(context.absentKey());
379379
var future2 = cache.refresh(context.firstKey());
380-
var future3 = cache.refresh(context.firstKey());
380+
var future3 = cache.refresh(context.lastKey());
381381
assertThat(future2, is(not(sameInstance(future3))));
382382
assertThat(future1.isCompletedExceptionally(), is(true));
383383
assertThat(future2.isCompletedExceptionally(), is(true));
@@ -648,6 +648,66 @@ public void refresh_evicted(CacheContext context) {
648648
verifyStats(context, verifier -> verifier.success(1).failures(0));
649649
}
650650

651+
/* --------------- refreshAll --------------- */
652+
653+
@CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING })
654+
@Test(dataProvider = "caches", expectedExceptions = NullPointerException.class)
655+
public void refreshAll_null(LoadingCache<Integer, Integer> cache, CacheContext context) {
656+
cache.refreshAll(null).join();
657+
}
658+
659+
@CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING })
660+
@Test(dataProvider = "caches", expectedExceptions = NullPointerException.class)
661+
public void refreshAll_nullKey(LoadingCache<Integer, Integer> cache, CacheContext context) {
662+
cache.refreshAll(Collections.singletonList(null)).join();
663+
}
664+
665+
@Test(dataProvider = "caches")
666+
@CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING })
667+
public void refreshAll_absent(LoadingCache<Integer, Integer> cache, CacheContext context) {
668+
var result = cache.refreshAll(context.absentKeys()).join();
669+
int count = context.absentKeys().size();
670+
assertThat(result, aMapWithSize(count));
671+
assertThat(cache.asMap(), aMapWithSize(context.original().size() + count));
672+
}
673+
674+
@Test(dataProvider = "caches")
675+
@CacheSpec(population = Population.FULL, loader = Loader.IDENTITY,
676+
removalListener = { Listener.DEFAULT, Listener.REJECTING })
677+
public void refreshAll_present(LoadingCache<Integer, Integer> cache, CacheContext context) {
678+
var result = cache.refreshAll(context.original().keySet()).join();
679+
int count = context.original().keySet().size();
680+
assertThat(result, aMapWithSize(count));
681+
682+
var expected = context.original().keySet().stream().collect(toMap(identity(), identity()));
683+
assertThat(cache.asMap(), is(equalTo(expected)));
684+
}
685+
686+
@Test(dataProvider = "caches")
687+
@CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL },
688+
loader = Loader.EXCEPTIONAL, removalListener = { Listener.DEFAULT, Listener.REJECTING })
689+
public void refreshAll_failure(LoadingCache<Integer, Integer> cache, CacheContext context) {
690+
var future = cache.refreshAll(List.of(
691+
context.absentKey(), context.firstKey(), context.lastKey()));
692+
assertThat(future.isCompletedExceptionally(), is(true));
693+
assertThat(cache.estimatedSize(), is(context.initialSize()));
694+
}
695+
696+
@Test(dataProvider = "caches")
697+
@CacheSpec(loader = Loader.ASYNC_INCOMPLETE, implementation = Implementation.Caffeine,
698+
removalListener = { Listener.DEFAULT, Listener.REJECTING })
699+
public void refreshAll_cancel(LoadingCache<Integer, Integer> cache, CacheContext context) {
700+
var key = context.original().isEmpty() ? context.absentKey() : context.firstKey();
701+
var future1 = cache.refresh(key);
702+
var future2 = cache.refreshAll(List.of(key));
703+
704+
assertThat(future1.isDone(), is(false));
705+
future1.cancel(true);
706+
707+
assertThat(future2.isCompletedExceptionally(), is(true));
708+
assertThat(cache.asMap(), is(equalTo(context.original())));
709+
}
710+
651711
/* --------------- CacheLoader --------------- */
652712

653713
@Test(expectedExceptions = UnsupportedOperationException.class)

caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/GuavaCacheFromContext.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,33 @@ public CompletableFuture<V> refresh(K key) {
514514
error.remove();
515515
return CompletableFuture.failedFuture(e);
516516
}
517+
518+
@Override
519+
public CompletableFuture<Map<K, V>> refreshAll(Iterable<? extends K> keys) {
520+
Map<K, CompletableFuture<V>> result = new LinkedHashMap<>();
521+
for (K key : keys) {
522+
result.computeIfAbsent(key, this::refresh);
523+
}
524+
return composeResult(result);
525+
}
526+
527+
CompletableFuture<Map<K, V>> composeResult(Map<K, CompletableFuture<V>> futures) {
528+
if (futures.isEmpty()) {
529+
return CompletableFuture.completedFuture(Collections.emptyMap());
530+
}
531+
@SuppressWarnings("rawtypes")
532+
CompletableFuture<?>[] array = futures.values().toArray(new CompletableFuture[0]);
533+
return CompletableFuture.allOf(array).thenApply(ignored -> {
534+
Map<K, V> result = new LinkedHashMap<>(futures.size());
535+
futures.forEach((key, future) -> {
536+
V value = future.getNow(null);
537+
if (value != null) {
538+
result.put(key, value);
539+
}
540+
});
541+
return Collections.unmodifiableMap(result);
542+
});
543+
}
517544
}
518545

519546
static final class GuavaWeigher<K, V> implements Weigher<K, V>, Serializable {

checksum.xml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@
184184
<trusted-key id='b2c4d8b48a99f98a' group='org.jetbrains.kotlin' />
185185
<trusted-key id='66f8e4860bf74983' group='org.jooq' />
186186
<trusted-key id='145d819475314d97' group='org.json' />
187+
<trusted-key id='83d7db7c18913ca4' group='org.json' />
187188
<trusted-key id='8db9fa0b0718bbf9' group='org.jsr107.ri' />
188189
<trusted-key id='0315bfb7970a144f' group='org.jvnet.staxex' />
189190
<trusted-key id='f1f286fc184b6bda' group='org.kordamp.gradle' />
@@ -355,6 +356,9 @@
355356
<dependency group='gradle.plugin.com.github.spotbugs.snom' module='spotbugs-gradle-plugin' version='4.6.0'>
356357
<sha512>80BE5ADF1A1D5F182DA3BC718953D0149673C43844690DEA30FE00D83EA3886517E88473DA48619007932047FD45E461643EC07704B3B7C5B52D798801C6066E</sha512>
357358
</dependency>
359+
<dependency group='gradle.plugin.com.github.spotbugs.snom' module='spotbugs-gradle-plugin' version='4.6.1'>
360+
<sha512>97823390838111768E99E68A9EB8E603E528A910850EF58B1A9198828C5DE9C8F4BE53238DBED56C46A2471E5F1871FA4CD1D34F86FA44B4E3643BD3D03901A9</sha512>
361+
</dependency>
358362
<dependency group='gradle.plugin.com.github.spotbugs' module='spotbugs-gradle-plugin' version='2.0.0'>
359363
<sha512>B3BFAD07E6A3D4D73CBCE802D8614CF4AC84E589166D243D41028DC077F84C027DF4D514F145360405F37DA73A8F2E7B65D90877A9EE1151174D2440530F9051</sha512>
360364
</dependency>
@@ -364,12 +368,12 @@
364368
<dependency group='io.ehdev' module='gradle-semantic-versioning' version='1.1.0'>
365369
<sha512>1A47AAF2442159C1CBD22521F31C74B4C71C4168AF5B22D04B4691FDD286E90F02B2792DEDAD3EEEC12B5034ADA1A9EE751C975B5A169AE0B33EE800A8D96E7F</sha512>
366370
</dependency>
367-
<dependency group='io.github.gradle-nexus' module='publish-plugin' version='1.0.0'>
368-
<sha512>B57469B9FA2C5D598688FF9C6A3B3283496921B91E269EA6D5A1DA6511125BD2545E2CFA8D3736739787B0BACF69C0AF5C5ECDEA474CB9FD56C96495E654682B</sha512>
369-
</dependency>
370371
<dependency group='io.freefair.gradle' module='lombok-plugin' version='5.3.0'>
371372
<sha512>4C8808B0607564006379FBEB63BCEFC03A0F5FE83F307E455EE66B0B40AC238D14388CEA3C1D883835AF089238F824037A423124348571085C6D5415AB3981CF</sha512>
372373
</dependency>
374+
<dependency group='io.github.gradle-nexus' module='publish-plugin' version='1.0.0'>
375+
<sha512>B57469B9FA2C5D598688FF9C6A3B3283496921B91E269EA6D5A1DA6511125BD2545E2CFA8D3736739787B0BACF69C0AF5C5ECDEA474CB9FD56C96495E654682B</sha512>
376+
</dependency>
373377
<dependency group='javax.inject' module='javax.inject' version='1'>
374378
<sha512>E126B7CCF3E42FD1984A0BEEF1004A7269A337C202E59E04E8E2AF714280D2F2D8D2BA5E6F59481B8DCD34AAF35C966A688D0B48EC7E96F102C274DC0D3B381E</sha512>
375379
</dependency>
@@ -436,6 +440,9 @@
436440
<dependency group='org.checkerframework' module='checkerframework-gradle-plugin' version='0.5.15'>
437441
<sha512>A2001C5E2F3D7EB6FFF5DD19E92925114DF28AE0E23357D811E7C82955751220C39AE73BFEB0EA0BC34C3AF95E27A1D39EBB9E7F5F9522F3957D269F72FD920E</sha512>
438442
</dependency>
443+
<dependency group='org.checkerframework' module='checkerframework-gradle-plugin' version='0.5.16'>
444+
<sha512>5A630269AE2E19E48529A5D768DD9C01DCC9A91247A9D24CC777F0D2B79DBA06AC40AEAC4560C57BFCB6BBBD423621313013B742B78C24C8861490C957A4DC92</sha512>
445+
</dependency>
439446
<dependency group='org.codehaus.groovy.modules.http-builder' module='http-builder' version='0.7.1'>
440447
<sha512>BC7BC2A514F8CA104A392ECF8736F4A3D316EE988FA91299D33B0AF46134B38E14E4A5061449D17B2DF7A521643E6C02DFA37CC277ED7CAB7CE83C28C00E9719</sha512>
441448
</dependency>

gradle/dependencies.gradle

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ ext {
3434
commonsMath3: '3.6.1',
3535
concurrentlinkedhashmap: '1.4.2',
3636
config: '1.4.1',
37-
ehcache3: '3.9.0',
37+
ehcache3: '3.9.1',
3838
errorprone: '2.5.1',
3939
errorproneJavac: '9+181-r4173-1',
40-
elasticSearch: '7.11.0',
40+
elasticSearch: '7.11.1',
4141
expiringMap: '0.5.9',
4242
fastfilter: '1.0',
4343
fastutil: '8.5.2',
@@ -82,7 +82,7 @@ ext {
8282
]
8383
pluginVersions = [
8484
bnd: '5.2.0',
85-
checkerFramework: '0.5.15',
85+
checkerFramework: '0.5.16',
8686
checkstyle: '8.40',
8787
coveralls: '2.8.4',
8888
errorprone: '1.3.0',
@@ -96,7 +96,7 @@ ext {
9696
shadow: '6.1.0',
9797
sonarqube: '3.1.1',
9898
spotbugs: '4.2.0',
99-
spotbugsPlugin: '4.6.0',
99+
spotbugsPlugin: '4.6.1',
100100
stats: '0.2.2',
101101
versions: '0.36.0',
102102
]

0 commit comments

Comments
 (0)