Skip to content

Commit dfdc813

Browse files
committed
Added an option to stop the caching of exceptional values
1 parent f0506c4 commit dfdc813

File tree

3 files changed

+69
-4
lines changed

3 files changed

+69
-4
lines changed

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,14 @@ private CompletableFuture<List<V>> dispatchQueueBatch(List<K> keys, List<Object>
176176
.thenApply(values -> {
177177
assertResultSize(keys, values);
178178

179+
List<K> clearCacheKeys = new ArrayList<>();
179180
for (int idx = 0; idx < queuedFutures.size(); idx++) {
180181
Object value = values.get(idx);
181182
CompletableFuture<V> future = queuedFutures.get(idx);
182183
if (value instanceof Throwable) {
183184
stats.incrementLoadErrorCount();
184185
future.completeExceptionally((Throwable) value);
185-
// we don't clear the cached view of this entry to avoid
186-
// frequently loading the same error
186+
clearCacheKeys.add(keys.get(idx));
187187
} else if (value instanceof Try) {
188188
// we allow the batch loader to return a Try so we can better represent a computation
189189
// that might have worked or not.
@@ -193,12 +193,14 @@ private CompletableFuture<List<V>> dispatchQueueBatch(List<K> keys, List<Object>
193193
} else {
194194
stats.incrementLoadErrorCount();
195195
future.completeExceptionally(tryValue.getThrowable());
196+
clearCacheKeys.add(keys.get(idx));
196197
}
197198
} else {
198199
V val = (V) value;
199200
future.complete(val);
200201
}
201202
}
203+
possiblyClearCacheEntriesOnExceptions(clearCacheKeys);
202204
return values;
203205
}).exceptionally(ex -> {
204206
stats.incrementBatchLoadExceptionCount();
@@ -218,6 +220,19 @@ private void assertResultSize(List<K> keys, List<V> values) {
218220
assertState(keys.size() == values.size(), "The size of the promised values MUST be the same size as the key list");
219221
}
220222

223+
private void possiblyClearCacheEntriesOnExceptions(List<K> keys) {
224+
if (keys.isEmpty()) {
225+
return;
226+
}
227+
// by default we don't clear the cached view of this entry to avoid
228+
// frequently loading the same error. This works for short lived request caches
229+
// but might work against long lived caches. Hence we have an option that allows
230+
// it to be cleared
231+
if (!loaderOptions.cachingExceptionsEnabled()) {
232+
keys.forEach(dataLoader::clear);
233+
}
234+
}
235+
221236

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

src/main/java/org/dataloader/DataLoaderOptions.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class DataLoaderOptions {
3535

3636
private boolean batchingEnabled;
3737
private boolean cachingEnabled;
38+
private boolean cachingExceptionsEnabled;
3839
private CacheKey cacheKeyFunction;
3940
private CacheMap cacheMap;
4041
private int maxBatchSize;
@@ -47,6 +48,7 @@ public class DataLoaderOptions {
4748
public DataLoaderOptions() {
4849
batchingEnabled = true;
4950
cachingEnabled = true;
51+
cachingExceptionsEnabled = true;
5052
maxBatchSize = -1;
5153
statisticsCollector = SimpleStatisticsCollector::new;
5254
environmentProvider = NULL_PROVIDER;
@@ -61,6 +63,7 @@ public DataLoaderOptions(DataLoaderOptions other) {
6163
nonNull(other);
6264
this.batchingEnabled = other.batchingEnabled;
6365
this.cachingEnabled = other.cachingEnabled;
66+
this.cachingExceptionsEnabled = other.cachingExceptionsEnabled;
6467
this.cacheKeyFunction = other.cacheKeyFunction;
6568
this.cacheMap = other.cacheMap;
6669
this.maxBatchSize = other.maxBatchSize;
@@ -117,6 +120,32 @@ public DataLoaderOptions setCachingEnabled(boolean cachingEnabled) {
117120
return this;
118121
}
119122

123+
/**
124+
* Option that determines whether to cache exceptional values (the default), or not.
125+
*
126+
* For short lived caches (that is request caches) it makes sense to cache exceptions since
127+
* its likely the key is still poisoned. However if you have long lived caches, then it may make
128+
* sense to set this to false since the downstream system may have recovered from its failure
129+
* mode.
130+
*
131+
* @return {@code true} when exceptional values are cached is enabled, {@code false} otherwise
132+
*/
133+
public boolean cachingExceptionsEnabled() {
134+
return cachingExceptionsEnabled;
135+
}
136+
137+
/**
138+
* Sets the option that determines whether exceptional values are cachedis enabled.
139+
*
140+
* @param cachingExceptionsEnabled {@code true} to enable caching exceptional values, {@code false} otherwise
141+
*
142+
* @return the data loader options for fluent coding
143+
*/
144+
public DataLoaderOptions setCachingExceptionsEnabled(boolean cachingExceptionsEnabled) {
145+
this.cachingExceptionsEnabled = cachingExceptionsEnabled;
146+
return this;
147+
}
148+
120149
/**
121150
* Gets an (optional) function to invoke for creation of the cache key, if caching is enabled.
122151
* <p>

src/test/java/org/dataloader/DataLoaderTest.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
import static org.hamcrest.Matchers.equalTo;
4343
import static org.hamcrest.Matchers.instanceOf;
4444
import static org.hamcrest.Matchers.is;
45-
import static org.hamcrest.Matchers.nullValue;
4645
import static org.junit.Assert.assertArrayEquals;
4746
import static org.junit.Assert.assertThat;
4847

@@ -418,6 +417,29 @@ public void should_Cache_failed_fetches() {
418417
assertThat(loadCalls, equalTo(singletonList(singletonList(1))));
419418
}
420419

420+
@Test
421+
public void should_NOT_Cache_failed_fetches_if_told_not_too() {
422+
DataLoaderOptions options = DataLoaderOptions.newOptions().setCachingExceptionsEnabled(false);
423+
List<Collection<Integer>> loadCalls = new ArrayList<>();
424+
DataLoader<Integer, Object> errorLoader = idLoaderAllExceptions(options, loadCalls);
425+
426+
CompletableFuture<Object> future1 = errorLoader.load(1);
427+
errorLoader.dispatch();
428+
429+
await().until(future1::isDone);
430+
assertThat(future1.isCompletedExceptionally(), is(true));
431+
assertThat(cause(future1), instanceOf(IllegalStateException.class));
432+
433+
CompletableFuture<Object> future2 = errorLoader.load(1);
434+
errorLoader.dispatch();
435+
436+
await().until(future2::isDone);
437+
assertThat(future2.isCompletedExceptionally(), is(true));
438+
assertThat(cause(future2), instanceOf(IllegalStateException.class));
439+
440+
assertThat(loadCalls, equalTo(asList(singletonList(1), singletonList(1))));
441+
}
442+
421443

422444
// Accepts object key in custom cacheKey function
423445

@@ -968,7 +990,6 @@ public void should_allow_composition_of_data_loader_calls() throws Exception {
968990
}
969991

970992

971-
972993
private static CacheKey<JsonObject> getJsonObjectCacheMapFn() {
973994
return key -> key.stream()
974995
.map(entry -> entry.getKey() + ":" + entry.getValue())

0 commit comments

Comments
 (0)