Skip to content

Commit 2e54ef1

Browse files
committed
Support virtual threads in webmvc.GraphQlWebSocketHandler
This commit ensures that the WebMVC `GraphQlWebSocketHandler` variant uses a virtual threads factory on Java 21+. On lower Java versions, the usual `Schedulers.newSingle` scheduler will be used. Closes gh-1326
1 parent a62538d commit 2e54ef1

File tree

6 files changed

+155
-2
lines changed

6 files changed

+155
-2
lines changed

spring-graphql/build.gradle

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1+
plugins {
2+
id 'org.springframework.graphql.multiReleaseJar'
3+
}
4+
15
description = "GraphQL Support for Spring Applications"
26

37
apply plugin: "kotlin"
48

9+
multiRelease {
10+
releaseVersions 21
11+
}
12+
13+
configurations {
14+
java21Api.extendsFrom(api)
15+
java21Implementation.extendsFrom(implementation)
16+
}
17+
518
dependencies {
619
api 'com.graphql-java:graphql-java'
720
api 'io.projectreactor:reactor-core'
@@ -99,3 +112,6 @@ dependencies {
99112
test {
100113
useJUnitPlatform()
101114
}
115+
java21Test {
116+
useJUnitPlatform()
117+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.execution;
18+
19+
import reactor.core.scheduler.Scheduler;
20+
import reactor.core.scheduler.Schedulers;
21+
22+
/**
23+
* Factory for Reactor {@link Scheduler schedulers}.
24+
* @author Brian Clozel
25+
* @since 2.0.0
26+
*/
27+
public abstract class ReactorSchedulers {
28+
29+
/**
30+
* Create a scheduler backed by a single new thread, using {@link Schedulers#newSingle(String)} on Java < 21
31+
* and a custom Virtual Thread factory on Java >= 21.
32+
* @param name component and thread name prefix
33+
*/
34+
public static Scheduler singleThread(String name) {
35+
return Schedulers.newSingle(name);
36+
}
37+
38+
}

spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlWebSocketHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@
4545
import reactor.core.publisher.Flux;
4646
import reactor.core.publisher.Mono;
4747
import reactor.core.scheduler.Scheduler;
48-
import reactor.core.scheduler.Schedulers;
4948

5049
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
5150
import org.springframework.graphql.execution.ErrorType;
51+
import org.springframework.graphql.execution.ReactorSchedulers;
5252
import org.springframework.graphql.execution.SubscriptionPublisherException;
5353
import org.springframework.graphql.server.WebGraphQlHandler;
5454
import org.springframework.graphql.server.WebGraphQlResponse;
@@ -487,7 +487,7 @@ private static class SessionState {
487487

488488
SessionState(String graphQlSessionId, WebMvcSessionInfo sessionInfo) {
489489
this.sessionInfo = sessionInfo;
490-
this.scheduler = Schedulers.newSingle("GraphQL-WsSession-" + graphQlSessionId);
490+
this.scheduler = ReactorSchedulers.singleThread("GraphQL-WsSession-" + graphQlSessionId);
491491
this.keepAliveSubscriber = new KeepAliveSubscriber(sessionInfo.getSession());
492492
}
493493

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.execution;
18+
19+
import reactor.core.scheduler.Scheduler;
20+
import reactor.core.scheduler.Schedulers;
21+
22+
/**
23+
* Factory for Reactor {@link Scheduler schedulers}.
24+
* @author Brian Clozel
25+
* @since 2.0.0
26+
*/
27+
public abstract class ReactorSchedulers {
28+
29+
/**
30+
* Create a scheduler backed by a single new thread, using {@link Schedulers#newSingle(String)} on Java < 21
31+
* and a custom Virtual Thread factory on Java >= 21.
32+
* @param name component and thread name prefix
33+
*/
34+
public static Scheduler singleThread(String name) {
35+
return Schedulers.newSingle(Thread.ofVirtual().name(name).factory());
36+
}
37+
38+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2020-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Support for GraphQL request execution, including abstractions to configure and invoke
19+
* {@code graphql.GraphQL}.
20+
*/
21+
@NullMarked
22+
package org.springframework.graphql.execution;
23+
24+
import org.jspecify.annotations.NullMarked;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.execution;
18+
19+
import org.junit.jupiter.api.Test;
20+
import reactor.core.Disposable;
21+
import reactor.core.scheduler.Scheduler;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
import static org.awaitility.Awaitility.await;
25+
26+
class ReactorSchedulersTests {
27+
28+
@Test
29+
void singleThreadReturnsVirtualThread() {
30+
Scheduler scheduler = ReactorSchedulers.singleThread("testThread");
31+
scheduler.init();
32+
Disposable disposable = scheduler.schedule(() -> assertThat(Thread.currentThread().isVirtual()).isTrue());
33+
await().until(disposable::isDisposed);
34+
scheduler.dispose();
35+
}
36+
37+
}

0 commit comments

Comments
 (0)