Skip to content

Commit d44070a

Browse files
authored
Merge pull request #151 from graphql-java/reactive-streams-branch
A PR for reactive streams support
2 parents 20b3c01 + c3e6ee5 commit d44070a

24 files changed

+1549
-144
lines changed

README.md

+66
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,66 @@ For example, let's assume you want to load users from a database, you could prob
286286
// ...
287287
```
288288

289+
### Returning a stream of results from your batch publisher
290+
291+
It may be that your batch loader function is a [Reactive Streams](https://www.reactive-streams.org/) [Publisher](https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/org/reactivestreams/Publisher.html), where values are emitted as an asynchronous stream.
292+
293+
For example, let's say you wanted to load many users from a service without forcing the service to load all
294+
users into its memory (which may exert considerable pressure on it).
295+
296+
A `org.dataloader.BatchPublisher` may be used to load this data:
297+
298+
```java
299+
BatchPublisher<Long, User> batchPublisher = new BatchPublisher<Long, User>() {
300+
@Override
301+
public void load(List<Long> userIds, Subscriber<User> userSubscriber) {
302+
userManager.publishUsersById(userIds, userSubscriber);
303+
}
304+
};
305+
DataLoader<Long, User> userLoader = DataLoaderFactory.newPublisherDataLoader(batchPublisher);
306+
307+
// ...
308+
```
309+
310+
Rather than waiting for all values to be returned, this `DataLoader` will complete
311+
the `CompletableFuture<User>` returned by `Dataloader#load(Long)` as each value is
312+
processed.
313+
314+
If an exception is thrown, the remaining futures yet to be completed are completed
315+
exceptionally.
316+
317+
You *MUST* ensure that the values are streamed in the same order as the keys provided,
318+
with the same cardinality (i.e. the number of values must match the number of keys).
319+
Failing to do so will result in incorrect data being returned from `DataLoader#load`.
320+
321+
322+
### Returning a mapped stream of results from your batch publisher
323+
324+
Your publisher may not necessarily return values in the same order in which it processes keys.
325+
326+
For example, let's say your batch publisher function loads user data which is spread across shards,
327+
with some shards responding more quickly than others.
328+
329+
In instances like these, `org.dataloader.MappedBatchPublisher` can be used.
330+
331+
```java
332+
MappedBatchPublisher<Long, User> mappedBatchPublisher = new MappedBatchPublisher<Long, User>() {
333+
@Override
334+
public void load(Set<Long> userIds, Subscriber<Map.Entry<Long, User>> userEntrySubscriber) {
335+
userManager.publishUsersById(userIds, userEntrySubscriber);
336+
}
337+
};
338+
DataLoader<Long, User> userLoader = DataLoaderFactory.newMappedPublisherDataLoader(mappedBatchPublisher);
339+
340+
// ...
341+
```
342+
343+
Like the `BatchPublisher`, if an exception is thrown, the remaining futures yet to be completed are completed
344+
exceptionally.
345+
346+
Unlike the `BatchPublisher`, however, it is not necessary to return values in the same order as the provided keys,
347+
or even the same number of values.
348+
289349
### Error object is not a thing in a type safe Java world
290350

291351
In the reference JS implementation if the batch loader returns an `Error` object back from the `load()` promise is rejected
@@ -541,6 +601,12 @@ The following is a `BatchLoaderScheduler` that waits 10 milliseconds before invo
541601
return scheduledCall.invoke();
542602
}).thenCompose(Function.identity());
543603
}
604+
605+
@Override
606+
public <K> void scheduleBatchPublisher(ScheduledBatchPublisherCall scheduledCall, List<K> keys, BatchLoaderEnvironment environment) {
607+
snooze(10);
608+
scheduledCall.invoke();
609+
}
544610
};
545611
```
546612

build.gradle

+7-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ def getDevelopmentVersion() {
3030
version
3131
}
3232

33-
def slf4jVersion = '1.7.30'
3433
def releaseVersion = System.env.RELEASE_VERSION
3534
version = releaseVersion ? releaseVersion : getDevelopmentVersion()
3635
group = 'com.graphql-java'
@@ -64,16 +63,23 @@ jar {
6463
}
6564
}
6665

66+
def slf4jVersion = '1.7.30'
67+
def reactiveStreamsVersion = '1.0.3'
68+
6769
dependencies {
6870
api 'org.slf4j:slf4j-api:' + slf4jVersion
71+
api 'org.reactivestreams:reactive-streams:' + reactiveStreamsVersion
72+
6973
testImplementation 'org.slf4j:slf4j-simple:' + slf4jVersion
7074
testImplementation 'org.awaitility:awaitility:2.0.0'
7175
testImplementation "org.hamcrest:hamcrest:2.2"
76+
testImplementation 'io.projectreactor:reactor-core:3.6.6'
7277
testImplementation 'com.github.ben-manes.caffeine:caffeine:2.9.0'
7378
testImplementation platform('org.junit:junit-bom:5.10.2')
7479
testImplementation 'org.junit.jupiter:junit-jupiter-api'
7580
testImplementation 'org.junit.jupiter:junit-jupiter-params'
7681
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
82+
testImplementation 'io.projectreactor:reactor-core:3.6.6'
7783
}
7884

7985
task sourcesJar(type: Jar) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.dataloader;
2+
3+
import org.reactivestreams.Subscriber;
4+
5+
import java.util.List;
6+
7+
/**
8+
* A function that is invoked for batch loading a stream of data values indicated by the provided list of keys.
9+
* <p>
10+
* The function <b>must</b> call the provided {@link Subscriber} to process the values it has retrieved to allow
11+
* the future returned by {@link DataLoader#load(Object)} to complete as soon as the individual value is available
12+
* (rather than when all values have been retrieved).
13+
* <p>
14+
* <b>NOTE:</b> It is <b>required</b> that {@link Subscriber#onNext(Object)} is invoked on each value in the same order as
15+
* the provided keys and that you provide a value for every key provided.
16+
*
17+
* @param <K> type parameter indicating the type of keys to use for data load requests.
18+
* @param <V> type parameter indicating the type of values returned
19+
* @see BatchLoader for the non-reactive version
20+
*/
21+
public interface BatchPublisher<K, V> {
22+
/**
23+
* Called to batch the provided keys into a stream of values. You <b>must</b> provide
24+
* the same number of values as there as keys, and they <b>must</b> be in the order of the keys.
25+
* <p>
26+
* The idiomatic approach would be to create a reactive {@link org.reactivestreams.Publisher} that provides
27+
* the values given the keys and then subscribe to it with the provided {@link Subscriber}.
28+
* <p>
29+
* <b>NOTE:</b> It is <b>required</b> that {@link Subscriber#onNext(Object)} is invoked on each value in the same order as
30+
* the provided keys and that you provide a value for every key provided.
31+
*
32+
* @param keys the collection of keys to load
33+
* @param subscriber as values arrive you must call the subscriber for each value
34+
*/
35+
void load(List<K> keys, Subscriber<V> subscriber);
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.dataloader;
2+
3+
import org.reactivestreams.Subscriber;
4+
5+
import java.util.List;
6+
7+
/**
8+
* This form of {@link BatchPublisher} is given a {@link org.dataloader.BatchLoaderEnvironment} object
9+
* that encapsulates the calling context. A typical use case is passing in security credentials or database details
10+
* for example.
11+
* <p>
12+
* See {@link BatchPublisher} for more details on the design invariants that you must implement in order to
13+
* use this interface.
14+
*/
15+
public interface BatchPublisherWithContext<K, V> {
16+
/**
17+
* Called to batch the provided keys into a stream of values. You <b>must</b> provide
18+
* the same number of values as there as keys, and they <b>must</b> be in the order of the keys.
19+
* <p>
20+
* The idiomatic approach would be to create a reactive {@link org.reactivestreams.Publisher} that provides
21+
* the values given the keys and then subscribe to it with the provided {@link Subscriber}.
22+
* <p>
23+
* <b>NOTE:</b> It is <b>required</b> that {@link Subscriber#onNext(Object)} is invoked on each value in the same order as
24+
* the provided keys and that you provide a value for every key provided.
25+
* <p>
26+
* This is given an environment object to that maybe be useful during the call. A typical use case
27+
* is passing in security credentials or database details for example.
28+
*
29+
* @param keys the collection of keys to load
30+
* @param subscriber as values arrive you must call the subscriber for each value
31+
* @param environment an environment object that can help with the call
32+
*/
33+
void load(List<K> keys, Subscriber<V> subscriber, BatchLoaderEnvironment environment);
34+
}

0 commit comments

Comments
 (0)