Skip to content

Commit

Permalink
Now with Try support in data loaders and way more doco (#9)
Browse files Browse the repository at this point in the history
* Now with Try support in data loaders and way more doco

* More Try code and Java doc
  • Loading branch information
bbakerman authored Sep 8, 2017
1 parent 232a21a commit fbb35ef
Show file tree
Hide file tree
Showing 10 changed files with 865 additions and 44 deletions.
156 changes: 133 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ and Nicholas Schrock (@schrockn) from [Facebook](https://www.facebook.com/), the

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

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

## Differences to reference implementation
### Error object is not a thing in a type safe Java world

In the reference JS implementation if the batch loader returns an `Error` object back from the `load()` promise is rejected
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
for B can contain a specific error.

This is not quite as loose in a Java implementation as Java is a type safe language.

A batch loader function is defined as `BatchLoader<K, V>` meaning for a key of type `K` it returns a value of type `V`.

It cant just return some `Exception` as an object of type `V`. Type safety matters.

However you can use the `Try` data type which can encapsulate a computation that succeeded or returned an exception.

### Manual dispatching
```java
Try<String> tryS = Try.tryCall(() -> {
if (rollDice()) {
return "OK";
} else {
throw new RuntimeException("Bang");
}
});

if (tryS.isSuccess()) {
System.out.println("It work " + tryS.get());
} else {
System.out.println("It failed with exception : " + tryS.getThrowable());

}
```

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
and some of which may have failed. From that data loader can infer the right behavior in terms of the `load(x)` promise.

```java
DataLoader<String, User> dataLoader = DataLoader.newDataLoaderWithTry(new BatchLoader<String, Try<User>>() {
@Override
public CompletionStage<List<Try<User>>> load(List<String> keys) {
return CompletableFuture.supplyAsync(() -> {
List<Try<User>> users = new ArrayList<>();
for (String key : keys) {
Try<User> userTry = loadUser(key);
users.add(userTry);
}
return users;
});
}
});

```

On the above example if one of the `Try` objects represents a failure, then its `load()` promise will complete exceptionally and you can
react to that, in a type safe manner.



## Disabling caching

In certain uncommon cases, a DataLoader which does not cache may be desirable.

```java
new DataLoader<String, User>(userBatchLoader, DataLoaderOptions.newOptions().setCachingEnabled(false));
```

Calling the above will ensure that every call to `.load()` will produce a new promise, and requested keys will not be saved in memory.

However, when the memoization cache is disabled, your batch function will receive an array of keys which may contain duplicates! Each key will
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

```java
userDataLoader.load("A");
userDataLoader.load("B");
userDataLoader.load("A");

userDataLoader.dispatch();

// will result in keys to the batch loader with [ "A", "B", "A" ]

```


More complex cache behavior can be achieved by calling `.clear()` or `.clearAll()` rather than disabling the cache completely.


## Caching errors

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

In some circumstances you may wish to clear the cache for these individual problems:

```java
userDataLoader.load("r2d2").whenComplete((user, throwable) -> {
if (throwable != null) {
userDataLoader.clear("r2dr");
throwable.printStackTrace();
} else {
processUser(user);
}
});
```

## The scope of a data loader is important

If you are serving web requests then the data can be specific to the user requesting it. If you have user specific data
then you will not want to cache data meant for user A to then later give it user B in a subsequent request.

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
web request and no more.

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.

## Custom caches

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
lives.

However you can create your own custom cache and supply it to the data loader on construction via the `org.dataloader.CacheMap` interface.

```java
MyCustomCache customCache = new MyCustomCache();
DataLoaderOptions options = DataLoaderOptions.newOptions().setCacheMap(customCache);
new DataLoader<String, User>(userBatchLoader, options);
```

You could choose to use one of the fancy cache implementations from Guava or Kaffeine and wrap it in a `CacheMap` wrapper ready
for data loader. They can do fancy things like time eviction and efficient LRU caching.

## Manual dispatching

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

### Error object is not a thing in a type safe Java world

In the reference JS implementation if the batch loader returns an `Error` object back then the `loadKey()` promise is rejected
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
for B can contain a specific error.

This is not quite as neat in a Java implementation

A batch loader function is defined as `BatchLoader<K, V>` meaning for a key of type `K` it returns a value of type `V`.

It cant just return some `Exception` as an object of type `V` since Java is type safe.

You in order for a batch loader function to return an `Exception` it must be declared as `BatchLoader<K, Object>` which
allows both values and exceptions to be returned . Some type safety is lost in this case if you want
to use the mix of exceptions and values pattern.

## Let's get started!

Expand All @@ -350,7 +460,7 @@ repositories {
}
dependencies {
compile 'org.dataloader:java-dataloader:1.0.0'
compile 'com.graphql-java:java-dataloader:1.0.2'
}
```

Expand Down Expand Up @@ -385,13 +495,13 @@ deal with minor changes.

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

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

[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 very desirable.
[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
very desirable.


This library is entirely inspired by the great works of [Lee Byron](https://github.com/leebyron) and
Expand Down
7 changes: 6 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ compileJava {
sourceCompatibility = 1.8
targetCompatibility = 1.8

options.compilerArgs = ["-Xlint:unchecked", "-Xdiags:verbose"]
options.compilerArgs = ["-Xlint:unchecked", "-Xdiags:verbose", "-Xdoclint:none"]
}

task myJavadocs(type: Javadoc) {
source = sourceSets.main.allJava
options.addStringOption('Xdoclint:none', '-quiet')
}

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

/**
* Creates new DataLoader with the specified batch loader function and default options
* (batching, caching and unlimited batch size).
*
* @param batchLoadFunction the batch load function to use
* @param <K> the key type
* @param <V> the value type
*
* @return a new DataLoader
*/
public static <K, V> DataLoader<K, V> newDataLoader(BatchLoader<K, V> batchLoadFunction) {
return newDataLoader(batchLoadFunction, null);
}

/**
* Creates new DataLoader with the specified batch loader function with the provided options
*
* @param batchLoadFunction the batch load function to use
* @param options the options to use
* @param <K> the key type
* @param <V> the value type
*
* @return a new DataLoader
*/
public static <K, V> DataLoader<K, V> newDataLoader(BatchLoader<K, V> batchLoadFunction, DataLoaderOptions options) {
return new DataLoader<>(batchLoadFunction, options);
}

/**
* Creates new DataLoader with the specified batch loader function and default options
* (batching, caching and unlimited batch size) where the batch loader function returns a list of
* {@link org.dataloader.Try} objects.
*
* 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
* know gther exact status of each item in a batch call and whether it threw exceptions when fetched then
* you can use this form to create the data loader.
*
* @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects
* @param <K> the key type
* @param <V> the value type
*
* @return a new DataLoader
*/
public static <K, V> DataLoader<K, V> newDataLoaderWithTry(BatchLoader<K, Try<V>> batchLoadFunction) {
return newDataLoaderWithTry(batchLoadFunction, null);
}

/**
* Creates new DataLoader with the specified batch loader function and with the provided options
* where the batch loader function returns a list of
* {@link org.dataloader.Try} objects.
*
* @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects
* @param options the options to use
* @param <K> the key type
* @param <V> the value type
*
* @return a new DataLoader
*
* @see #newDataLoaderWithTry(BatchLoader)
*/
@SuppressWarnings("unchecked")
public static <K, V> DataLoader<K, V> newDataLoaderWithTry(BatchLoader<K, Try<V>> batchLoadFunction, DataLoaderOptions options) {
return new DataLoader<>((BatchLoader<K, V>) batchLoadFunction, options);
}


/**
* Creates a new data loader with the provided batch load function, and default options.
*
Expand Down Expand Up @@ -215,6 +289,7 @@ private CompletableFuture<List<V>> sliceIntoBatchesOfBatches(List<K> keys, List<
.collect(Collectors.toList()));
}

@SuppressWarnings("unchecked")
private CompletableFuture<List<V>> dispatchQueueBatch(List<K> keys, List<CompletableFuture<V>> queuedFutures) {
return batchLoadFunction.load(keys)
.toCompletableFuture()
Expand All @@ -226,8 +301,13 @@ private CompletableFuture<List<V>> dispatchQueueBatch(List<K> keys, List<Complet
CompletableFuture<V> future = queuedFutures.get(idx);
if (value instanceof Throwable) {
future.completeExceptionally((Throwable) value);
// we don't clear the cached view of this entry to avoid
// frequently loading the same error
} else if (value instanceof Try) {
// we allow the batch loader to return a Try so we can better represent a computation
// that might have worked or not.
handleTry((Try<V>) value, future);
} else {
@SuppressWarnings("unchecked")
V val = (V) value;
future.complete(val);
}
Expand All @@ -238,13 +318,21 @@ private CompletableFuture<List<V>> dispatchQueueBatch(List<K> keys, List<Complet
K key = keys.get(idx);
CompletableFuture<V> future = queuedFutures.get(idx);
future.completeExceptionally(ex);
// clear any cached view of this key
// clear any cached view of this key because they all failed
clear(key);
}
return emptyList();
});
}

private void handleTry(Try<V> vTry, CompletableFuture<V> future) {
if (vTry.isSuccess()) {
future.complete(vTry.get());
} else {
future.completeExceptionally(vTry.getThrowable());
}
}

/**
* Normally {@link #dispatch()} is an asynchronous operation but this version will 'join' on the
* results if dispatch and wait for them to complete. If the {@link CompletableFuture} callbacks make more
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/org/dataloader/DataLoaderOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ public DataLoaderOptions(DataLoaderOptions other) {
this.maxBatchSize = other.maxBatchSize;
}

public static DataLoaderOptions create() {
/**
* @return a new default data loader options that you can then customize
*/
public static DataLoaderOptions newOptions() {
return new DataLoaderOptions();
}

Expand Down
4 changes: 3 additions & 1 deletion src/main/java/org/dataloader/DataLoaderRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@ public DataLoaderRegistry unregister(String key) {
* Returns the dataloader that was registered under the specified key
*
* @param key the key of the data loader
* @param <K> the type of keys
* @param <V> the type of values
*
* @return a data loader or null if its not present
*/
@SuppressWarnings("unchecked")
public <K, V> DataLoader<K, V> getDataLoader(String key) {
//noinspection unchecked
return (DataLoader<K, V>) dataLoaders.get(key);
}

Expand Down
Loading

0 comments on commit fbb35ef

Please sign in to comment.