Skip to content

Commit

Permalink
Add refreshAll(keys) convenience method (bulk not supported)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ben-manes committed Feb 21, 2021
1 parent b792cc3 commit 157a91b
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 15 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,18 @@ jobs:
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}

- name: Setup Java JDK
uses: actions/[email protected]
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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,23 @@ public interface LoadingCache<K extends @NonNull Object, V extends @NonNull Obje
* @throws NullPointerException if the specified key is null
*/
CompletableFuture<V> 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, <i>and the exception will
* be logged (using {@link System.Logger}) and swallowed</i>. If another thread is currently
* loading the value for {@code key}, then does not perform an additional load.
* <p>
* 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<Map<K, V>> refreshAll(Iterable<? extends K> keys);
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ default CompletableFuture<V> get(K key, BiFunction<? super K, ? super Executor,
}

@SuppressWarnings({"FutureReturnValueIgnored", "NullAway"})
default CompletableFuture<V> get(K key,
BiFunction<? super K, ? super Executor, ? extends CompletableFuture<? extends V>> mappingFunction, boolean recordStats) {
default CompletableFuture<V> get(K key, BiFunction<? super K, ? super Executor,
? extends CompletableFuture<? extends V>> mappingFunction, boolean recordStats) {
long startTime = cache().statsTicker().read();
@SuppressWarnings({"unchecked", "rawtypes"})
CompletableFuture<? extends V>[] result = new CompletableFuture[1];
Expand Down Expand Up @@ -151,7 +151,7 @@ default CompletableFuture<Map<K, V>> getAll(Iterable<? extends K> keys,
* combined mapping if successful. If any future fails then it is automatically removed from
* the cache if still present.
*/
default CompletableFuture<Map<K, V>> composeResult(Map<K, CompletableFuture<V>> futures) {
static <K, V> CompletableFuture<Map<K, V>> composeResult(Map<K, CompletableFuture<V>> futures) {
if (futures.isEmpty()) {
return CompletableFuture.completedFuture(Collections.emptyMap());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -195,6 +196,15 @@ public CompletableFuture<V> refresh(K key) {
}
}

@Override
public CompletableFuture<Map<K, V>> refreshAll(Iterable<? extends K> keys) {
Map<K, CompletableFuture<V>> 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<V> tryOptimisticRefresh(K key, Object keyReference) {
// If a refresh is in-flight, then return it directly. If completed and not yet removed, then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -175,6 +176,15 @@ default CompletableFuture<V> refresh(K key) {
return castedFuture;
}

@Override
default CompletableFuture<Map<K, V>> refreshAll(Iterable<? extends K> keys) {
Map<K, CompletableFuture<V>> 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 <K, V> Function<K, V> newMappingFunction(CacheLoader<? super K, V> cacheLoader) {
return key -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -370,14 +371,13 @@ public void refresh_ignored(LoadingCache<Integer, Integer> 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<Integer, Integer> 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));
Expand Down Expand Up @@ -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<Integer, Integer> cache, CacheContext context) {
cache.refreshAll(null).join();
}

@CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING })
@Test(dataProvider = "caches", expectedExceptions = NullPointerException.class)
public void refreshAll_nullKey(LoadingCache<Integer, Integer> cache, CacheContext context) {
cache.refreshAll(Collections.singletonList(null)).join();
}

@Test(dataProvider = "caches")
@CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING })
public void refreshAll_absent(LoadingCache<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,33 @@ public CompletableFuture<V> refresh(K key) {
error.remove();
return CompletableFuture.failedFuture(e);
}

@Override
public CompletableFuture<Map<K, V>> refreshAll(Iterable<? extends K> keys) {
Map<K, CompletableFuture<V>> result = new LinkedHashMap<>();
for (K key : keys) {
result.computeIfAbsent(key, this::refresh);
}
return composeResult(result);
}

CompletableFuture<Map<K, V>> composeResult(Map<K, CompletableFuture<V>> 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<K, V> 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<K, V> implements Weigher<K, V>, Serializable {
Expand Down
13 changes: 10 additions & 3 deletions checksum.xml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
<trusted-key id='b2c4d8b48a99f98a' group='org.jetbrains.kotlin' />
<trusted-key id='66f8e4860bf74983' group='org.jooq' />
<trusted-key id='145d819475314d97' group='org.json' />
<trusted-key id='83d7db7c18913ca4' group='org.json' />
<trusted-key id='8db9fa0b0718bbf9' group='org.jsr107.ri' />
<trusted-key id='0315bfb7970a144f' group='org.jvnet.staxex' />
<trusted-key id='f1f286fc184b6bda' group='org.kordamp.gradle' />
Expand Down Expand Up @@ -355,6 +356,9 @@
<dependency group='gradle.plugin.com.github.spotbugs.snom' module='spotbugs-gradle-plugin' version='4.6.0'>
<sha512>80BE5ADF1A1D5F182DA3BC718953D0149673C43844690DEA30FE00D83EA3886517E88473DA48619007932047FD45E461643EC07704B3B7C5B52D798801C6066E</sha512>
</dependency>
<dependency group='gradle.plugin.com.github.spotbugs.snom' module='spotbugs-gradle-plugin' version='4.6.1'>
<sha512>97823390838111768E99E68A9EB8E603E528A910850EF58B1A9198828C5DE9C8F4BE53238DBED56C46A2471E5F1871FA4CD1D34F86FA44B4E3643BD3D03901A9</sha512>
</dependency>
<dependency group='gradle.plugin.com.github.spotbugs' module='spotbugs-gradle-plugin' version='2.0.0'>
<sha512>B3BFAD07E6A3D4D73CBCE802D8614CF4AC84E589166D243D41028DC077F84C027DF4D514F145360405F37DA73A8F2E7B65D90877A9EE1151174D2440530F9051</sha512>
</dependency>
Expand All @@ -364,12 +368,12 @@
<dependency group='io.ehdev' module='gradle-semantic-versioning' version='1.1.0'>
<sha512>1A47AAF2442159C1CBD22521F31C74B4C71C4168AF5B22D04B4691FDD286E90F02B2792DEDAD3EEEC12B5034ADA1A9EE751C975B5A169AE0B33EE800A8D96E7F</sha512>
</dependency>
<dependency group='io.github.gradle-nexus' module='publish-plugin' version='1.0.0'>
<sha512>B57469B9FA2C5D598688FF9C6A3B3283496921B91E269EA6D5A1DA6511125BD2545E2CFA8D3736739787B0BACF69C0AF5C5ECDEA474CB9FD56C96495E654682B</sha512>
</dependency>
<dependency group='io.freefair.gradle' module='lombok-plugin' version='5.3.0'>
<sha512>4C8808B0607564006379FBEB63BCEFC03A0F5FE83F307E455EE66B0B40AC238D14388CEA3C1D883835AF089238F824037A423124348571085C6D5415AB3981CF</sha512>
</dependency>
<dependency group='io.github.gradle-nexus' module='publish-plugin' version='1.0.0'>
<sha512>B57469B9FA2C5D598688FF9C6A3B3283496921B91E269EA6D5A1DA6511125BD2545E2CFA8D3736739787B0BACF69C0AF5C5ECDEA474CB9FD56C96495E654682B</sha512>
</dependency>
<dependency group='javax.inject' module='javax.inject' version='1'>
<sha512>E126B7CCF3E42FD1984A0BEEF1004A7269A337C202E59E04E8E2AF714280D2F2D8D2BA5E6F59481B8DCD34AAF35C966A688D0B48EC7E96F102C274DC0D3B381E</sha512>
</dependency>
Expand Down Expand Up @@ -436,6 +440,9 @@
<dependency group='org.checkerframework' module='checkerframework-gradle-plugin' version='0.5.15'>
<sha512>A2001C5E2F3D7EB6FFF5DD19E92925114DF28AE0E23357D811E7C82955751220C39AE73BFEB0EA0BC34C3AF95E27A1D39EBB9E7F5F9522F3957D269F72FD920E</sha512>
</dependency>
<dependency group='org.checkerframework' module='checkerframework-gradle-plugin' version='0.5.16'>
<sha512>5A630269AE2E19E48529A5D768DD9C01DCC9A91247A9D24CC777F0D2B79DBA06AC40AEAC4560C57BFCB6BBBD423621313013B742B78C24C8861490C957A4DC92</sha512>
</dependency>
<dependency group='org.codehaus.groovy.modules.http-builder' module='http-builder' version='0.7.1'>
<sha512>BC7BC2A514F8CA104A392ECF8736F4A3D316EE988FA91299D33B0AF46134B38E14E4A5061449D17B2DF7A521643E6C02DFA37CC277ED7CAB7CE83C28C00E9719</sha512>
</dependency>
Expand Down
8 changes: 4 additions & 4 deletions gradle/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
]
Expand Down

0 comments on commit 157a91b

Please sign in to comment.