Skip to content

Commit fbb35ef

Browse files
authored
Now with Try support in data loaders and way more doco (#9)
* Now with Try support in data loaders and way more doco * More Try code and Java doc
1 parent 232a21a commit fbb35ef

10 files changed

+865
-44
lines changed

README.md

Lines changed: 133 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ and Nicholas Schrock (@schrockn) from [Facebook](https://www.facebook.com/), the
2525

2626
- [Features](#features)
2727
- [Examples](#examples)
28-
- [Differences to reference implementation](#differences-to-reference-implementation)
29-
- [Manual dispatching](#manual-dispatching)
3028
- [Let's get started!](#lets-get-started)
3129
- [Installing](#installing)
3230
- [Building](#building)
@@ -290,9 +288,136 @@ this was not in place, then all the promises to data will never be dispatched ot
290288

291289
See below for more details on `dataLoader.dispatch()`
292290

293-
## Differences to reference implementation
291+
### Error object is not a thing in a type safe Java world
292+
293+
In the reference JS implementation if the batch loader returns an `Error` object back from the `load()` promise is rejected
294+
with that error. This allows fine grain (per object in the list) sets of error. If I ask for keys A,B,C and B errors out the promise
295+
for B can contain a specific error.
296+
297+
This is not quite as loose in a Java implementation as Java is a type safe language.
298+
299+
A batch loader function is defined as `BatchLoader<K, V>` meaning for a key of type `K` it returns a value of type `V`.
300+
301+
It cant just return some `Exception` as an object of type `V`. Type safety matters.
302+
303+
However you can use the `Try` data type which can encapsulate a computation that succeeded or returned an exception.
294304

295-
### Manual dispatching
305+
```java
306+
Try<String> tryS = Try.tryCall(() -> {
307+
if (rollDice()) {
308+
return "OK";
309+
} else {
310+
throw new RuntimeException("Bang");
311+
}
312+
});
313+
314+
if (tryS.isSuccess()) {
315+
System.out.println("It work " + tryS.get());
316+
} else {
317+
System.out.println("It failed with exception : " + tryS.getThrowable());
318+
319+
}
320+
```
321+
322+
DataLoader supports this type and you can use this form to create a batch loader that returns a list of `Try` objects, some of which may have succeeded
323+
and some of which may have failed. From that data loader can infer the right behavior in terms of the `load(x)` promise.
324+
325+
```java
326+
DataLoader<String, User> dataLoader = DataLoader.newDataLoaderWithTry(new BatchLoader<String, Try<User>>() {
327+
@Override
328+
public CompletionStage<List<Try<User>>> load(List<String> keys) {
329+
return CompletableFuture.supplyAsync(() -> {
330+
List<Try<User>> users = new ArrayList<>();
331+
for (String key : keys) {
332+
Try<User> userTry = loadUser(key);
333+
users.add(userTry);
334+
}
335+
return users;
336+
});
337+
}
338+
});
339+
340+
```
341+
342+
On the above example if one of the `Try` objects represents a failure, then its `load()` promise will complete exceptionally and you can
343+
react to that, in a type safe manner.
344+
345+
346+
347+
## Disabling caching
348+
349+
In certain uncommon cases, a DataLoader which does not cache may be desirable.
350+
351+
```java
352+
new DataLoader<String, User>(userBatchLoader, DataLoaderOptions.newOptions().setCachingEnabled(false));
353+
```
354+
355+
Calling the above will ensure that every call to `.load()` will produce a new promise, and requested keys will not be saved in memory.
356+
357+
However, when the memoization cache is disabled, your batch function will receive an array of keys which may contain duplicates! Each key will
358+
be associated with each call to `.load()`. Your batch loader should provide a value for each instance of the requested key as per the contract
359+
360+
```java
361+
userDataLoader.load("A");
362+
userDataLoader.load("B");
363+
userDataLoader.load("A");
364+
365+
userDataLoader.dispatch();
366+
367+
// will result in keys to the batch loader with [ "A", "B", "A" ]
368+
369+
```
370+
371+
372+
More complex cache behavior can be achieved by calling `.clear()` or `.clearAll()` rather than disabling the cache completely.
373+
374+
375+
## Caching errors
376+
377+
If a batch load fails (that is, a batch function returns a rejected CompletionStage), then the requested values will not be cached.
378+
However if a batch function returns a `Try` or `Throwable` instance for an individual value, then that will be cached to avoid frequently loading
379+
the same problem object.
380+
381+
In some circumstances you may wish to clear the cache for these individual problems:
382+
383+
```java
384+
userDataLoader.load("r2d2").whenComplete((user, throwable) -> {
385+
if (throwable != null) {
386+
userDataLoader.clear("r2dr");
387+
throwable.printStackTrace();
388+
} else {
389+
processUser(user);
390+
}
391+
});
392+
```
393+
394+
## The scope of a data loader is important
395+
396+
If you are serving web requests then the data can be specific to the user requesting it. If you have user specific data
397+
then you will not want to cache data meant for user A to then later give it user B in a subsequent request.
398+
399+
The scope of your `DataLoader` instances is important. You might want to create them per web request to ensure data is only cached within that
400+
web request and no more.
401+
402+
If your data can be shared across web requests then you might want to scope your data loaders so they survive longer than the web request say.
403+
404+
## Custom caches
405+
406+
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
407+
lives.
408+
409+
However you can create your own custom cache and supply it to the data loader on construction via the `org.dataloader.CacheMap` interface.
410+
411+
```java
412+
MyCustomCache customCache = new MyCustomCache();
413+
DataLoaderOptions options = DataLoaderOptions.newOptions().setCacheMap(customCache);
414+
new DataLoader<String, User>(userBatchLoader, options);
415+
```
416+
417+
You could choose to use one of the fancy cache implementations from Guava or Kaffeine and wrap it in a `CacheMap` wrapper ready
418+
for data loader. They can do fancy things like time eviction and efficient LRU caching.
419+
420+
## Manual dispatching
296421

297422
The original [Facebook DataLoader](https://github.com/facebook/dataloader) was written in Javascript for NodeJS. NodeJS is single-threaded in nature, but simulates
298423
asynchronous logic by invoking functions on separate threads in an event loop, as explained
@@ -320,21 +445,6 @@ and there are also gains to this different mode of operation:
320445
However, with batch execution control comes responsibility! If you forget to make the call to `dispatch()` then the futures
321446
in the load request queue will never be batched, and thus _will never complete_! So be careful when crafting your loader designs.
322447

323-
### Error object is not a thing in a type safe Java world
324-
325-
In the reference JS implementation if the batch loader returns an `Error` object back then the `loadKey()` promise is rejected
326-
with that error. This allows fine grain (per object in the list) sets of error. If I ask for keys A,B,C and B errors out the promise
327-
for B can contain a specific error.
328-
329-
This is not quite as neat in a Java implementation
330-
331-
A batch loader function is defined as `BatchLoader<K, V>` meaning for a key of type `K` it returns a value of type `V`.
332-
333-
It cant just return some `Exception` as an object of type `V` since Java is type safe.
334-
335-
You in order for a batch loader function to return an `Exception` it must be declared as `BatchLoader<K, Object>` which
336-
allows both values and exceptions to be returned . Some type safety is lost in this case if you want
337-
to use the mix of exceptions and values pattern.
338448

339449
## Let's get started!
340450

@@ -350,7 +460,7 @@ repositories {
350460
}
351461
352462
dependencies {
353-
compile 'org.dataloader:java-dataloader:1.0.0'
463+
compile 'com.graphql-java:java-dataloader:1.0.2'
354464
}
355465
```
356466

@@ -385,13 +495,13 @@ deal with minor changes.
385495

386496
This library was originally written for use within a [VertX world](http://vertx.io/) and it used the vertx-core `Future` classes to implement
387497
itself. All the heavy lifting has been done by this project : [vertx-dataloader](https://github.com/engagingspaces/vertx-dataloader)
388-
including the extensive testing.
498+
including the extensive testing (which itself came from Facebook).
389499

390500
This particular port was done to reduce the dependency on Vertx and to write a pure Java 8 implementation with no dependencies and also
391501
to use the more normative Java CompletableFuture.
392502

393-
[vertx-core](http://vertx.io/docs/vertx-core/java/) is not a lightweight library by any means
394-
so having a pure Java 8 implementation is very desirable.
503+
[vertx-core](http://vertx.io/docs/vertx-core/java/) is not a lightweight library by any means so having a pure Java 8 implementation is
504+
very desirable.
395505

396506

397507
This library is entirely inspired by the great works of [Lee Byron](https://github.com/leebyron) and

build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ compileJava {
4545
sourceCompatibility = 1.8
4646
targetCompatibility = 1.8
4747

48-
options.compilerArgs = ["-Xlint:unchecked", "-Xdiags:verbose"]
48+
options.compilerArgs = ["-Xlint:unchecked", "-Xdiags:verbose", "-Xdoclint:none"]
49+
}
50+
51+
task myJavadocs(type: Javadoc) {
52+
source = sourceSets.main.allJava
53+
options.addStringOption('Xdoclint:none', '-quiet')
4954
}
5055

5156
dependencies {

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

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,19 @@
3838
* With batching enabled the execution will start after calling {@link DataLoader#dispatch()}, causing the queue of
3939
* loaded keys to be sent to the batch function, clears the queue, and returns a promise to the values.
4040
* <p>
41-
* As batch functions are executed the resulting futures are cached using a cache implementation of choice, so they
42-
* will only execute once. Individual cache keys can be cleared, so they will be re-fetched when referred to again.
41+
* As {@link org.dataloader.BatchLoader} batch functions are executed the resulting futures are cached using a cache
42+
* implementation of choice, so they will only execute once. Individual cache keys can be cleared, so they will
43+
* be re-fetched when referred to again.
44+
* <p>
4345
* It is also possible to clear the cache entirely, and prime it with values before they are used.
4446
* <p>
4547
* Both caching and batching can be disabled. Configuration of the data loader is done by providing a
4648
* {@link DataLoaderOptions} instance on creation.
49+
* <p>
50+
* A call to the batch loader might result in individual exception failures for item with the returned list. if
51+
* you want to capture these specific item failures then use {@link org.dataloader.Try} as a return value and
52+
* create the data loader with {@link #newDataLoaderWithTry(BatchLoader)} form. The Try values will be interpreted
53+
* as either success values or cause the {@link #load(Object)} promise to complete exceptionally.
4754
*
4855
* @param <K> type parameter indicating the type of the data load keys
4956
* @param <V> type parameter indicating the type of the data that is returned
@@ -58,6 +65,73 @@ public class DataLoader<K, V> {
5865
private final CacheMap<Object, CompletableFuture<V>> futureCache;
5966
private final Map<K, CompletableFuture<V>> loaderQueue;
6067

68+
/**
69+
* Creates new DataLoader with the specified batch loader function and default options
70+
* (batching, caching and unlimited batch size).
71+
*
72+
* @param batchLoadFunction the batch load function to use
73+
* @param <K> the key type
74+
* @param <V> the value type
75+
*
76+
* @return a new DataLoader
77+
*/
78+
public static <K, V> DataLoader<K, V> newDataLoader(BatchLoader<K, V> batchLoadFunction) {
79+
return newDataLoader(batchLoadFunction, null);
80+
}
81+
82+
/**
83+
* Creates new DataLoader with the specified batch loader function with the provided options
84+
*
85+
* @param batchLoadFunction the batch load function to use
86+
* @param options the options to use
87+
* @param <K> the key type
88+
* @param <V> the value type
89+
*
90+
* @return a new DataLoader
91+
*/
92+
public static <K, V> DataLoader<K, V> newDataLoader(BatchLoader<K, V> batchLoadFunction, DataLoaderOptions options) {
93+
return new DataLoader<>(batchLoadFunction, options);
94+
}
95+
96+
/**
97+
* Creates new DataLoader with the specified batch loader function and default options
98+
* (batching, caching and unlimited batch size) where the batch loader function returns a list of
99+
* {@link org.dataloader.Try} objects.
100+
*
101+
* This allows you to capture both the value that might be returned and also whether exception that might have occurred getting that individual value. If its important you to
102+
* know gther exact status of each item in a batch call and whether it threw exceptions when fetched then
103+
* you can use this form to create the data loader.
104+
*
105+
* @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects
106+
* @param <K> the key type
107+
* @param <V> the value type
108+
*
109+
* @return a new DataLoader
110+
*/
111+
public static <K, V> DataLoader<K, V> newDataLoaderWithTry(BatchLoader<K, Try<V>> batchLoadFunction) {
112+
return newDataLoaderWithTry(batchLoadFunction, null);
113+
}
114+
115+
/**
116+
* Creates new DataLoader with the specified batch loader function and with the provided options
117+
* where the batch loader function returns a list of
118+
* {@link org.dataloader.Try} objects.
119+
*
120+
* @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects
121+
* @param options the options to use
122+
* @param <K> the key type
123+
* @param <V> the value type
124+
*
125+
* @return a new DataLoader
126+
*
127+
* @see #newDataLoaderWithTry(BatchLoader)
128+
*/
129+
@SuppressWarnings("unchecked")
130+
public static <K, V> DataLoader<K, V> newDataLoaderWithTry(BatchLoader<K, Try<V>> batchLoadFunction, DataLoaderOptions options) {
131+
return new DataLoader<>((BatchLoader<K, V>) batchLoadFunction, options);
132+
}
133+
134+
61135
/**
62136
* Creates a new data loader with the provided batch load function, and default options.
63137
*
@@ -215,6 +289,7 @@ private CompletableFuture<List<V>> sliceIntoBatchesOfBatches(List<K> keys, List<
215289
.collect(Collectors.toList()));
216290
}
217291

292+
@SuppressWarnings("unchecked")
218293
private CompletableFuture<List<V>> dispatchQueueBatch(List<K> keys, List<CompletableFuture<V>> queuedFutures) {
219294
return batchLoadFunction.load(keys)
220295
.toCompletableFuture()
@@ -226,8 +301,13 @@ private CompletableFuture<List<V>> dispatchQueueBatch(List<K> keys, List<Complet
226301
CompletableFuture<V> future = queuedFutures.get(idx);
227302
if (value instanceof Throwable) {
228303
future.completeExceptionally((Throwable) value);
304+
// we don't clear the cached view of this entry to avoid
305+
// frequently loading the same error
306+
} else if (value instanceof Try) {
307+
// we allow the batch loader to return a Try so we can better represent a computation
308+
// that might have worked or not.
309+
handleTry((Try<V>) value, future);
229310
} else {
230-
@SuppressWarnings("unchecked")
231311
V val = (V) value;
232312
future.complete(val);
233313
}
@@ -238,13 +318,21 @@ private CompletableFuture<List<V>> dispatchQueueBatch(List<K> keys, List<Complet
238318
K key = keys.get(idx);
239319
CompletableFuture<V> future = queuedFutures.get(idx);
240320
future.completeExceptionally(ex);
241-
// clear any cached view of this key
321+
// clear any cached view of this key because they all failed
242322
clear(key);
243323
}
244324
return emptyList();
245325
});
246326
}
247327

328+
private void handleTry(Try<V> vTry, CompletableFuture<V> future) {
329+
if (vTry.isSuccess()) {
330+
future.complete(vTry.get());
331+
} else {
332+
future.completeExceptionally(vTry.getThrowable());
333+
}
334+
}
335+
248336
/**
249337
* Normally {@link #dispatch()} is an asynchronous operation but this version will 'join' on the
250338
* results if dispatch and wait for them to complete. If the {@link CompletableFuture} callbacks make more

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ public DataLoaderOptions(DataLoaderOptions other) {
5656
this.maxBatchSize = other.maxBatchSize;
5757
}
5858

59-
public static DataLoaderOptions create() {
59+
/**
60+
* @return a new default data loader options that you can then customize
61+
*/
62+
public static DataLoaderOptions newOptions() {
6063
return new DataLoaderOptions();
6164
}
6265

src/main/java/org/dataloader/DataLoaderRegistry.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,13 @@ public DataLoaderRegistry unregister(String key) {
6767
* Returns the dataloader that was registered under the specified key
6868
*
6969
* @param key the key of the data loader
70+
* @param <K> the type of keys
71+
* @param <V> the type of values
7072
*
7173
* @return a data loader or null if its not present
7274
*/
75+
@SuppressWarnings("unchecked")
7376
public <K, V> DataLoader<K, V> getDataLoader(String key) {
74-
//noinspection unchecked
7577
return (DataLoader<K, V>) dataLoaders.get(key);
7678
}
7779

0 commit comments

Comments
 (0)