graphql-kotlin
graphql-kotlin copied to clipboard
Using a DataLoader in a FederatedTypeResolver hangs execution
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
DataLoadervia the givenDataFetchingEnvironment .await()on theCompletableFuture<T>list created by calling into theDataLoaderinFederatedTypeResolver.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.
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<*>>
regarding the FederatedTypeResolver, will check if we can just generalize and change the signature to return a CompletableFuture or have a different interface
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
Fixed in https://github.com/ExpediaGroup/graphql-kotlin/pull/1514