Skip to content

Commit

Permalink
Merge pull request #34855 from cescoffier/gRPC-Virtual-threads
Browse files Browse the repository at this point in the history
Implement support for @RunOnVirtualThread for gRPC services
  • Loading branch information
cescoffier authored Jul 21, 2023
2 parents 199ab44 + 1c825ef commit 9e7befa
Show file tree
Hide file tree
Showing 24 changed files with 1,303 additions and 62 deletions.
45 changes: 44 additions & 1 deletion .github/workflows/ci-actions-incremental.yml
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,50 @@ jobs:
quarkus-quickstarts/target/build-report.json
quarkus-quickstarts/LICENSE
retention-days: 2

virtual-thread-tests:
name: Virtual Thread Support Tests - JDK ${{matrix.java.name}}
runs-on: ${{matrix.java.os-name}}
needs: [build-jdk11, calculate-test-jobs]
# Skip main in forks
if: "needs.calculate-test-jobs.outputs.run_quickstarts == 'true' && (github.repository == 'quarkusio/quarkus' || !endsWith(github.ref, '/main'))"
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
java:
- {
name: "20",
java-version: 20,
os-name: "ubuntu-latest",
extra-args: "--enable-preview"
}
steps:
- uses: actions/checkout@v3
- name: Download Maven Repo
uses: actions/download-artifact@v3
with:
name: maven-repo
path: .
- name: Extract Maven Repo
shell: bash
run: tar -xzf maven-repo.tgz -C ~
- name: Set up JDK ${{ matrix.java.java-version }}
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: ${{ matrix.java.java-version }}
- name: Run tests
run: |
export LANG=en_US && ./mvnw -e -B -fae --settings .github/mvn-settings.xml -f integration-tests/virtual-threads clean verify -Dextra-args=${{matrix.java.extra-args}}
- name: Upload build reports (if build failed)
uses: actions/upload-artifact@v3
if: ${{ failure() || cancelled() }}
with:
name: "build-reports-Virtual Thread Support - JDK ${{matrix.java.name}}"
path: |
integration-tests/virtual-threads/**/target/*-reports/TEST-*.xml
integration-tests/virtual-threads/target/build-report.json
retention-days: 2
tcks-test:
name: MicroProfile TCKs Tests
needs: [build-jdk11, calculate-test-jobs]
Expand Down
4 changes: 4 additions & 0 deletions docs/src/main/asciidoc/grpc-service-implementation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,7 @@ To disable the gRPC server metrics when `quarkus-micrometer` is used, add the fo
----
quarkus.micrometer.binder.grpc-server.enabled=false
----

=== Use virtual threads

To use virtual threads in your gRPC service implementation, check the dedicated xref:./grpc-virtual-threads.adoc[guide].
150 changes: 150 additions & 0 deletions docs/src/main/asciidoc/grpc-virtual-threads.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
= Quarkus Virtual Thread support for gRPC services

include::_attributes.adoc[]
:runonvthread: https://javadoc.io/doc/io.smallrye.common/smallrye-common-annotation/latest/io/smallrye/common/annotation/RunOnVirtualThread.html
:blocking_annotation: https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/latest/io/smallrye/reactive/messaging/annotations/Blocking.html

This guide explains how to benefit from Java virtual threads when implementing a gRPC service.

[TIP]
====
This guide focuses on using virtual threads with the gRPC extensions.
Please refer to xref:virtual-threads.adoc[Writing simpler reactive REST services with Quarkus Virtual Thread support]
to read more about Java virtual threads in general and the Quarkus Virtual Thread support.
====

By default, the Quarkus gRPC extension invokes service methods on an event-loop thread.
See the xref:quarkus-reactive-architecture.adoc[Quarkus Reactive Architecture documentation] for further details on this topic.
But, you can also use the link:{blocking_annotation}[@Blocking] annotation to indicate that the service is _blocking_ and should be run on a worker thread.

The idea behind Quarkus Virtual Thread support for gRPC services is to offload the service method invocation on virtual threads, instead of running it on an event-loop thread or a worker thread.

To enable virtual thread support on a service method, simply add the link:{runonvthread}[@RunOnVirtualThread] annotation to the method.
If the JDK is compatible (Java 19 or later versions) then the invocation will be offloaded to a new virtual thread.
It will then be possible to perform blocking operations without blocking the platform thread upon which the virtual thread is mounted.

== Configuring gRPC services to use virtual threads

Let's see an example of how to implement a gRPC service using virtual threads.
First, make sure to have the gRPC extension dependency in your build file:

[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
.pom.xml
----
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-grpc</artifactId>
</dependency>
----

[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"]
.build.gradle
----
implementation("io.quarkus:quarkus-grpc")
----

You also need to make sure that you are using Java 19 or later, this can be enforced in your `pom.xml` file with the following:

[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
.pom.xml
----
<properties>
<maven.compiler.source>19</maven.compiler.source>
<maven.compiler.target>19</maven.compiler.target>
</properties>
----

Virtual threads are still a preview feature, so you need to start your application with the `--enable-preview` flag:

[source, bash]
----
java --enable-preview -jar target/quarkus-app/quarkus-run.jar
----

or to use the Quarkus Dev mode, insert the following to the `quarkus-maven-plugin` configuration:

[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
.pom.xml
----
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.version}</version>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
<configuration>
<source>19</source>
<target>19</target>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
----

NOTE: The `--enable-preview` flag is not necessary with Java 21+.

Then you can start using the annotation `@RunOnVirtualThread` in your service implementation:

[source, java]
----
package io.quarkus.grpc.example.streaming;
import com.google.protobuf.ByteString;
import com.google.protobuf.EmptyProtos;
import io.grpc.testing.integration.Messages;
import io.grpc.testing.integration.TestService;
import io.quarkus.grpc.GrpcService;
import io.smallrye.common.annotation.RunOnVirtualThread;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
@GrpcService
public class TestServiceImpl implements TestService {
@RunOnVirtualThread
@Override
public Uni<EmptyProtos.Empty> emptyCall(EmptyProtos.Empty request) {
return Uni.createFrom().item(EmptyProtos.Empty.newBuilder().build());
}
@RunOnVirtualThread
@Override
public Uni<Messages.SimpleResponse> unaryCall(Messages.SimpleRequest request) {
var value = request.getPayload().getBody().toStringUtf8();
var resp = Messages.SimpleResponse.newBuilder()
.setPayload(Messages.Payload.newBuilder().setBody(ByteString.copyFromUtf8(value.toUpperCase())).build())
.build();
return Uni.createFrom().item(resp);
}
@Override
@RunOnVirtualThread
public Multi<Messages.StreamingOutputCallResponse> streamingOutputCall(Messages.StreamingOutputCallRequest request) {
var value = request.getPayload().getBody().toStringUtf8();
return Multi.createFrom().<String> emitter(emitter -> {
emitter.emit(value.toUpperCase());
emitter.emit(value.toUpperCase());
emitter.emit(value.toUpperCase());
emitter.complete();
}).map(v -> Messages.StreamingOutputCallResponse.newBuilder()
.setPayload(Messages.Payload.newBuilder().setBody(ByteString.copyFromUtf8(v)).build())
.build());
}
}
----

[IMPORTANT]
.Limitations
====
The gRPC methods receiving _streams_, such as a `Multi` cannot use `@RunOnVirtualThread`, as the method must not be blocking and produce its result (`Multi` or `Uni`) immediately.
====
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public final class BindableServiceBuildItem extends MultiBuildItem {

final DotName serviceClass;
final List<String> blockingMethods = new ArrayList<>();
final List<String> virtualMethods = new ArrayList<>();

public BindableServiceBuildItem(DotName serviceClass) {
this.serviceClass = serviceClass;
Expand All @@ -27,10 +28,25 @@ public void registerBlockingMethod(String method) {
blockingMethods.add(method);
}

/**
* A method from {@code serviceClass} is annotated with {@link io.smallrye.common.annotation.RunOnVirtualThread}.
* Stores the method name so the runtime interceptor can recognize it.
* Note: gRPC method have unique names - overloading is not permitted.
*
* @param method the method name
*/
public void registerVirtualMethod(String method) {
virtualMethods.add(method);
}

public boolean hasBlockingMethods() {
return !blockingMethods.isEmpty();
}

public boolean hasVirtualMethods() {
return !virtualMethods.isEmpty();
}

public DotName getServiceClass() {
return serviceClass;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.quarkus.grpc.runtime.supports.GrpcClientConfigProvider;
import io.smallrye.common.annotation.Blocking;
import io.smallrye.common.annotation.NonBlocking;
import io.smallrye.common.annotation.RunOnVirtualThread;

public class GrpcDotNames {

Expand All @@ -38,6 +39,7 @@ public class GrpcDotNames {

public static final DotName BLOCKING = DotName.createSimple(Blocking.class.getName());
public static final DotName NON_BLOCKING = DotName.createSimple(NonBlocking.class.getName());
public static final DotName RUN_ON_VIRTUAL_THREAD = DotName.createSimple(RunOnVirtualThread.class.getName());
public static final DotName TRANSACTIONAL = DotName.createSimple("jakarta.transaction.Transactional");

public static final DotName ABSTRACT_BLOCKING_STUB = DotName.createSimple(AbstractBlockingStub.class.getName());
Expand Down
Loading

0 comments on commit 9e7befa

Please sign in to comment.