graphql-kotlin icon indicating copy to clipboard operation
graphql-kotlin copied to clipboard

Using a DataLoader in a FederatedTypeResolver hangs execution

Open roookeee opened this issue 3 years ago • 3 comments

Library Version 6.1.0

Describe the bug When loading data via a DataLoader through the DataFetchingEnvironment in a FederatedTypeResolver, the execution hangs and will never finish. This is probalby related to #986 as FederatedTypeResolver.resolve is a suspend fun which calls into the DataLoader which creates nested asynchronicity: suspend fun -> CompletableFuture<T> of the DataLoader. This probably breaks the automatic dispatch as manually calling dispatch after the List<CompletableFuture<T>> has been created by calling the DataLoader removes the issue.

To Reproduce

  • Create a FederatedTypeResolver
  • Use a DataLoader via the given DataFetchingEnvironment
  • .await() on the CompletableFuture<T> list created by calling into the DataLoader in FederatedTypeResolver.resolve

Expected behavior DataLoaders should be usable in a FederatedTypeResolver. Compared to #986 we do not have a work around / alternative way because we are locked into the suspend + CompletableFuture<T> of the DataLoader here as the signature of resolve forces us to await() here.

roookeee avatar Aug 03 '22 11:08 roookeee

Hello 👋 , as mentioned on our docs

Given that graphql-java relies on CompletableFutures for scheduling and asynchronous execution of DataLoader calls, currently we don't provide any native support for DataLoader pattern using coroutines. Instead, return the CompletableFuture directly from your DataLoaders.

if you want to use DataLoaders you need to return a CompletableFuture<*> and stop marking the method as suspend, the graphql-java engine only supports CompletableFuture for async computations.

So for example in your data fetcher you need to go from this:

suspend fun user(id: Int): User? =
        userService.getUser(id)

to this:

fun user(id: Int, environment: DataFetchingEnvironment): CompletableFuture<User?> =
  environment
    .getDataLoader<Int, User?>("UserDataLoader")
    .load(id)

And in your DataLoader do something like this:

DataLoaderFactory.newDataLoader { ids, environment ->
  val coroutineScope =
    environment.getLoadContext<DataFetchingEnvironment>()?.graphQlContext?.get<CoroutineScope>()
    ?: CoroutineScope(EmptyCoroutineContext)

    coroutineScope.async {
      userService.getUsers(ids)
    }.asCompletableFuture()
}

grabbing a CoroutineScope from the GraphQLCotext will effectively allow you run suspend functions and then easily interop the computation to a CompletableFuture<List<*>>

samuelAndalon avatar Aug 03 '22 16:08 samuelAndalon

regarding the FederatedTypeResolver, will check if we can just generalize and change the signature to return a CompletableFuture or have a different interface

samuelAndalon avatar Aug 03 '22 16:08 samuelAndalon

I am aware of what you wrote in your first response, but I am just talking about the FederatedTypeResolver + DataLoader via DataFetchingEnvironment scenario. FederatedTypeResolver exposes an API that always runs into this issue: you cannot use any DataLoader in a FederatedTypeResolver which makes a DataLoader centric design break apart in this regard.

Another FederatedTypeResolver interface based on CompletableFuture would be an interesting solution, thanks! <3

roookeee avatar Aug 03 '22 16:08 roookeee

Fixed in https://github.com/ExpediaGroup/graphql-kotlin/pull/1514

dariuszkuc avatar Sep 13 '22 16:09 dariuszkuc