Skip to content

Commit 50e4179

Browse files
committed
A re-work of the #82 that renamed a few things, tested more and fixed a few bugs
1 parent 11a7348 commit 50e4179

14 files changed

+679
-61
lines changed

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: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,37 +28,39 @@
2828
* implementation could also have used a regular {@link java.util.Map} instead of this {@link CacheMap}, but
2929
* this aligns better to the reference data loader implementation provided by Facebook
3030
* <p>
31-
* Also it doesn't require you to implement the full set of map overloads, just the required methods.
31+
* This is really a cache of completed {@link CompletableFuture} values in memory. It is used, when caching is enabled, to
32+
* 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
33+
* then you will want to use {{@link CachedValueStore}} which is designed for external cache access.
3234
*
33-
* @param <U> type parameter indicating the type of the cache keys
35+
* @param <K> type parameter indicating the type of the cache keys
3436
* @param <V> type parameter indicating the type of the data that is cached
3537
*
3638
* @author <a href="https://github.com/aschrijver/">Arnold Schrijver</a>
3739
* @author <a href="https://github.com/bbakerman/">Brad Baker</a>
3840
*/
3941
@PublicSpi
40-
public interface CacheMap<U, V> {
42+
public interface CacheMap<K, V> {
4143

4244
/**
4345
* Creates a new cache map, using the default implementation that is based on a {@link java.util.LinkedHashMap}.
4446
*
45-
* @param <U> type parameter indicating the type of the cache keys
47+
* @param <K> type parameter indicating the type of the cache keys
4648
* @param <V> type parameter indicating the type of the data that is cached
4749
*
4850
* @return the cache map
4951
*/
50-
static <U, V> CacheMap<U, CompletableFuture<V>> simpleMap() {
52+
static <K, V> CacheMap<K, V> simpleMap() {
5153
return new DefaultCacheMap<>();
5254
}
5355

5456
/**
55-
* Checks whether the specified key is contained in the cach map.
57+
* Checks whether the specified key is contained in the cache map.
5658
*
5759
* @param key the key to check
5860
*
5961
* @return {@code true} if the cache contains the key, {@code false} otherwise
6062
*/
61-
boolean containsKey(U key);
63+
boolean containsKey(K key);
6264

6365
/**
6466
* Gets the specified key from the cache map.
@@ -70,7 +72,7 @@ static <U, V> CacheMap<U, CompletableFuture<V>> simpleMap() {
7072
*
7173
* @return the cached value, or {@code null} if not found (depends on cache implementation)
7274
*/
73-
V get(U key);
75+
CompletableFuture<V> get(K key);
7476

7577
/**
7678
* Creates a new cache map entry with the specified key and value, or updates the value if the key already exists.
@@ -80,7 +82,7 @@ static <U, V> CacheMap<U, CompletableFuture<V>> simpleMap() {
8082
*
8183
* @return the cache map for fluent coding
8284
*/
83-
CacheMap<U, V> set(U key, V value);
85+
CacheMap<K, V> set(K key, CompletableFuture<V> value);
8486

8587
/**
8688
* Deletes the entry with the specified key from the cache map, if it exists.
@@ -89,12 +91,12 @@ static <U, V> CacheMap<U, CompletableFuture<V>> simpleMap() {
8991
*
9092
* @return the cache map for fluent coding
9193
*/
92-
CacheMap<U, V> delete(U key);
94+
CacheMap<K, V> delete(K key);
9395

9496
/**
9597
* Clears all entries of the cache map
9698
*
9799
* @return the cache map for fluent coding
98100
*/
99-
CacheMap<U, V> clear();
101+
CacheMap<K, V> clear();
100102
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package org.dataloader;
2+
3+
import org.dataloader.annotations.PublicSpi;
4+
import org.dataloader.impl.DefaultCachedValueStore;
5+
6+
import java.util.List;
7+
import java.util.concurrent.CompletableFuture;
8+
9+
/**
10+
* Cache value store for data loaders that use caching and want a long-lived or external cache.
11+
* <p>
12+
* The default implementation is a no-op store which replies with the key always missing and doesn't
13+
* store any actual results. This is to avoid duplicating the stored data between the {@link CacheMap}
14+
* and the store.
15+
* <p>
16+
* The API signature uses completable futures because the backing implementation MAY be a remote external cache
17+
* and hence exceptions may happen in retrieving values.
18+
*
19+
* @param <K> the type of cache keys
20+
* @param <V> the type of cache values
21+
*
22+
* @author <a href="https://github.com/craig-day">Craig Day</a>
23+
*/
24+
@PublicSpi
25+
public interface CachedValueStore<K, V> {
26+
27+
/**
28+
* Creates a new store, using the default no-op implementation.
29+
*
30+
* @param <K> the type of cache keys
31+
* @param <V> the type of cache values
32+
*
33+
* @return the cache store
34+
*/
35+
static <K, V> CachedValueStore<K, V> defaultStore() {
36+
return new DefaultCachedValueStore<>();
37+
}
38+
39+
/**
40+
* Checks whether the specified key is contained in the store.
41+
* <p>
42+
* {@link DataLoader} first calls {@link #containsKey(Object)} and then calls {@link #get(Object)}. If the
43+
* backing cache implementation cannot answer the `containsKey` call then simply return true and the
44+
* following `get` call can complete exceptionally to cause the {@link DataLoader}
45+
* to enqueue the key to the {@link BatchLoader#load(List)} call since it is not present in cache.
46+
*
47+
* @param key the key to check if its present in the cache
48+
*
49+
* @return {@code true} if the cache contains the key, {@code false} otherwise
50+
*/
51+
CompletableFuture<Boolean> containsKey(K key);
52+
53+
/**
54+
* Gets the specified key from the store.
55+
*
56+
* @param key the key to retrieve
57+
*
58+
* @return a future containing the cached value (which maybe null) or an exception if the key does
59+
* not exist in the cache.
60+
*
61+
* IMPORTANT: The future may fail if the key does not exist depending on implementation. Implementations should
62+
* return an exceptional future if the key is not present in the cache, not null which is a valid value
63+
* for a key.
64+
*/
65+
CompletableFuture<V> get(K key);
66+
67+
/**
68+
* Stores the value with the specified key, or updates it if the key already exists.
69+
*
70+
* @param key the key to store
71+
* @param value the value to store
72+
*
73+
* @return a future containing the stored value for fluent composition
74+
*/
75+
CompletableFuture<V> set(K key, V value);
76+
77+
/**
78+
* Deletes the entry with the specified key from the store, if it exists.
79+
*
80+
* @param key the key to delete
81+
*
82+
* @return a void future for error handling and fluent composition
83+
*/
84+
CompletableFuture<Void> delete(K key);
85+
86+
/**
87+
* Clears all entries from the store.
88+
*
89+
* @return a void future for error handling and fluent composition
90+
*/
91+
CompletableFuture<Void> clear();
92+
}

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

Lines changed: 42 additions & 5 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 CachedValueStore<Object, V> cachedValueStore;
6971

7072
/**
7173
* Creates new DataLoader with the specified batch loader function and default options
@@ -414,18 +416,23 @@ public DataLoader(BatchLoader<K, V> batchLoadFunction, DataLoaderOptions options
414416
DataLoader(Object batchLoadFunction, DataLoaderOptions options, Clock clock) {
415417
DataLoaderOptions loaderOptions = options == null ? new DataLoaderOptions() : options;
416418
this.futureCache = determineCacheMap(loaderOptions);
419+
this.cachedValueStore = determineCacheStore(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.cachedValueStore, 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> determineCacheMap(DataLoaderOptions loaderOptions) {
429+
return (CacheMap<Object, V>) loaderOptions.cacheMap().orElseGet(CacheMap::simpleMap);
427430
}
428431

432+
@SuppressWarnings("unchecked")
433+
private CachedValueStore<Object, V> determineCacheStore(DataLoaderOptions loaderOptions) {
434+
return (CachedValueStore<Object, V>) loaderOptions.cachedValueStore().orElseGet(CachedValueStore::defaultStore);
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+
cachedValueStore.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+
cachedValueStore.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 CachedValueStore#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)