Skip to content

Commit 4435c0d

Browse files
authored
Merge pull request #47802 from robp94/feature/graphql-vt
Virtual thread support for smallrye-graphql
2 parents 271aeb6 + 3280ec4 commit 4435c0d

File tree

13 files changed

+875
-10
lines changed

13 files changed

+875
-10
lines changed

bom/application/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
<smallrye-health.version>4.2.0</smallrye-health.version>
5252
<smallrye-metrics.version>4.0.0</smallrye-metrics.version>
5353
<smallrye-open-api.version>4.0.11</smallrye-open-api.version>
54-
<smallrye-graphql.version>2.13.0</smallrye-graphql.version>
54+
<smallrye-graphql.version>2.14.0</smallrye-graphql.version>
5555
<smallrye-fault-tolerance.version>6.9.1</smallrye-fault-tolerance.version>
5656
<smallrye-jwt.version>4.6.2</smallrye-jwt.version>
5757
<smallrye-context-propagation.version>2.2.1</smallrye-context-propagation.version>

docs/src/main/asciidoc/smallrye-graphql.adoc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,22 @@ You can mix Blocking and Non-blocking in one request,
503503

504504
Above will fetch the film on the event-loop threads, but switch to the worker thread to fetch the heroes.
505505

506+
=== RunOnVirtualThread
507+
508+
Queries can be run on a virtual thread by adding `@RunOnVirtualThread` to the method:
509+
510+
[source,java]
511+
----
512+
@Query
513+
@Description("Get a Films from a galaxy far far away")
514+
@RunOnVirtualThread
515+
public Film getFilm(int filmId) {
516+
// ...
517+
}
518+
----
519+
520+
Please note the general guidelines for using xref:./virtual-threads.adoc[virtual threads]
521+
506522
== Abstract Types
507523

508524
The current schema is simple with only two concrete types, `Hero` and `Film`.

extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/BlockingHelper.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package io.quarkus.smallrye.graphql.runtime.spi.datafetcher;
22

33
import java.util.concurrent.Callable;
4+
import java.util.concurrent.ExecutorService;
45

6+
import io.quarkus.virtual.threads.VirtualThreadsRecorder;
57
import io.smallrye.graphql.schema.model.Execute;
68
import io.smallrye.graphql.schema.model.Operation;
79
import io.vertx.core.Context;
@@ -22,9 +24,27 @@ public static boolean nonBlockingShouldExecuteBlocking(Operation operation, Cont
2224
return operation.getExecute().equals(Execute.BLOCKING) && vc.isEventLoopContext();
2325
}
2426

27+
public static boolean shouldUseVirtualThread(Operation operation, Context vc) {
28+
// Use virtual threads if the operation is marked as run on virtual thread
29+
return operation.getExecute().equals(Execute.RUN_ON_VIRTUAL_THREAD);
30+
}
31+
2532
@SuppressWarnings("unchecked")
26-
public static void runBlocking(Context vc, Callable<Object> contextualCallable, Promise result) {
27-
// Here call blocking with context
28-
vc.executeBlocking(contextualCallable).onComplete(result);
33+
public static void runBlocking(Context vc, Callable<Object> contextualCallable, Promise result, Operation operation) {
34+
// Check if we should use virtual threads
35+
if (shouldUseVirtualThread(operation, vc)) {
36+
ExecutorService virtualThreadsExecutor = VirtualThreadsRecorder.getCurrent();
37+
virtualThreadsExecutor.submit(() -> {
38+
try {
39+
Object value = contextualCallable.call();
40+
result.complete(value);
41+
} catch (Throwable t) {
42+
result.fail(t);
43+
}
44+
});
45+
} else {
46+
// use regular blocking execution
47+
vc.executeBlocking(contextualCallable).onComplete(result);
48+
}
2949
}
3050
}

extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusCompletionStageDataFetcher.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ private Uni<?> handleUserMethodCallBlocking(Object[] transformedArguments, Conte
6161
});
6262

6363
// Here call blocking with context
64-
BlockingHelper.runBlocking(vc, contextualCallable, result);
64+
BlockingHelper.runBlocking(vc, contextualCallable, result, operation);
6565
return Uni.createFrom().completionStage(result.future().toCompletionStage());
6666
}
6767

@@ -86,7 +86,7 @@ private Uni<List<T>> handleUserBatchLoadBlocking(Object[] arguments, Context vc)
8686
});
8787

8888
// Here call blocking with context
89-
BlockingHelper.runBlocking(vc, contextualCallable, result);
89+
BlockingHelper.runBlocking(vc, contextualCallable, result, operation);
9090
return Uni.createFrom().completionStage(result.future().toCompletionStage());
9191
}
9292

extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusDefaultDataFetcher.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ private <T> T invokeAndTransformBlocking(final io.smallrye.graphql.api.Context c
103103
}
104104
});
105105
// Here call blocking with context
106-
BlockingHelper.runBlocking(vc, contextualCallable, result);
106+
BlockingHelper.runBlocking(vc, contextualCallable, result, operation);
107107

108108
return (T) Uni.createFrom().completionStage(result.future().toCompletionStage()).onItemOrFailure()
109109
.invoke((item, error) -> {
@@ -134,7 +134,7 @@ private CompletionStage<List<T>> invokeBatchBlocking(DataFetchingEnvironment dfe
134134
});
135135

136136
// Here call blocking with context
137-
BlockingHelper.runBlocking(vc, contextualCallable, result);
137+
BlockingHelper.runBlocking(vc, contextualCallable, result, operation);
138138
return result.future().toCompletionStage()
139139
.whenComplete((resultList, error) -> {
140140
if (error != null) {

extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusUniDataFetcher.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ private Uni<?> handleUserMethodCallBlocking(Object[] transformedArguments, Conte
5858
});
5959

6060
// Here call blocking with context
61-
BlockingHelper.runBlocking(vc, contextualCallable, result);
61+
BlockingHelper.runBlocking(vc, contextualCallable, result, operation);
6262
return Uni.createFrom().completionStage(result.future().toCompletionStage());
6363
}
6464

@@ -83,7 +83,7 @@ private Uni<List<T>> handleUserBatchLoadBlocking(Object[] arguments, Context vc)
8383
});
8484

8585
// Here call blocking with context
86-
BlockingHelper.runBlocking(vc, contextualCallable, result);
86+
BlockingHelper.runBlocking(vc, contextualCallable, result, operation);
8787
return Uni.createFrom().completionStage(result.future().toCompletionStage());
8888
}
8989

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<artifactId>quarkus-virtual-threads-integration-tests-parent</artifactId>
9+
<groupId>io.quarkus</groupId>
10+
<version>999-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>quarkus-integration-test-virtual-threads-graphql</artifactId>
14+
<name>Quarkus - Integration Tests - Virtual Threads - Graphql</name>
15+
16+
<dependencies>
17+
<dependency>
18+
<groupId>io.quarkus</groupId>
19+
<artifactId>quarkus-smallrye-graphql</artifactId>
20+
</dependency>
21+
<!-- Use the "compile" scope because we need to include the VirtualThreadsAssertions in the app -->
22+
<dependency>
23+
<groupId>io.quarkus</groupId>
24+
<artifactId>quarkus-test-vertx</artifactId>
25+
</dependency>
26+
<dependency>
27+
<groupId>io.quarkus</groupId>
28+
<artifactId>quarkus-junit5</artifactId>
29+
<scope>test</scope>
30+
</dependency>
31+
<dependency>
32+
<groupId>io.quarkus.junit5</groupId>
33+
<artifactId>junit5-virtual-threads</artifactId>
34+
<scope>test</scope>
35+
</dependency>
36+
<dependency>
37+
<groupId>io.rest-assured</groupId>
38+
<artifactId>rest-assured</artifactId>
39+
<scope>test</scope>
40+
</dependency>
41+
<dependency>
42+
<groupId>org.awaitility</groupId>
43+
<artifactId>awaitility</artifactId>
44+
<scope>test</scope>
45+
</dependency>
46+
<dependency>
47+
<groupId>org.assertj</groupId>
48+
<artifactId>assertj-core</artifactId>
49+
<scope>test</scope>
50+
</dependency>
51+
<dependency>
52+
<groupId>io.quarkus</groupId>
53+
<artifactId>quarkus-smallrye-graphql-deployment</artifactId>
54+
<version>${project.version}</version>
55+
<type>pom</type>
56+
<scope>test</scope>
57+
<exclusions>
58+
<exclusion>
59+
<groupId>*</groupId>
60+
<artifactId>*</artifactId>
61+
</exclusion>
62+
</exclusions>
63+
</dependency>
64+
</dependencies>
65+
66+
<build>
67+
<plugins>
68+
<plugin>
69+
<groupId>io.quarkus</groupId>
70+
<artifactId>quarkus-maven-plugin</artifactId>
71+
</plugin>
72+
<plugin>
73+
<groupId>org.apache.maven.plugins</groupId>
74+
<artifactId>maven-surefire-plugin</artifactId>
75+
</plugin>
76+
</plugins>
77+
</build>
78+
79+
</project>

integration-tests/virtual-threads/graphql-virtual-threads/src/main/resources/application.properties

Whitespace-only changes.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package io.quarkus.virtual.graphql;
2+
3+
import static io.quarkus.jsonp.JsonProviderHolder.jsonProvider;
4+
5+
import java.io.IOException;
6+
import java.io.StringReader;
7+
import java.io.StringWriter;
8+
import java.util.Map;
9+
import java.util.Properties;
10+
11+
import jakarta.json.JsonObject;
12+
import jakarta.json.JsonObjectBuilder;
13+
import jakarta.json.JsonReader;
14+
15+
import org.hamcrest.CoreMatchers;
16+
17+
import io.restassured.RestAssured;
18+
import io.restassured.parsing.Parser;
19+
import io.vertx.core.Context;
20+
import io.vertx.core.Vertx;
21+
22+
/**
23+
* Some shared methods
24+
*/
25+
public abstract class AbstractGraphQLTest {
26+
27+
static {
28+
RestAssured.registerParser("application/graphql+json", Parser.JSON);
29+
}
30+
31+
protected void pingTest() {
32+
pingPongTest("ping", "pong");
33+
}
34+
35+
protected void pongTest() {
36+
pingPongTest("pong", "ping");
37+
}
38+
39+
private void pingPongTest(String operationName, String message) {
40+
String pingRequest = getPayload("{\n" +
41+
" " + operationName + " {\n" +
42+
" message\n" +
43+
" }\n" +
44+
"}");
45+
46+
RestAssured.given().when()
47+
.accept(MEDIATYPE_JSON)
48+
.contentType(MEDIATYPE_JSON)
49+
.body(pingRequest)
50+
.post("/graphql")
51+
.then()
52+
.assertThat()
53+
.statusCode(200)
54+
.and()
55+
.body(CoreMatchers.containsString("{\"data\":{\"" + operationName + "\":{\"message\":\"" + message + "\"}}}"));
56+
}
57+
58+
protected String getPayload(String query) {
59+
return getPayload(query, null);
60+
}
61+
62+
protected String getPayload(String query, String variables) {
63+
JsonObject jsonObject = createRequestBody(query, variables);
64+
return jsonObject.toString();
65+
}
66+
67+
protected JsonObject createRequestBody(String graphQL, String variables) {
68+
// Create the request
69+
JsonObject vjo = jsonProvider().createObjectBuilder().build();
70+
if (variables != null && !variables.isEmpty()) {
71+
try (JsonReader jsonReader = jsonProvider().createReader(new StringReader(variables))) {
72+
vjo = jsonReader.readObject();
73+
}
74+
}
75+
76+
JsonObjectBuilder job = jsonProvider().createObjectBuilder();
77+
if (graphQL != null && !graphQL.isEmpty()) {
78+
job.add(QUERY, graphQL);
79+
}
80+
81+
return job.add(VARIABLES, vjo).build();
82+
}
83+
84+
protected static String getPropertyAsString() {
85+
return getPropertyAsString(null);
86+
}
87+
88+
protected static String getPropertyAsString(Map<String, String> otherProperties) {
89+
try {
90+
Properties p = new Properties();
91+
p.putAll(PROPERTIES);
92+
StringWriter writer = new StringWriter();
93+
if (otherProperties != null) {
94+
p.putAll(otherProperties);
95+
}
96+
p.store(writer, "Test Properties");
97+
return writer.toString();
98+
} catch (IOException ex) {
99+
throw new RuntimeException(ex);
100+
}
101+
}
102+
103+
protected static final String MEDIATYPE_JSON = "application/json";
104+
protected static final String MEDIATYPE_TEXT = "text/plain";
105+
protected static final String QUERY = "query";
106+
protected static final String VARIABLES = "variables";
107+
108+
protected static final Properties PROPERTIES = new Properties();
109+
static {
110+
PROPERTIES.put("smallrye.graphql.allowGet", "true");
111+
PROPERTIES.put("smallrye.graphql.printDataFetcherException", "true");
112+
PROPERTIES.put("smallrye.graphql.events.enabled", "true");
113+
}
114+
115+
/**
116+
* Hold info about a thread
117+
*/
118+
public static class TestThread {
119+
120+
private long id;
121+
private String name;
122+
private int priority;
123+
private String state;
124+
private String group;
125+
126+
public TestThread() {
127+
super();
128+
}
129+
130+
public TestThread(long id, String name, int priority, String state, String group) {
131+
this.id = id;
132+
this.name = name;
133+
this.priority = priority;
134+
this.state = state;
135+
this.group = group;
136+
}
137+
138+
public long getId() {
139+
return id;
140+
}
141+
142+
public void setId(long id) {
143+
this.id = id;
144+
}
145+
146+
public String getName() {
147+
return name;
148+
}
149+
150+
public void setName(String name) {
151+
this.name = name;
152+
}
153+
154+
public int getPriority() {
155+
return priority;
156+
}
157+
158+
public void setPriority(int priority) {
159+
this.priority = priority;
160+
}
161+
162+
public String getState() {
163+
return state;
164+
}
165+
166+
public void setState(String state) {
167+
this.state = state;
168+
}
169+
170+
public String getGroup() {
171+
return group;
172+
}
173+
174+
public void setGroup(String group) {
175+
this.group = group;
176+
}
177+
178+
public String getVertxContextClassName() {
179+
Context vc = Vertx.currentContext();
180+
return vc.getClass().getName();
181+
}
182+
}
183+
}

0 commit comments

Comments
 (0)