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

Async execution of blocking controller methods on Java 21+ #958

Closed
jord1e opened this issue Apr 24, 2024 · 6 comments
Closed

Async execution of blocking controller methods on Java 21+ #958

jord1e opened this issue Apr 24, 2024 · 6 comments
Assignees
Labels
type: enhancement A general enhancement
Milestone

Comments

@jord1e
Copy link
Contributor

jord1e commented Apr 24, 2024

Hello everyone,

I looked into the virtual thread support today.

It seems, that we have to wrap all @schemamapping annotated methods to return Callable:

  @SchemaMapping
  public boolean isLatest(ParentObject parent) {
    return true; // Long running I/O task
  }

Would have to become

  @SchemaMapping
  public Callable<Boolean> isLatest(ParentObject parent) {
    return () -> {    
        return true; // Long running I/O task
    };
  }

DGS automatically works with virtual threads on annotated controllers. Would it be an idea to implement this in Spring for GraphQL?
An opt-in configuration option would probably be required, and it would only work on JDK 21+.

https://netflix.github.io/dgs/advanced/virtual-threads/

It filters out CompletableFutures etc.:
https://github.com/Netflix/dgs-framework/blob/ee3d9eba9485cafadfc4cc9d2bbf0c6d4c8bdd89/graphql-dgs/src/main/java21/com.netflix.graphql.dgs.internal.VirtualThreadTaskExecutor/VirtualThreadTaskExecutor.java#L35
https://github.com/Netflix/dgs-framework/blob/master/graphql-dgs/src/main/kotlin/com/netflix/graphql/dgs/internal/CompletableFutureWrapper.kt#L51-L53

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Apr 24, 2024
@rstoyanchev
Copy link
Contributor

I think we can do this similar to the blocking execution support for WebFlux controller methods where you can configure an Executor and, optionally, a controller method Predicate<HandlerMethod> to control which methods are considered blocking. The default predicate matches all methods that don't return a type that's not a reactive type, nor future, nor a Kotlin suspending method.

@rstoyanchev rstoyanchev added this to the 1.3.0 milestone May 1, 2024
@rstoyanchev rstoyanchev added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels May 1, 2024
@rstoyanchev rstoyanchev changed the title Why not automatically wrap non-TrivialDataFetcher's in virtual threads? Blocking method execution for virtual thread setups May 1, 2024
@jord1e
Copy link
Contributor Author

jord1e commented May 1, 2024

I think it's a solid proposal.

I think we can do this similar to the blocking execution support for WebFlux controller methods where you can configure an Executor

This would create two separate points to define an Executor:

  1. Using the AnnotatedControllerConfigurer for methods returning Callable (how this is done properly should maybe be documented?)
  2. For this new feature where we wrap controller methods with virtual threads using this new configurable Executor

The default predicate matches all methods that don't return a type that's not a reactive type, nor future, nor a Kotlin suspending method.

Reactive type would be everything mentioned in the Javadoc of ReactiveAdapterRegistry right?
I think this is reasonable. As long as it applies to methods annotated by SpringGraphQL-controller-annotations

Should Callable be in this list? I assume it is a special case within Spring for GraphQL
grafik

and, optionally, a controller method Predicate to control which methods are considered blocking.

Within GraphQL Java it is perfectly possible to have something like this:

@QueryMapping
public Object x() {
  if (...) {
    return CompletableFuture.supplyAsync(() -> "String Result", cpuBoundExecutor);
  } else {
    return "Invalid String result";
  }
}

Thus it would execute everything in a virtual thread (including the nested CompletableFuture), because the return type is Object.

In DGS they also use the return type of the method for determining this, which makes sense.
Maybe this edge-case should be documented/a warning message produced?

@rstoyanchev
Copy link
Contributor

rstoyanchev commented May 1, 2024

Maybe we are saying the same thing, but for this we would re-use the existing Executor configurable via AnnotatedControllerConfigurer. However, instead of Callable return types only, we would also use it for imperative controller methods, and that would be possible to control with an additional blockingMethodPredicate property. We'll apply this behavior automatically with Java 21+ when an Executor is configured.

Reactive type would be everything mentioned in the Javadoc of ReactiveAdapterRegistry right?

That's correct. As for Callable, by itself is not a reactive/async type.

it would execute everything in a virtual thread (including the nested CompletableFuture), because the return type is Object.

We could have special handling for Object to ensure the actual instance isn't a reactive/async type already.

@jord1e
Copy link
Contributor Author

jord1e commented May 1, 2024

Maybe we are saying the same thing, but for this we would re-use the existing Executor configurable via AnnotatedControllerConfigurer. However, instead of Callable return types only, we would also use it for imperative controller methods, and that would be possible to control with an additional blockingMethodPredicate property. We'll apply this behavior automatically with Java 21+ when an Executor is configured.

👍
Do we really want this to be default behaviour?
Not all libraries are fully adapted yet (MySQL connector and HikariCP are ones I know of). The carrier thread of the virtual thread will stay pinned until a later JDK version: https://mail.openjdk.org/pipermail/loom-dev/2024-February/006433.html

I think DGS approaches this in a good way:
https://netflix.github.io/dgs/advanced/virtual-threads/#virtual-threads-jdk-21

They have a configuration option that creates a virtual thread group specifically for the data fetchers.
(defined here https://github.com/Netflix/dgs-framework/blob/ee3d9eba9485cafadfc4cc9d2bbf0c6d4c8bdd89/graphql-dgs/src/main/java21/com.netflix.graphql.dgs.internal.VirtualThreadTaskExecutor/VirtualThreadTaskExecutor.java#L43)
Although what you propose is a bit more general than virtual threads, users could also just refer to a ForkJoinPool executor (although it would make little sense).

And you need to manually enable Spring Framework support (spring.threads.virtual.enabled=true)

We could have special handling for Object to ensure the actual instance isn't a reactive/async type already.
This would require calling the method. If we want the method to execute in a virtual thread, this is thus not possible (as the type will be a non-"async" type)

Only after the execution/return of the method do we know if the return type is "async/CF/Callable/Kotlin Couritine" and if this edge-case applies (it might only return the non-async-type in a fast path or something).
If that makes sense?
I think we shouldn't worry about it for now.

@rstoyanchev
Copy link
Contributor

rstoyanchev commented May 1, 2024

Do we really want this to be default behaviour?

It's not on by default. You'd have to explicitly configure an Executor on AnnotatedControllerConfigurer, and the Boot starter does this when spring.threads.virtual.enabled=true. I don't imagine anyone would pass a non-elastic (e.g. forkjoin threadpool) for data fetching purposes. If this doesn't work well, you can get control through the blockingMethodPredicate.

Only after the execution/return of the method do we know if the return type is "async/CF/Callable/Kotlin Couritine" and if this edge-case applies (it might only return the non-async-type in a fast path or something).

Yes, this would have to be a request time check. This is more of a, what we could do if the need arises, but I don't think it needs to be there to start.

@rstoyanchev rstoyanchev self-assigned this May 7, 2024
@rstoyanchev rstoyanchev changed the title Blocking method execution for virtual thread setups Async execution of blocking controller methods on Java 21+ May 7, 2024
@rstoyanchev
Copy link
Contributor

This is now in to try. The behavior is enabled if running on Java 21+ and the Executor is not a thread pool executor.

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

No branches or pull requests

3 participants