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

Implement support for @RunOnVirtualThread for gRPC services #34855

Merged
merged 1 commit into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

Check warning on line 1 in docs/src/main/asciidoc/grpc-virtual-threads.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Quarkus Virtual Thread support for gRPC services'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Quarkus Virtual Thread support for gRPC services'.", "location": {"path": "docs/src/main/asciidoc/grpc-virtual-threads.adoc", "range": {"start": {"line": 1, "column": 3}}}, "severity": "INFO"}

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]

Check warning on line 12 in docs/src/main/asciidoc/grpc-virtual-threads.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'see (For more information, see...)' rather than 'refer to'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'see (For more information, see...)' rather than 'refer to'.", "location": {"path": "docs/src/main/asciidoc/grpc-virtual-threads.adoc", "range": {"start": {"line": 12, "column": 8}}}, "severity": "INFO"}
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.

Check warning on line 20 in docs/src/main/asciidoc/grpc-virtual-threads.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer. Raw Output: {"message": "[Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer.", "location": {"path": "docs/src/main/asciidoc/grpc-virtual-threads.adoc", "range": {"start": {"line": 20, "column": 1}}}, "severity": "INFO"}

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.

Check warning on line 23 in docs/src/main/asciidoc/grpc-virtual-threads.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.CaseSensitiveTerms] Use 'OpenJDK' rather than 'JDK'. Raw Output: {"message": "[Quarkus.CaseSensitiveTerms] Use 'OpenJDK' rather than 'JDK'.", "location": {"path": "docs/src/main/asciidoc/grpc-virtual-threads.adoc", "range": {"start": {"line": 23, "column": 8}}}, "severity": "INFO"}
It will then be possible to perform blocking operations without blocking the platform thread upon which the virtual thread is mounted.

Check warning on line 24 in docs/src/main/asciidoc/grpc-virtual-threads.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'.", "location": {"path": "docs/src/main/asciidoc/grpc-virtual-threads.adoc", "range": {"start": {"line": 24, "column": 98}}}, "severity": "INFO"}

== 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:

Check warning on line 29 in docs/src/main/asciidoc/grpc-virtual-threads.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'verify' rather than 'make sure' unless updating existing content that uses it. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'verify' rather than 'make sure' unless updating existing content that uses it.", "location": {"path": "docs/src/main/asciidoc/grpc-virtual-threads.adoc", "range": {"start": {"line": 29, "column": 8}}}, "severity": "WARNING"}

[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

Check warning on line 41 in docs/src/main/asciidoc/grpc-virtual-threads.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.CaseSensitiveTerms] Use 'Gradle' rather than 'gradle'. Raw Output: {"message": "[Quarkus.CaseSensitiveTerms] Use 'Gradle' rather than 'gradle'.", "location": {"path": "docs/src/main/asciidoc/grpc-virtual-threads.adoc", "range": {"start": {"line": 41, "column": 8}}}, "severity": "INFO"}
----
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:

Check warning on line 46 in docs/src/main/asciidoc/grpc-virtual-threads.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'.", "location": {"path": "docs/src/main/asciidoc/grpc-virtual-threads.adoc", "range": {"start": {"line": 46, "column": 10}}}, "severity": "INFO"}

Check warning on line 46 in docs/src/main/asciidoc/grpc-virtual-threads.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'verify' rather than 'make sure' unless updating existing content that uses it. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'verify' rather than 'make sure' unless updating existing content that uses it.", "location": {"path": "docs/src/main/asciidoc/grpc-virtual-threads.adoc", "range": {"start": {"line": 46, "column": 18}}}, "severity": "WARNING"}

[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:

Check warning on line 57 in docs/src/main/asciidoc/grpc-virtual-threads.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'.", "location": {"path": "docs/src/main/asciidoc/grpc-virtual-threads.adoc", "range": {"start": {"line": 57, "column": 53}}}, "severity": "INFO"}

[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
Loading