Im trying to create a Java App using SpringBoot 3 framework and GraphQL. I'm trying to resolve a field which depends on several dataLoaders within my App however, I seem to be running into an issue where my app just hangs for while then completely timeouts when I try combining them using thenCompose method.
Curiously, when I use one dependent data loaded in conjunction with thenCompose method, it works fine, however, when I'm trying to add more it breaks.
Ive added some example code below:
@Component
public class ListDataLoader implements BiFunction<Set<Page>, BatchLoaderEnvironment, Mono<Map<Page, List>>> {
@Override
public Mono<Map<Page, List>> apply(Set<Page> page, BatchLoaderEnvironment batchLoaderEnvironment) {
return Flux.fromStream(page.stream())
.filter(page -> page.getProperty() != null)
.map(page ->
{
return Map.entry(page, List.of(1, 2, 3));
}
)
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
}
}
@Component
public class NameDataLoader implements BiFunction<Set<Page>, BatchLoaderEnvironment, Mono<Map<Page, String>>> {
@Override
public Mono<Map<Page, String>> apply(Set<Page> page, BatchLoaderEnvironment batchLoaderEnvironment) {
return Flux.fromStream(page.stream())
.filter(page -> page.getProperty() != null)
.map(page ->
{
return Map.entry(page, "true");
}
)
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
}
}
In the resolver:
@Controller
public class FilterController {
@SchemaMapping(typeName = "Page", field = "filter")
public CompletableFuture<Filter> filter(
Page page,
DataFetchingEnvironment env,
DataLoader<Page, List<Integer>> list,
DataLoader<Page, Filter> filter,
DataLoader<Page, String> name
) {
return list.load(page, env)
.thenCompose(x -> filter.load(page, env));
}
}
However, when I am trying to add one more data loader it breaks.
return list.load(page, env)
.thenCompose(y -> name.load(page, env)
.thenCompose(x -> filter.load(page, env)));
Have you tried using CompletableFuture.allOf(args...) instead of chaining multiple thenCompose() calls?
Seems that your current approach of nesting DataLoader.load(args...).thenCompose(args...) is causing the GraphQL request to hang and eventually time out, I suspect the issue lies in how those asynchronous operations are getting scheduled, or more accurately, not scheduled properly.
I noticed this on your return statement:
return list.load(page, env)
.thenCompose(y -> name.load(page, env)
.thenCompose(x -> filter.load(page, env)));
Your return statement is creating a deep dependency chain, where filter.load(args...) won’t even be triggered until name.load(args...) completes which only happens after list.load(args...). Doing it like this could interfere with Spring GraphQL’s batch dispatching lifecycle.
Spring collects all DataLoader.load(args...) calls before executing them in a batch. However, if one load(args...) depends on the result of another, that batching process can break or deadlock.
Maybe try resolving all Loads in parallel using CompletableFuture.allOf(args...) . See my example below:
@SchemaMapping(typeName = "Page", field = "filter")
public CompletableFuture<Filter> filter(
Page page,
DataFetchingEnvironment env,
DataLoader<Page, List<Integer>> listLoader,
DataLoader<Page, Filter> filterLoader,
DataLoader<Page, String> nameLoader
) {
CompletableFuture<List<Integer>> listFuture = listLoader.load(page, env);
CompletableFuture<String> nameFuture = nameLoader.load(page, env);
CompletableFuture<Filter> filterFuture = filterLoader.load(page, env);
return CompletableFuture.allOf(listFuture, nameFuture, filterFuture)
.thenApply(voidResult -> {
List<Integer> list = listFuture.join();
String name = nameFuture.join();
Filter filter = filterFuture.join();
return filter;
});
}
Doing it like this allows for all three DataLoader calls can run in parallel, and Spring can batch them as expected. Once all are resolved, you can safely .join() their results and do whatever processing you need.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With