Skip to content

Commit c3e6ee5

Browse files
authored
Merge pull request #160 from AlexandreCarlton/add-documentation-for-publishers
Have MappedBatchPublisher take in a Set<K> keys (and add README sections)
2 parents 4b9356e + 2e82858 commit c3e6ee5

File tree

7 files changed

+113
-9
lines changed

7 files changed

+113
-9
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

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,7 @@ private CompletableFuture<List<V>> invokeBatchPublisher(List<K> keys, List<Objec
536536
private CompletableFuture<List<V>> invokeMappedBatchPublisher(List<K> keys, List<Object> keyContexts, List<CompletableFuture<V>> queuedFutures, BatchLoaderEnvironment environment) {
537537
CompletableFuture<List<V>> loadResult = new CompletableFuture<>();
538538
Subscriber<Map.Entry<K, V>> subscriber = ReactiveSupport.mappedBatchSubscriber(loadResult, keys, keyContexts, queuedFutures, helperIntegration());
539-
539+
Set<K> setOfKeys = new LinkedHashSet<>(keys);
540540
BatchLoaderScheduler batchLoaderScheduler = loaderOptions.getBatchLoaderScheduler();
541541
if (batchLoadFunction instanceof MappedBatchPublisherWithContext) {
542542
//noinspection unchecked
@@ -551,10 +551,10 @@ private CompletableFuture<List<V>> invokeMappedBatchPublisher(List<K> keys, List
551551
//noinspection unchecked
552552
MappedBatchPublisher<K, V> loadFunction = (MappedBatchPublisher<K, V>) batchLoadFunction;
553553
if (batchLoaderScheduler != null) {
554-
BatchLoaderScheduler.ScheduledBatchPublisherCall loadCall = () -> loadFunction.load(keys, subscriber);
554+
BatchLoaderScheduler.ScheduledBatchPublisherCall loadCall = () -> loadFunction.load(setOfKeys, subscriber);
555555
batchLoaderScheduler.scheduleBatchPublisher(loadCall, keys, null);
556556
} else {
557-
loadFunction.load(keys, subscriber);
557+
loadFunction.load(setOfKeys, subscriber);
558558
}
559559
}
560560
return loadResult;

src/main/java/org/dataloader/MappedBatchPublisher.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import org.reactivestreams.Subscriber;
44

5-
import java.util.List;
65
import java.util.Map;
6+
import java.util.Set;
77

88
/**
99
* A function that is invoked for batch loading a stream of data values indicated by the provided list of keys.
@@ -26,5 +26,5 @@ public interface MappedBatchPublisher<K, V> {
2626
* @param keys the collection of keys to load
2727
* @param subscriber as values arrive you must call the subscriber for each value
2828
*/
29-
void load(List<K> keys, Subscriber<Map.Entry<K, V>> subscriber);
29+
void load(Set<K> keys, Subscriber<Map.Entry<K, V>> subscriber);
3030
}

src/test/java/ReadmeExamples.java

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import org.dataloader.BatchLoader;
22
import org.dataloader.BatchLoaderEnvironment;
33
import org.dataloader.BatchLoaderWithContext;
4+
import org.dataloader.BatchPublisher;
45
import org.dataloader.CacheMap;
56
import org.dataloader.DataLoader;
67
import org.dataloader.DataLoaderFactory;
78
import org.dataloader.DataLoaderOptions;
89
import org.dataloader.MappedBatchLoaderWithContext;
10+
import org.dataloader.MappedBatchPublisher;
911
import org.dataloader.Try;
1012
import org.dataloader.fixtures.SecurityCtx;
1113
import org.dataloader.fixtures.User;
@@ -15,6 +17,7 @@
1517
import org.dataloader.scheduler.BatchLoaderScheduler;
1618
import org.dataloader.stats.Statistics;
1719
import org.dataloader.stats.ThreadLocalStatisticsCollector;
20+
import org.reactivestreams.Subscriber;
1821

1922
import java.time.Duration;
2023
import java.util.ArrayList;
@@ -171,7 +174,7 @@ private void tryExample() {
171174
}
172175
}
173176

174-
private void tryBatcLoader() {
177+
private void tryBatchLoader() {
175178
DataLoader<String, User> dataLoader = DataLoaderFactory.newDataLoaderWithTry(new BatchLoader<String, Try<User>>() {
176179
@Override
177180
public CompletionStage<List<Try<User>>> load(List<String> keys) {
@@ -187,6 +190,26 @@ public CompletionStage<List<Try<User>>> load(List<String> keys) {
187190
});
188191
}
189192

193+
private void batchPublisher() {
194+
BatchPublisher<Long, User> batchPublisher = new BatchPublisher<Long, User>() {
195+
@Override
196+
public void load(List<Long> userIds, Subscriber<User> userSubscriber) {
197+
userManager.publishUsersById(userIds, userSubscriber);
198+
}
199+
};
200+
DataLoader<Long, User> userLoader = DataLoaderFactory.newPublisherDataLoader(batchPublisher);
201+
}
202+
203+
private void mappedBatchPublisher() {
204+
MappedBatchPublisher<Long, User> mappedBatchPublisher = new MappedBatchPublisher<Long, User>() {
205+
@Override
206+
public void load(Set<Long> userIds, Subscriber<Map.Entry<Long, User>> userEntrySubscriber) {
207+
userManager.publishUsersById(userIds, userEntrySubscriber);
208+
}
209+
};
210+
DataLoader<Long, User> userLoader = DataLoaderFactory.newMappedPublisherDataLoader(mappedBatchPublisher);
211+
}
212+
190213
DataLoader<String, User> userDataLoader;
191214

192215
private void clearCacheOnError() {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,7 @@ public void should_work_with_duplicate_keys_when_caching_disabled(TestDataLoader
740740
assertThat(future1.get(), equalTo("A"));
741741
assertThat(future2.get(), equalTo("B"));
742742
assertThat(future3.get(), equalTo("A"));
743-
if (factory instanceof MappedDataLoaderFactory) {
743+
if (factory instanceof MappedDataLoaderFactory || factory instanceof MappedPublisherDataLoaderFactory) {
744744
assertThat(loadCalls, equalTo(singletonList(asList("A", "B"))));
745745
} else {
746746
assertThat(loadCalls, equalTo(singletonList(asList("A", "B", "A"))));

src/test/java/org/dataloader/fixtures/UserManager.java

+11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package org.dataloader.fixtures;
22

3+
import org.reactivestreams.Subscriber;
4+
import reactor.core.publisher.Flux;
5+
36
import java.util.HashMap;
47
import java.util.LinkedHashMap;
58
import java.util.List;
@@ -52,6 +55,14 @@ public List<User> loadUsersById(List<Long> userIds) {
5255
return userIds.stream().map(this::loadUserById).collect(Collectors.toList());
5356
}
5457

58+
public void publishUsersById(List<Long> userIds, Subscriber<? super User> userSubscriber) {
59+
Flux.fromIterable(loadUsersById(userIds)).subscribe(userSubscriber);
60+
}
61+
62+
public void publishUsersById(Set<Long> userIds, Subscriber<? super Map.Entry<Long, User>> userEntrySubscriber) {
63+
Flux.fromIterable(loadMapOfUsersByIds(null, userIds).entrySet()).subscribe(userEntrySubscriber);
64+
}
65+
5566
public Map<Long, User> loadMapOfUsersByIds(SecurityCtx callCtx, Set<Long> userIds) {
5667
Map<Long, User> map = new HashMap<>();
5768
userIds.forEach(userId -> {

src/test/java/org/dataloader/fixtures/parameterized/MappedPublisherDataLoaderFactory.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
import java.util.HashMap;
1111
import java.util.List;
1212
import java.util.Map;
13+
import java.util.Set;
14+
import java.util.stream.Collectors;
1315
import java.util.stream.Stream;
1416

17+
import static java.util.stream.Collectors.toList;
18+
import static java.util.stream.Collectors.toSet;
1519
import static org.dataloader.DataLoaderFactory.newMappedPublisherDataLoader;
1620
import static org.dataloader.DataLoaderFactory.newMappedPublisherDataLoaderWithTry;
1721

@@ -69,7 +73,7 @@ public <K> DataLoader<K, K> idLoaderBlowsUpsAfterN(int N, DataLoaderOptions opti
6973
return newMappedPublisherDataLoader((keys, subscriber) -> {
7074
loadCalls.add(new ArrayList<>(keys));
7175

72-
List<K> nKeys = keys.subList(0, N);
76+
List<K> nKeys = keys.stream().limit(N).collect(toList());
7377
Flux<Map.Entry<K, K>> subFlux = Flux.fromIterable(nKeys).map(k -> Map.entry(k, k));
7478
subFlux.concatWith(Flux.error(new IllegalStateException("Error")))
7579
.subscribe(subscriber);
@@ -81,7 +85,7 @@ public DataLoader<String, String> onlyReturnsNValues(int N, DataLoaderOptions op
8185
return newMappedPublisherDataLoader((keys, subscriber) -> {
8286
loadCalls.add(new ArrayList<>(keys));
8387

84-
List<String> nKeys = keys.subList(0, N);
88+
List<String> nKeys = keys.stream().limit(N).collect(toList());
8589
Flux.fromIterable(nKeys).map(k -> Map.entry(k, k))
8690
.subscribe(subscriber);
8791
}, options);

0 commit comments

Comments
 (0)