Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support using DataLoader from suspend function #653

Conversation

koenpunt
Copy link
Contributor

@koenpunt koenpunt commented Mar 21, 2023

Rudimentary implementation to support returning a CompletableFuture from a suspend fun.

CoroutinesUtils.invokeSuspendingFunction wraps the return value of the function in a Mono (or Flux). But it also does this when a CompletableFuture is returned, and thus results in a Mono<CompletableFuture<?>>, which isn't captured by graphql-java, and thus the dataloader is never dispatched.

By unwrapping the future, and converting it to a mono (as opposed to wrapping), the dataloader seems to be dispatched correctly.

Haven't added any tests yet, but if you also think this would be the right approach I can work on adding some tests as well.

Related issue: #343

Rudimentary implementation to support returning a `CompletableFuture` from a suspend fun.

`CoroutinesUtils.invokeSuspendingFunction` wraps the return value of the function in a `Mono` (or `Flux`). But it also does this when a `CompletableFuture` is returned, and thus results in a `Mono<CompletableFuture<?>>`, which isn't captured by graphql-java, and thus the dataloader is never dispatched.

By unwrapping the future, and _converting_ it to a mono (as opposed to wrapping), the dataloader seems to be dispatched correctly.
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Mar 21, 2023
Comment on lines +87 to +94
Class<?> returnType = KotlinReflectionUtils.getReturnType(method);

if (CompletableFuture.class.isAssignableFrom(returnType)) {
@SuppressWarnings("unchecked")
Mono<CompletableFuture<?>> mono = (Mono<CompletableFuture<?>>)result;
// Unwrap nested CompletableFuture
return mono.flatMap(Mono::fromFuture);
}
Copy link
Contributor Author

@koenpunt koenpunt Mar 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@koenpunt
Copy link
Contributor Author

koenpunt commented Sep 4, 2023

@rstoyanchev did you or anyone on the team had a chance to look at this?

Copy link
Contributor

@rstoyanchev rstoyanchev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the ping and apologies for the delay.

Forgive the naive question, but just wondering why declare such a method that uses a DataLoader as a coroutine? It should already be non-blocking.

The implementation looks straight forward otherwise.

@hantsy
Copy link
Contributor

hantsy commented Sep 14, 2023

Check and vote up #393

@koenpunt
Copy link
Contributor Author

koenpunt commented Sep 14, 2023

Thanks for the ping and apologies for the delay.

Forgive the naive question, but just wondering why declare such a method that uses a DataLoader as a coroutine? It should already be non-blocking.

The implementation looks straight forward otherwise.

When first calling other suspending functions before loading data using the dataloader, e.g.;

We currently use this:

    @SchemaMapping
    fun transactions(
        model: Order,
        orderDataLoader: OrderDataLoader,
        paymentAccountDataLoader: PaymentAccountDataLoader,
        principal: Principal,
    ): CompletableFuture<List<OrderTransaction>> {
        return CoroutineScope(Dispatchers.IO).future {
            orderPaymentService.findAllByOrderId(model.id)
        }.thenCompose { orderPayments ->
            paymentAccountDataLoader
                .loadMany(orderPayments.mapNotNull { it.paymentAccountId })
                .also { paymentAccountDataLoader.dispatch() }

But when we can return a dataloader from a suspend function it would look more like:

    @SchemaMapping
    suspend fun transactions(
        model: Order,
        orderDataLoader: OrderDataLoader,
        paymentAccountDataLoader: PaymentAccountDataLoader,
        principal: Principal,
    ): CompletableFuture<List<OrderTransaction>> {
        val orderPayments = orderPaymentService.findAllByOrderId(model.id)

        return paymentAccountDataLoader
            .loadMany(orderPayments.mapNotNull { it.paymentAccountId })
            // Not sure if manual dispatch would still be necessary here.
            .also { paymentAccountDataLoader.dispatch() }
    }

@rstoyanchev rstoyanchev added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Sep 14, 2023
@rstoyanchev rstoyanchev added this to the 1.2.3 milestone Sep 14, 2023
@rstoyanchev
Copy link
Contributor

Thanks for clarifying.

@rstoyanchev rstoyanchev changed the title Support returning dataloader from suspend function Support using DataLoader from suspend function Sep 14, 2023
@hantsy
Copy link
Contributor

hantsy commented Sep 14, 2023

@koenpunt Add kotlin-coroutines-jdk8 into your deps, and use future builder to convert the block to a CompleteableFuture.

fun test() : CompletableFuture<List<OrderTransaction>>  = future{
}

@koenpunt
Copy link
Contributor Author

That doesn't work, because future is a method on the coroutine scope.

This is valid code, but doesn't work because then we're back at the original problem:

suspend fun test(): CompletableFuture<List<OrderTransaction>> = withContext(Dispatchers.IO) {
   future {
      // ...
   }
}

@rstoyanchev rstoyanchev self-assigned this Sep 14, 2023
rstoyanchev pushed a commit that referenced this pull request Sep 14, 2023
Rudimentary implementation to support returning a `CompletableFuture`
from a suspend function.

`CoroutinesUtils.invokeSuspendingFunction` wraps the return value of
the function in a `Mono` (or `Flux`). But it also does this when a
`CompletableFuture` is returned, and thus results in a
`Mono<CompletableFuture<?>>`, which isn't captured by graphql-java,
and thus the dataloader is never dispatched.

By unwrapping the future, and _converting_ it to a mono (as opposed
to wrapping), the dataloader is dispatched correctly.

See gh-653
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants