Skip to content

3 instrumentation dispatcher #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 125 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,136 @@ a list of user ids in one call.

```

That said, with key caching turn on (the default), it may still be more efficient using `dataloader` than without it.
That said, with key caching turn on (the default), it will still be more efficient using `dataloader` than without it.

# Using dataloader in graphql for maximum efficiency


If you are using `graphql`, you are likely to making queries on a graph of data (surprise surprise). `dataloader` will help
you to make this a more efficient process by both caching and batching requests for that graph of data items. If `dataloader`
has previously see a data item before, it will cached the value and will return it without having to ask for it again.

Imagine we have the StarWars query outlined below. It asks us to find a hero and their friend's names and their friend's friend's
names. It is likely that many of these people will be friends in common.



{
hero {
name
friends {
name
friends {
name
}
}
}
}

The result of this query is displayed below. You can see that Han, Leia, Luke and R2-D2 are tight knit bunch of friends and
share many friends in common.


[hero: [name: 'R2-D2', friends: [
[name: 'Luke Skywalker', friends: [
[name: 'Han Solo'], [name: 'Leia Organa'], [name: 'C-3PO'], [name: 'R2-D2']]],
[name: 'Han Solo', friends: [
[name: 'Luke Skywalker'], [name: 'Leia Organa'], [name: 'R2-D2']]],
[name: 'Leia Organa', friends: [
[name: 'Luke Skywalker'], [name: 'Han Solo'], [name: 'C-3PO'], [name: 'R2-D2']]]]]
]

A naive implementation would called a `DataFetcher` to retrieved a person object every time it was invoked.

In this case it would be *15* calls over the network. Even though the group of people have a lot of common friends.
With `dataloader` you can make the `graphql` query much more efficient.

As `graphql` descends each level of the query ( eg as it processes `hero` and then `friends` and then for each their `friends`),
the data loader is called to "promise" to deliver a person object. At each level `dataloader.dispatch()` will be
called to fire off the batch requests for that part of the query. With caching turned on (the default) then
any previously returned person will be returned as is for no cost.

In the above example there are only *5* unique people mentioned but with caching and batching retrieval in place their will be only
*3* calls to the batch loader function. *3* calls over the network or to a database is much better than *15* calls you will agree.

If you use capabilities like `java.util.concurrent.CompletableFuture.supplyAsync()` then you can make it even more efficient by making the
the remote calls asynchronous to the rest of the query. This will make it even more timely since multiple calls can happen at once
if need be.

Here is how you might put this in place:


```java

// a batch loader function that will be called with N or more keys for batch loading
BatchLoader<String, Object> characterBatchLoader = new BatchLoader<String, Object>() {
@Override
public CompletionStage<List<Object>> load(List<String> keys) {
//
// we use supplyAsync() of values here for maximum parellisation
//
return CompletableFuture.supplyAsync(() -> getCharacterDataViaBatchHTTPApi(keys));
}
};

// a data loader for characters that points to the character batch loader
DataLoader characterDataLoader = new DataLoader<String, Object>(characterBatchLoader);

//
// use this data loader in the data fetchers associated with characters and put them into
// the graphql schema (not shown)
//
DataFetcher heroDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
return characterDataLoader.load("2001"); // R2D2
}
};

DataFetcher friendsDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
StarWarsCharacter starWarsCharacter = environment.getSource();
List<String> friendIds = starWarsCharacter.getFriendIds();
return characterDataLoader.loadMany(friendIds);
}
};

//
// DataLoaderRegistry is a place to register all data loaders in that needs to be dispatched together
// in this case there is 1 but you can have many
//
DataLoaderRegistry registry = new DataLoaderRegistry();
registry.register(characterDataLoader);

//
// this instrumentation implementation will dispatched all the dataloaders
// as each level fo the graphql query is executed and hence make batched objects
// available to the query and the associated DataFetchers
//
DataLoaderDispatcherInstrumentation dispatcherInstrumentation
= new DataLoaderDispatcherInstrumentation(registry);

//
// now build your graphql object and execute queries on it.
// the data loader will be invoked via the data fetchers on the
// schema fields
//
GraphQL graphQL = GraphQL.newGraphQL(buildSchema())
.instrumentation(dispatcherInstrumentation)
.build();
```

One thing to note is the above only works if you use `DataLoaderDispatcherInstrumentation` which makes sure `dataLoader.dispatch()` is called. If
this was not in place, then all the promises to data will never be dispatched ot the batch loader function and hence nothing would ever resolve.

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

## Differences to reference implementation

### Manual dispatching

The original data loader was written in Javascript for NodeJS. NodeJS is single-threaded in nature, but simulates
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
[in this post](http://stackoverflow.com/a/19823583/3455094) on StackOverflow.

Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ compileJava {
}

dependencies {
compile "com.graphql-java:graphql-java:4.0"
testCompile "junit:junit:$junitVersion"
testCompile 'org.awaitility:awaitility:2.0.0'
}
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/org/dataloader/DataLoaderRegistry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.dataloader;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
* This allows data loaders to be registered together into a single place so
* they can be dispatched as one.
*/
public class DataLoaderRegistry {
private final List<DataLoader<?, ?>> dataLoaders = new CopyOnWriteArrayList<>();

/**
* @return the currently registered data loaders
*/
public List<DataLoader<?, ?>> getDataLoaders() {
return new ArrayList<>(dataLoaders);
}

/**
* This will register a new dataloader
*
* @param dataLoader the data loader to register
*
* @return this registry
*/
public DataLoaderRegistry register(DataLoader<?, ?> dataLoader) {
if (!dataLoaders.contains(dataLoader)) {
dataLoaders.add(dataLoader);
}
return this;
}

/**
* This will unregister a new dataloader
*
* @param dataLoader the data loader to unregister
*
* @return this registry
*/
public DataLoaderRegistry unregister(DataLoader<?, ?> dataLoader) {
dataLoaders.remove(dataLoader);
return this;
}

/**
* This will called {@link org.dataloader.DataLoader#dispatch()} on each of the registered
* {@link org.dataloader.DataLoader}s
*/
public void dispatchAll() {
dataLoaders.forEach(DataLoader::dispatch);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.dataloader.graphql;

import graphql.ExecutionResult;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.NoOpInstrumentation;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters;
import org.dataloader.DataLoaderRegistry;

import java.util.concurrent.CompletableFuture;

/**
* This graphql {@link graphql.execution.instrumentation.Instrumentation} will dispatch
* all the contained {@link org.dataloader.DataLoader}s when each level of the graphql
* query is executed.
*/
public class DataLoaderDispatcherInstrumentation extends NoOpInstrumentation {

private final DataLoaderRegistry dataLoaderRegistry;

public DataLoaderDispatcherInstrumentation(DataLoaderRegistry dataLoaderRegistry) {
this.dataLoaderRegistry = dataLoaderRegistry;
}

@Override
public InstrumentationContext<CompletableFuture<ExecutionResult>> beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) {
return (result, t) -> {
if (t == null) {
// only dispatch when there are no errors
dataLoaderRegistry.dispatchAll();
}
};
}
}
86 changes: 86 additions & 0 deletions src/test/java/ReadmeExamples.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import graphql.GraphQL;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.GraphQLSchema;
import org.dataloader.BatchLoader;
import org.dataloader.DataLoader;
import org.dataloader.DataLoaderRegistry;
import org.dataloader.fixtures.User;
import org.dataloader.fixtures.UserManager;
import org.dataloader.graphql.DataLoaderDispatcherInstrumentation;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.stream.Collectors;

@SuppressWarnings("ALL")
public class ReadmeExamples {


UserManager userManager = new UserManager();

public static void main(String[] args) {
Expand Down Expand Up @@ -68,4 +76,82 @@ public CompletionStage<List<User>> load(List<Long> userIds) {
userLoader.dispatchAndJoin();
}


class StarWarsCharacter {
List<String> getFriendIds() {
return null;
}
}

void starWarsExample() {

// a batch loader function that will be called with N or more keys for batch loading
BatchLoader<String, Object> characterBatchLoader = new BatchLoader<String, Object>() {
@Override
public CompletionStage<List<Object>> load(List<String> keys) {
//
// we use supplyAsync() of values here for maximum parellisation
//
return CompletableFuture.supplyAsync(() -> getCharacterDataViaBatchHTTPApi(keys));
}
};

// a data loader for characters that points to the character batch loader
DataLoader characterDataLoader = new DataLoader<String, Object>(characterBatchLoader);

//
// use this data loader in the data fetchers associated with characters and put them into
// the graphql schema (not shown)
//
DataFetcher heroDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
return characterDataLoader.load("2001"); // R2D2
}
};

DataFetcher friendsDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
StarWarsCharacter starWarsCharacter = environment.getSource();
List<String> friendIds = starWarsCharacter.getFriendIds();
return characterDataLoader.loadMany(friendIds);
}
};

//
// DataLoaderRegistry is a place to register all data loaders in that needs to be dispatched together
// in this case there is 1 but you can have many
//
DataLoaderRegistry registry = new DataLoaderRegistry();
registry.register(characterDataLoader);

//
// this instrumentation implementation will dispatched all the dataloaders
// as each level fo the graphql query is executed and hence make batched objects
// available to the query and the associated DataFetchers
//
DataLoaderDispatcherInstrumentation dispatcherInstrumentation
= new DataLoaderDispatcherInstrumentation(registry);

//
// now build your graphql object and execute queries on it.
// the data loader will be invoked via the data fetchers on the
// schema fields
//
GraphQL graphQL = GraphQL.newGraphQL(buildSchema())
.instrumentation(dispatcherInstrumentation)
.build();

}

private GraphQLSchema buildSchema() {
return null;
}

private List<Object> getCharacterDataViaBatchHTTPApi(List<String> keys) {
return null;
}


}
41 changes: 41 additions & 0 deletions src/test/java/org/dataloader/DataLoaderRegistryTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.dataloader;

import org.junit.Test;

import java.util.concurrent.CompletableFuture;

import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

public class DataLoaderRegistryTest {
final BatchLoader<Object, Object> identityBatchLoader = CompletableFuture::completedFuture;

@Test
public void registration_works() throws Exception {
DataLoader<Object, Object> dlA = new DataLoader<>(identityBatchLoader);
DataLoader<Object, Object> dlB = new DataLoader<>(identityBatchLoader);
DataLoader<Object, Object> dlC = new DataLoader<>(identityBatchLoader);

DataLoaderRegistry registry = new DataLoaderRegistry();

registry.register(dlA).register(dlB).register(dlC);

assertThat(registry.getDataLoaders(), equalTo(asList(dlA, dlB, dlC)));

// the same dl twice is one add


registry = new DataLoaderRegistry();

registry.register(dlA).register(dlB).register(dlC).register(dlA).register(dlB);

assertThat(registry.getDataLoaders(), equalTo(asList(dlA, dlB, dlC)));


// and unregister
registry.unregister(dlC);

assertThat(registry.getDataLoaders(), equalTo(asList(dlA, dlB)));
}
}
Loading