Skip to content

Commit 2d99ee1

Browse files
committed
Merge remote-tracking branch 'origin/master' into scheduled-dataloader-registry
# Conflicts: # README.md
2 parents 2b781e6 + 7e2e609 commit 2d99ee1

15 files changed

+916
-375
lines changed

README.md

+301-308
Large diffs are not rendered by default.

build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ dependencies {
5858
testCompile 'org.slf4j:slf4j-simple:' + slf4jVersion
5959
testCompile "junit:junit:4.12"
6060
testCompile 'org.awaitility:awaitility:2.0.0'
61+
testImplementation 'com.github.ben-manes.caffeine:caffeine:2.9.0'
6162
}
6263

6364
task sourcesJar(type: Jar) {

src/main/java/org/dataloader/CacheMap.java

+16-15
Original file line numberDiff line numberDiff line change
@@ -22,43 +22,44 @@
2222
import java.util.concurrent.CompletableFuture;
2323

2424
/**
25-
* Cache map interface for data loaders that use caching.
25+
* CacheMap is used by data loaders that use caching promises to values aka {@link CompletableFuture}<V>. A better name for this
26+
* class might have been FutureCache but that is history now.
2627
* <p>
27-
* The default implementation used by the data loader is based on a {@link java.util.LinkedHashMap}. Note that the
28-
* implementation could also have used a regular {@link java.util.Map} instead of this {@link CacheMap}, but
29-
* this aligns better to the reference data loader implementation provided by Facebook
28+
* The default implementation used by the data loader is based on a {@link java.util.LinkedHashMap}.
3029
* <p>
31-
* Also it doesn't require you to implement the full set of map overloads, just the required methods.
30+
* This is really a cache of completed {@link CompletableFuture}&lt;V&gt; values in memory. It is used, when caching is enabled, to
31+
* give back the same future to any code that may call it. If you need a cache of the underlying values that is possible external to the JVM
32+
* then you will want to use {{@link ValueCache}} which is designed for external cache access.
3233
*
33-
* @param <U> type parameter indicating the type of the cache keys
34+
* @param <K> type parameter indicating the type of the cache keys
3435
* @param <V> type parameter indicating the type of the data that is cached
3536
*
3637
* @author <a href="https://github.com/aschrijver/">Arnold Schrijver</a>
3738
* @author <a href="https://github.com/bbakerman/">Brad Baker</a>
3839
*/
3940
@PublicSpi
40-
public interface CacheMap<U, V> {
41+
public interface CacheMap<K, V> {
4142

4243
/**
4344
* Creates a new cache map, using the default implementation that is based on a {@link java.util.LinkedHashMap}.
4445
*
45-
* @param <U> type parameter indicating the type of the cache keys
46+
* @param <K> type parameter indicating the type of the cache keys
4647
* @param <V> type parameter indicating the type of the data that is cached
4748
*
4849
* @return the cache map
4950
*/
50-
static <U, V> CacheMap<U, CompletableFuture<V>> simpleMap() {
51+
static <K, V> CacheMap<K, V> simpleMap() {
5152
return new DefaultCacheMap<>();
5253
}
5354

5455
/**
55-
* Checks whether the specified key is contained in the cach map.
56+
* Checks whether the specified key is contained in the cache map.
5657
*
5758
* @param key the key to check
5859
*
5960
* @return {@code true} if the cache contains the key, {@code false} otherwise
6061
*/
61-
boolean containsKey(U key);
62+
boolean containsKey(K key);
6263

6364
/**
6465
* Gets the specified key from the cache map.
@@ -70,7 +71,7 @@ static <U, V> CacheMap<U, CompletableFuture<V>> simpleMap() {
7071
*
7172
* @return the cached value, or {@code null} if not found (depends on cache implementation)
7273
*/
73-
V get(U key);
74+
CompletableFuture<V> get(K key);
7475

7576
/**
7677
* Creates a new cache map entry with the specified key and value, or updates the value if the key already exists.
@@ -80,7 +81,7 @@ static <U, V> CacheMap<U, CompletableFuture<V>> simpleMap() {
8081
*
8182
* @return the cache map for fluent coding
8283
*/
83-
CacheMap<U, V> set(U key, V value);
84+
CacheMap<K, V> set(K key, CompletableFuture<V> value);
8485

8586
/**
8687
* Deletes the entry with the specified key from the cache map, if it exists.
@@ -89,12 +90,12 @@ static <U, V> CacheMap<U, CompletableFuture<V>> simpleMap() {
8990
*
9091
* @return the cache map for fluent coding
9192
*/
92-
CacheMap<U, V> delete(U key);
93+
CacheMap<K, V> delete(K key);
9394

9495
/**
9596
* Clears all entries of the cache map
9697
*
9798
* @return the cache map for fluent coding
9899
*/
99-
CacheMap<U, V> clear();
100+
CacheMap<K, V> clear();
100101
}

src/main/java/org/dataloader/DataLoader.java

+43-6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.List;
3131
import java.util.Optional;
3232
import java.util.concurrent.CompletableFuture;
33+
import java.util.function.BiConsumer;
3334

3435
import static org.dataloader.impl.Assertions.nonNull;
3536

@@ -64,8 +65,9 @@
6465
public class DataLoader<K, V> {
6566

6667
private final DataLoaderHelper<K, V> helper;
67-
private final CacheMap<Object, CompletableFuture<V>> futureCache;
6868
private final StatisticsCollector stats;
69+
private final CacheMap<Object, V> futureCache;
70+
private final ValueCache<Object, V> valueCache;
6971

7072
/**
7173
* Creates new DataLoader with the specified batch loader function and default options
@@ -413,19 +415,24 @@ public DataLoader(BatchLoader<K, V> batchLoadFunction, DataLoaderOptions options
413415
@VisibleForTesting
414416
DataLoader(Object batchLoadFunction, DataLoaderOptions options, Clock clock) {
415417
DataLoaderOptions loaderOptions = options == null ? new DataLoaderOptions() : options;
416-
this.futureCache = determineCacheMap(loaderOptions);
418+
this.futureCache = determineFutureCache(loaderOptions);
419+
this.valueCache = determineValueCache(loaderOptions);
417420
// order of keys matter in data loader
418421
this.stats = nonNull(loaderOptions.getStatisticsCollector());
419422

420-
this.helper = new DataLoaderHelper<>(this, batchLoadFunction, loaderOptions, this.futureCache, this.stats, clock);
423+
this.helper = new DataLoaderHelper<>(this, batchLoadFunction, loaderOptions, this.futureCache, this.valueCache, this.stats, clock);
421424
}
422425

423426

424427
@SuppressWarnings("unchecked")
425-
private CacheMap<Object, CompletableFuture<V>> determineCacheMap(DataLoaderOptions loaderOptions) {
426-
return loaderOptions.cacheMap().isPresent() ? (CacheMap<Object, CompletableFuture<V>>) loaderOptions.cacheMap().get() : CacheMap.simpleMap();
428+
private CacheMap<Object, V> determineFutureCache(DataLoaderOptions loaderOptions) {
429+
return (CacheMap<Object, V>) loaderOptions.cacheMap().orElseGet(CacheMap::simpleMap);
427430
}
428431

432+
@SuppressWarnings("unchecked")
433+
private ValueCache<Object, V> determineValueCache(DataLoaderOptions loaderOptions) {
434+
return (ValueCache<Object, V>) loaderOptions.valueCache().orElseGet(ValueCache::defaultValueCache);
435+
}
429436

430437
/**
431438
* This returns the last instant the data loader was dispatched. When the data loader is created this value is set to now.
@@ -628,9 +635,24 @@ public int dispatchDepth() {
628635
* @return the data loader for fluent coding
629636
*/
630637
public DataLoader<K, V> clear(K key) {
638+
return clear(key, (v, e) -> {
639+
});
640+
}
641+
642+
/**
643+
* Clears the future with the specified key from the cache remote value store, if caching is enabled
644+
* and a remote store is set, so it will be re-fetched and stored on the next load request.
645+
*
646+
* @param key the key to remove
647+
* @param handler a handler that will be called after the async remote clear completes
648+
*
649+
* @return the data loader for fluent coding
650+
*/
651+
public DataLoader<K, V> clear(K key, BiConsumer<Void, Throwable> handler) {
631652
Object cacheKey = getCacheKey(key);
632653
synchronized (this) {
633654
futureCache.delete(cacheKey);
655+
valueCache.delete(key).whenComplete(handler);
634656
}
635657
return this;
636658
}
@@ -641,14 +663,29 @@ public DataLoader<K, V> clear(K key) {
641663
* @return the data loader for fluent coding
642664
*/
643665
public DataLoader<K, V> clearAll() {
666+
return clearAll((v, e) -> {
667+
});
668+
}
669+
670+
/**
671+
* Clears the entire cache map of the loader, and of the cached value store.
672+
*
673+
* @param handler a handler that will be called after the async remote clear all completes
674+
*
675+
* @return the data loader for fluent coding
676+
*/
677+
public DataLoader<K, V> clearAll(BiConsumer<Void, Throwable> handler) {
644678
synchronized (this) {
645679
futureCache.clear();
680+
valueCache.clear().whenComplete(handler);
646681
}
647682
return this;
648683
}
649684

650685
/**
651-
* Primes the cache with the given key and value.
686+
* Primes the cache with the given key and value. Note this will only prime the future cache
687+
* and not the value store. Use {@link ValueCache#set(Object, Object)} if you want
688+
* o prime it with values before use
652689
*
653690
* @param key the key
654691
* @param value the value

src/main/java/org/dataloader/DataLoaderHelper.java

+73-26
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.util.concurrent.CompletableFuture;
1717
import java.util.concurrent.CompletionStage;
1818
import java.util.concurrent.atomic.AtomicReference;
19+
import java.util.function.BiConsumer;
1920
import java.util.stream.Collectors;
2021

2122
import static java.util.Collections.emptyList;
@@ -61,17 +62,25 @@ Object getCallContext() {
6162
private final DataLoader<K, V> dataLoader;
6263
private final Object batchLoadFunction;
6364
private final DataLoaderOptions loaderOptions;
64-
private final CacheMap<Object, CompletableFuture<V>> futureCache;
65+
private final CacheMap<Object, V> futureCache;
66+
private final ValueCache<Object, V> valueCache;
6567
private final List<LoaderQueueEntry<K, CompletableFuture<V>>> loaderQueue;
6668
private final StatisticsCollector stats;
6769
private final Clock clock;
6870
private final AtomicReference<Instant> lastDispatchTime;
6971

70-
DataLoaderHelper(DataLoader<K, V> dataLoader, Object batchLoadFunction, DataLoaderOptions loaderOptions, CacheMap<Object, CompletableFuture<V>> futureCache, StatisticsCollector stats, Clock clock) {
72+
DataLoaderHelper(DataLoader<K, V> dataLoader,
73+
Object batchLoadFunction,
74+
DataLoaderOptions loaderOptions,
75+
CacheMap<Object, V> futureCache,
76+
ValueCache<Object, V> valueCache,
77+
StatisticsCollector stats,
78+
Clock clock) {
7179
this.dataLoader = dataLoader;
7280
this.batchLoadFunction = batchLoadFunction;
7381
this.loaderOptions = loaderOptions;
7482
this.futureCache = futureCache;
83+
this.valueCache = valueCache;
7584
this.loaderQueue = new ArrayList<>();
7685
this.stats = stats;
7786
this.clock = clock;
@@ -120,35 +129,13 @@ CompletableFuture<V> load(K key, Object loadContext) {
120129
boolean batchingEnabled = loaderOptions.batchingEnabled();
121130
boolean cachingEnabled = loaderOptions.cachingEnabled();
122131

123-
Object cacheKey = null;
124-
if (cachingEnabled) {
125-
if (loadContext == null) {
126-
cacheKey = getCacheKey(key);
127-
} else {
128-
cacheKey = getCacheKeyWithContext(key, loadContext);
129-
}
130-
}
131132
stats.incrementLoadCount();
132133

133134
if (cachingEnabled) {
134-
if (futureCache.containsKey(cacheKey)) {
135-
stats.incrementCacheHitCount();
136-
return futureCache.get(cacheKey);
137-
}
138-
}
139-
140-
CompletableFuture<V> future = new CompletableFuture<>();
141-
if (batchingEnabled) {
142-
loaderQueue.add(new LoaderQueueEntry<>(key, future, loadContext));
135+
return loadFromCache(key, loadContext, batchingEnabled);
143136
} else {
144-
stats.incrementBatchLoadCountBy(1);
145-
// immediate execution of batch function
146-
future = invokeLoaderImmediately(key, loadContext);
147-
}
148-
if (cachingEnabled) {
149-
futureCache.set(cacheKey, future);
137+
return queueOrInvokeLoader(key, loadContext, batchingEnabled);
150138
}
151-
return future;
152139
}
153140
}
154141

@@ -296,6 +283,66 @@ private void possiblyClearCacheEntriesOnExceptions(List<K> keys) {
296283
}
297284
}
298285

286+
private CompletableFuture<V> loadFromCache(K key, Object loadContext, boolean batchingEnabled) {
287+
final Object cacheKey = loadContext == null ? getCacheKey(key) : getCacheKeyWithContext(key, loadContext);
288+
289+
if (futureCache.containsKey(cacheKey)) {
290+
// We already have a promise for this key, no need to check value cache or queue up load
291+
stats.incrementCacheHitCount();
292+
return futureCache.get(cacheKey);
293+
}
294+
295+
/*
296+
We haven't been asked for this key yet. We want to do one of two things:
297+
298+
1. Check if our cache store has it. If so:
299+
a. Get the value from the cache store
300+
b. Add a recovery case so we queue the load if fetching from cache store fails
301+
c. Put that future in our futureCache to hit the early return next time
302+
d. Return the resilient future
303+
2. If not in value cache:
304+
a. queue or invoke the load
305+
b. Add a success handler to store the result in the cache store
306+
c. Return the result
307+
*/
308+
final CompletableFuture<V> future = new CompletableFuture<>();
309+
310+
valueCache.get(cacheKey).whenComplete((cachedValue, getCallEx) -> {
311+
if (getCallEx == null) {
312+
future.complete(cachedValue);
313+
} else {
314+
queueOrInvokeLoader(key, loadContext, batchingEnabled)
315+
.whenComplete(setValueIntoCacheAndCompleteFuture(cacheKey, future));
316+
}
317+
});
318+
319+
futureCache.set(cacheKey, future);
320+
321+
return future;
322+
}
323+
324+
private BiConsumer<V, Throwable> setValueIntoCacheAndCompleteFuture(Object cacheKey, CompletableFuture<V> future) {
325+
return (result, loadCallEx) -> {
326+
if (loadCallEx == null) {
327+
valueCache.set(cacheKey, result)
328+
.whenComplete((v, setCallExIgnored) -> future.complete(result));
329+
} else {
330+
future.completeExceptionally(loadCallEx);
331+
}
332+
};
333+
}
334+
335+
private CompletableFuture<V> queueOrInvokeLoader(K key, Object loadContext, boolean batchingEnabled) {
336+
if (batchingEnabled) {
337+
CompletableFuture<V> future = new CompletableFuture<>();
338+
loaderQueue.add(new LoaderQueueEntry<>(key, future, loadContext));
339+
return future;
340+
} else {
341+
stats.incrementBatchLoadCountBy(1);
342+
// immediate execution of batch function
343+
return invokeLoaderImmediately(key, loadContext);
344+
}
345+
}
299346

300347
CompletableFuture<V> invokeLoaderImmediately(K key, Object keyContext) {
301348
List<K> keys = singletonList(key);

0 commit comments

Comments
 (0)