Skip to content

Commit 7e2e609

Browse files
authored
Merge pull request #88 from graphql-java/value-cache-support
Value cache support
2 parents 11a7348 + 0f3dbef commit 7e2e609

15 files changed

+664
-88
lines changed

README.md

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ a list of user ids in one call.
144144
This is important consideration. By using `dataloader` you have batched up the requests for N keys in a list of keys that can be
145145
retrieved at one time.
146146

147-
If you don't have batched backing services, then you cant be as efficient as possible as you will have to make N calls for each key.
147+
If you don't have batched backing services, then you can't be as efficient as possible as you will have to make N calls for each key.
148148

149149
```java
150150
BatchLoader<Long, User> lessEfficientUserBatchLoader = new BatchLoader<Long, User>() {
@@ -313,6 +313,49 @@ and some of which may have failed. From that data loader can infer the right be
313313
On the above example if one of the `Try` objects represents a failure, then its `load()` promise will complete exceptionally and you can
314314
react to that, in a type safe manner.
315315

316+
## Caching
317+
318+
`DataLoader` has a two tiered caching system in place.
319+
320+
The first cache is represented by the interface `org.dataloader.CacheMap`. It will cache `CompletableFuture`s by key and hence future `load(key)` calls
321+
will be given the same future and hence the same value.
322+
323+
This cache can only work local to the JVM, since its caches `CompletableFuture`s which cannot be serialised across a network say.
324+
325+
The second level cache is a value cache represented by the interface `org.dataloader.ValueCache`. By default, this is not enabled and is a no-op.
326+
327+
The value cache uses an async API pattern to encapsulate the idea that the value cache could be in a remote place such as REDIS or Memcached.
328+
329+
## Custom future caches
330+
331+
The default future cache behind `DataLoader` is an in memory `HashMap`. There is no expiry on this, and it lives for as long as the data loader
332+
lives.
333+
334+
However, you can create your own custom cache and supply it to the data loader on construction via the `org.dataloader.CacheMap` interface.
335+
336+
```java
337+
MyCustomCache customCache = new MyCustomCache();
338+
DataLoaderOptions options = DataLoaderOptions.newOptions().setCacheMap(customCache);
339+
DataLoaderFactory.newDataLoader(userBatchLoader, options);
340+
```
341+
342+
You could choose to use one of the fancy cache implementations from Guava or Caffeine and wrap it in a `CacheMap` wrapper ready
343+
for data loader. They can do fancy things like time eviction and efficient LRU caching.
344+
345+
As stated above, a custom `org.dataloader.CacheMap` is a local cache of futures with values, not values per se.
346+
347+
## Custom value caches
348+
349+
You will need to create your own implementations of the `org.dataloader.ValueCache` if your want to use an external cache.
350+
351+
This library does not ship with any implementations of `ValueCache` because it does not want to have
352+
production dependencies on external cache libraries, but you can easily write your own.
353+
354+
The tests have an example based on [Caffeine](https://github.com/ben-manes/caffeine).
355+
356+
The API of `ValueCache` has been designed to be asynchronous because it is expected that the value cache could be outside
357+
your JVM. It uses `CompleteableFuture`s to get and set values into cache, which may involve a network call and hence exceptional failures to get
358+
or set values.
316359

317360

318361
## Disabling caching
@@ -346,7 +389,7 @@ More complex cache behavior can be achieved by calling `.clear()` or `.clearAll(
346389
## Caching errors
347390

348391
If a batch load fails (that is, a batch function returns a rejected CompletionStage), then the requested values will not be cached.
349-
However if a batch function returns a `Try` or `Throwable` instance for an individual value, then that will be cached to avoid frequently loading
392+
However, if a batch function returns a `Try` or `Throwable` instance for an individual value, then that will be cached to avoid frequently loading
350393
the same problem object.
351394

352395
In some circumstances you may wish to clear the cache for these individual problems:
@@ -406,33 +449,18 @@ If your data can be shared across web requests then use a custom cache to keep v
406449

407450
Data loaders are stateful components that contain promises (with context) that are likely share the same affinity as the request.
408451

409-
## Custom caches
410-
411-
The default cache behind `DataLoader` is an in memory `HashMap`. There is no expiry on this, and it lives for as long as the data loader
412-
lives.
413-
414-
However, you can create your own custom cache and supply it to the data loader on construction via the `org.dataloader.CacheMap` interface.
415-
416-
```java
417-
MyCustomCache customCache = new MyCustomCache();
418-
DataLoaderOptions options = DataLoaderOptions.newOptions().setCacheMap(customCache);
419-
DataLoaderFactory.newDataLoader(userBatchLoader, options);
420-
```
421-
422-
You could choose to use one of the fancy cache implementations from Guava or Kaffeine and wrap it in a `CacheMap` wrapper ready
423-
for data loader. They can do fancy things like time eviction and efficient LRU caching.
424-
425452
## Manual dispatching
426453

427-
The original [Facebook DataLoader](https://github.com/facebook/dataloader) was written in Javascript for NodeJS. NodeJS is single-threaded in nature, but simulates
428-
asynchronous logic by invoking functions on separate threads in an event loop, as explained
454+
The original [Facebook DataLoader](https://github.com/facebook/dataloader) was written in Javascript for NodeJS.
455+
456+
NodeJS is single-threaded in nature, but simulates asynchronous logic by invoking functions on separate threads in an event loop, as explained
429457
[in this post](http://stackoverflow.com/a/19823583/3455094) on StackOverflow.
430458

431459
NodeJS generates so-call 'ticks' in which queued functions are dispatched for execution, and Facebook `DataLoader` uses
432460
the `nextTick()` function in NodeJS to _automatically_ dequeue load requests and send them to the batch execution function
433461
for processing.
434462

435-
And here there is an **IMPORTANT DIFFERENCE** compared to how `java-dataloader` operates!!
463+
Here there is an **IMPORTANT DIFFERENCE** compared to how `java-dataloader` operates!!
436464

437465
In NodeJS the batch preparation will not affect the asynchronous processing behaviour in any way. It will just prepare
438466
batches in 'spare time' as it were.

build.gradle

Lines changed: 1 addition & 0 deletions
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

Lines changed: 16 additions & 15 deletions
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}&lt;V&gt;. 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

Lines changed: 43 additions & 6 deletions
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

0 commit comments

Comments
 (0)