Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

thenCompose hangs/timeout web graphql application

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)));
like image 574
maxs87 Avatar asked Dec 01 '25 10:12

maxs87


1 Answers

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.

like image 122
White Rabbit Avatar answered Dec 02 '25 23:12

White Rabbit