graphql-java-tools icon indicating copy to clipboard operation
graphql-java-tools copied to clipboard

Kotlin coroutine suspend fun resolver hangs when awaiting a data loader

Open arian opened this issue 4 years ago • 3 comments

We're using java-graphql batch loaders (https://www.graphql-java.com/documentation/v15/batching/)

Instead of returning CompletableFutures in our resolver methods, I tried using kotlin suspend functions, like this:

    suspend fun loadUsers(env: DataFetchingEnvironment): List<UserGQL> {
        val users = Context.getDataLoaders(env)
            .usersLoader
            .loadMany(listOf(1, 2, 3))
            .await()
        return users.map { UserGQL(it) } 
    }

However it seems to be hanging on the await. I think it's blocking there, before java-graphql called the dispatch on the registered data loaders (in DataLoaderRegistry).

What would be the best way to solve this? I couldn't really find examples in the documentation or the original pull request (https://github.com/graphql-java-kickstart/graphql-java-tools/pull/201)

arian avatar Jul 16 '20 07:07 arian

Might it be because this example in the graphql-java docs: https://www.graphql-java.com/documentation/v15/batching/

        BatchLoader<String, Object> batchLoader = new BatchLoader<String, Object>() {
            @Override
            public CompletionStage<List<Object>> load(List<String> keys) {
                return CompletableFuture.completedFuture(getTheseCharacters(keys));
            }
        };

        DataLoader<String, Object> characterDataLoader = DataLoader.newDataLoader(batchLoader);

        // .... later in your data fetcher

        DataFetcher dataFetcherThatCallsTheDataLoader = new DataFetcher() {
            @Override
            public Object get(DataFetchingEnvironment environment) {
                //
                // Don't DO THIS!
                //
                return CompletableFuture.supplyAsync(() -> {
                    String argId = environment.getArgument("id");
                    DataLoader<String, Object> characterLoader = environment.getDataLoader("characterLoader");
                    return characterLoader.load(argId);
                });
            }
        };

and with MethodFieldResolver.kt:190

            environment.coroutineScope().future(options.coroutineContextProvider.provide()) {
                invokeSuspend(source, resolverMethod, args)?.transformWithGenericWrapper(environment)
            }

it might be equivalent to the supplyAsync example the docs tells us not to do?

arian avatar Aug 04 '20 15:08 arian

I think the problem is that you're wrapping the CompletionStage provided by the DataLoader API with the coroutine's CompletableFuture representation created by graphql-java-tools. As you've pointed out already, this disconnects graphql-java from your dataloader, and so it is unable to dispatch it.

My understanding is that .loadMany will not actually call your data loader method directly - it'll only create a "request to fetch these users at some point", which you need to return in your data fetcher so that graphql-java can orchestrate when to actually call the data loader (maybe there'll be more data fetchers making use of the same data loader - then all those keys will be aggregated by graphql-java and the data loader will only be called once, when graphql-java sees fit).

I haven't worked with dataloader + graphql-java-tools yet, but I think you need something like this

fun loadUsers(env: DataFetchingEnvironment): Any {
  return Context.getDataLoaders(env)
    .usersLoader
    .loadMany(listOf(1, 2, 3))
}

williamboman-pp avatar Sep 26 '20 21:09 williamboman-pp

Any updates for that issue? I'm facing the same problem now. Here was discussion connected with dataFetcher link The easiest solution is calling dispatch every time but it not the right choice.

suspend fun loadUsers(env: DataFetchingEnvironment): List<UserGQL> {
        val dataLoader = Context.getDataLoaders(env).usersLoader
        val users = dataLoader
            .loadMany(listOf(1, 2, 3))
        dataLoader.dispatch()
           
        return users.await().map { UserGQL(it) } 
    }

So what is the best option to solve this?

Nokinori avatar Dec 19 '20 16:12 Nokinori