Skip to content

Commit 51c4bb0

Browse files
authored
Merge pull request #44498 from mkouba/issue-44469
WebSockets Next: add rule for Transactional annotation
2 parents 0e67055 + ce12ec3 commit 51c4bb0

File tree

8 files changed

+136
-12
lines changed

8 files changed

+136
-12
lines changed

docs/src/main/asciidoc/websockets-next-reference.adoc

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -233,20 +233,23 @@ Method receiving messages from the client are annotated with `@OnTextMessage` or
233233

234234
==== Invocation rules
235235

236-
When invoking these annotated methods, the _session_ scope linked to the WebSocket connection remains active.
236+
When invoking the callback methods, the _session_ scope linked to the WebSocket connection remains active.
237237
In addition, the request scope is active until the completion of the method (or until it produces its result for async and reactive methods).
238238

239-
Quarkus WebSocket Next supports _blocking_ and _non-blocking_ logic, akin to Quarkus REST, determined by the method signature and additional annotations such as `@Blocking` and `@NonBlocking`.
239+
WebSocket Next supports _blocking_ and _non-blocking_ logic, akin to Quarkus REST, determined from the return type of the method and additional annotations such as `@Blocking` and `@NonBlocking`.
240240

241241
Here are the rules governing execution:
242242

243-
* Non-blocking methods must execute on the connection's event loop.
244-
* Methods annotated with `@RunOnVirtualThread` are considered blocking and should execute on a virtual thread.
245-
* Blocking methods must execute on a worker thread if not annotated with `@RunOnVirtualThread`.
246-
* When `@RunOnVirtualThread` is employed, each invocation spawns a new virtual thread.
247-
* Methods returning `CompletionStage`, `Uni` and `Multi` are considered non-blocking.
248-
* Methods returning `void` or plain objects are considered blocking.
249-
* Kotlin `suspend` functions are considered non-blocking.
243+
* Methods annotated with `@RunOnVirtualThread`, `@Blocking` or `@Transactional` are considered blocking.
244+
* Methods annotated with `@NonBlocking` are considered non-blocking.
245+
* Methods declared on a class annotated with `@Transactional` are considered blocking unless annotated with `@NonBlocking`.
246+
* If the method does not declare any of the annotations listed above the execution model is derived from the return type:
247+
** Methods returning `Uni` and `Multi` are considered non-blocking.
248+
** Methods returning `void` or any other type are considered blocking.
249+
* Kotlin `suspend` functions are always considered non-blocking and may not be annotated with `@Blocking`, `@NonBlocking` or `@RunOnVirtualThread`.
250+
* Non-blocking methods must execute on the connection's event loop thread.
251+
* Blocking methods must execute on a worker thread unless annotated with `@RunOnVirtualThread`.
252+
* Methods annotated with `@RunOnVirtualThread` must execute on a virtual thread, each invocation spawns a new virtual thread.
250253

251254
==== Method parameters
252255

extensions/websockets-next/deployment/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@
9191
<artifactId>opentelemetry-semconv</artifactId>
9292
<scope>test</scope>
9393
</dependency>
94+
<dependency>
95+
<groupId>jakarta.transaction</groupId>
96+
<artifactId>jakarta.transaction-api</artifactId>
97+
<scope>test</scope>
98+
</dependency>
9499
</dependencies>
95100

96101
<build>

extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketDotNames.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ final class WebSocketDotNames {
5454
static final DotName HANDSHAKE_REQUEST = DotName.createSimple(HandshakeRequest.class);
5555
static final DotName THROWABLE = DotName.createSimple(Throwable.class);
5656
static final DotName CLOSE_REASON = DotName.createSimple(CloseReason.class);
57+
static final DotName TRANSACTIONAL = DotName.createSimple("jakarta.transaction.Transactional");
5758

5859
static final List<DotName> CALLBACK_ANNOTATIONS = List.of(ON_OPEN, ON_CLOSE, ON_BINARY_MESSAGE, ON_TEXT_MESSAGE,
5960
ON_PONG_MESSAGE, ON_ERROR);

extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1578,13 +1578,16 @@ private static ExecutionModel executionModel(MethodInfo method, TransformedAnnot
15781578
throw new WebSocketException("Kotlin `suspend` functions in WebSockets Next endpoints may not be "
15791579
+ "annotated @Blocking, @NonBlocking or @RunOnVirtualThread: " + method);
15801580
}
1581-
15821581
if (transformedAnnotations.hasAnnotation(method, WebSocketDotNames.RUN_ON_VIRTUAL_THREAD)) {
15831582
return ExecutionModel.VIRTUAL_THREAD;
15841583
} else if (transformedAnnotations.hasAnnotation(method, WebSocketDotNames.BLOCKING)) {
15851584
return ExecutionModel.WORKER_THREAD;
15861585
} else if (transformedAnnotations.hasAnnotation(method, WebSocketDotNames.NON_BLOCKING)) {
15871586
return ExecutionModel.EVENT_LOOP;
1587+
} else if (transformedAnnotations.hasAnnotation(method, WebSocketDotNames.TRANSACTIONAL)
1588+
|| transformedAnnotations.hasAnnotation(method.declaringClass(), WebSocketDotNames.TRANSACTIONAL)) {
1589+
// Method annotated with @Transactional or declared on a class annotated @Transactional is also treated as a blocking method
1590+
return ExecutionModel.WORKER_THREAD;
15881591
} else {
15891592
return hasBlockingSignature(method) ? ExecutionModel.WORKER_THREAD : ExecutionModel.EVENT_LOOP;
15901593
}

extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/executionmodel/BlockingAnnotationTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ public class BlockingAnnotationTest {
3535

3636
@Test
3737
void testEndoint() {
38-
try (WSClient client = new WSClient(vertx).connect(endUri)) {
38+
try (WSClient client = new WSClient(vertx)) {
39+
client.connect(endUri);
3940
assertEquals("evenloop:false,worker:true", client.sendAndAwaitReply("foo").toString());
4041
}
4142
}

extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/executionmodel/NonBlockingAnnotationTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ public class NonBlockingAnnotationTest {
3434

3535
@Test
3636
void testEndoint() {
37-
try (WSClient client = new WSClient(vertx).connect(endUri)) {
37+
try (WSClient client = new WSClient(vertx)) {
38+
client.connect(endUri);
3839
assertEquals("evenloop:true,worker:false", client.sendAndAwaitReply("foo").toString());
3940
}
4041
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package io.quarkus.websockets.next.test.executionmodel;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.net.URI;
6+
7+
import jakarta.inject.Inject;
8+
import jakarta.transaction.Transactional;
9+
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import io.quarkus.test.QuarkusUnitTest;
14+
import io.quarkus.test.common.http.TestHTTPResource;
15+
import io.quarkus.websockets.next.OnTextMessage;
16+
import io.quarkus.websockets.next.WebSocket;
17+
import io.quarkus.websockets.next.test.utils.WSClient;
18+
import io.smallrye.mutiny.Uni;
19+
import io.vertx.core.Context;
20+
import io.vertx.core.Vertx;
21+
22+
public class TransactionalClassTest {
23+
24+
@RegisterExtension
25+
public static final QuarkusUnitTest test = new QuarkusUnitTest()
26+
.withApplicationRoot(root -> {
27+
root.addClasses(Endpoint.class, WSClient.class);
28+
});
29+
30+
@Inject
31+
Vertx vertx;
32+
33+
@TestHTTPResource("endpoint")
34+
URI endUri;
35+
36+
@Test
37+
void testEndoint() {
38+
try (WSClient client = new WSClient(vertx)) {
39+
client.connect(endUri);
40+
assertEquals("evenloop:false,worker:true", client.sendAndAwaitReply("foo").toString());
41+
}
42+
}
43+
44+
@Transactional
45+
@WebSocket(path = "/endpoint")
46+
public static class Endpoint {
47+
48+
@OnTextMessage
49+
Uni<String> message(String ignored) {
50+
return Uni.createFrom().item("evenloop:" + Context.isOnEventLoopThread() + ",worker:" + Context.isOnWorkerThread());
51+
}
52+
53+
}
54+
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package io.quarkus.websockets.next.test.executionmodel;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.net.URI;
6+
7+
import jakarta.inject.Inject;
8+
import jakarta.transaction.Transactional;
9+
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import io.quarkus.test.QuarkusUnitTest;
14+
import io.quarkus.test.common.http.TestHTTPResource;
15+
import io.quarkus.websockets.next.OnTextMessage;
16+
import io.quarkus.websockets.next.WebSocket;
17+
import io.quarkus.websockets.next.test.utils.WSClient;
18+
import io.smallrye.mutiny.Uni;
19+
import io.vertx.core.Context;
20+
import io.vertx.core.Vertx;
21+
22+
public class TransactionalMethodTest {
23+
24+
@RegisterExtension
25+
public static final QuarkusUnitTest test = new QuarkusUnitTest()
26+
.withApplicationRoot(root -> {
27+
root.addClasses(Endpoint.class, WSClient.class);
28+
});
29+
30+
@Inject
31+
Vertx vertx;
32+
33+
@TestHTTPResource("endpoint")
34+
URI endUri;
35+
36+
@Test
37+
void testEndoint() {
38+
try (WSClient client = new WSClient(vertx)) {
39+
client.connect(endUri);
40+
assertEquals("evenloop:false,worker:true", client.sendAndAwaitReply("foo").toString());
41+
}
42+
}
43+
44+
@WebSocket(path = "/endpoint")
45+
public static class Endpoint {
46+
47+
@Transactional
48+
@OnTextMessage
49+
Uni<String> message(String ignored) {
50+
return Uni.createFrom().item("evenloop:" + Context.isOnEventLoopThread() + ",worker:" + Context.isOnWorkerThread());
51+
}
52+
53+
}
54+
55+
}

0 commit comments

Comments
 (0)