Skip to content

Commit 1142d29

Browse files
committed
8369920: HttpClient QuicSelectorThread could be a VirtualThread
Reviewed-by: vyazici, djelinski
1 parent 98f40e4 commit 1142d29

File tree

3 files changed

+287
-11
lines changed

3 files changed

+287
-11
lines changed

src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ public abstract sealed class QuicEndpoint implements AutoCloseable
116116
static final boolean DGRAM_SEND_ASYNC;
117117
static final int MAX_BUFFERED_HIGH;
118118
static final int MAX_BUFFERED_LOW;
119+
enum UseVTForSelector { ALWAYS, NEVER, DEFAULT }
120+
static final UseVTForSelector USE_VT_FOR_SELECTOR;
119121
static {
120122
// This default value is the maximum payload size of
121123
// an IPv6 datagram, which is 65527 (which is bigger
@@ -142,6 +144,11 @@ public abstract sealed class QuicEndpoint implements AutoCloseable
142144
if (maxBufferLow >= maxBufferHigh) maxBufferLow = maxBufferHigh >> 1;
143145
MAX_BUFFERED_HIGH = maxBufferHigh;
144146
MAX_BUFFERED_LOW = maxBufferLow;
147+
String useVtForSelector =
148+
System.getProperty("jdk.internal.httpclient.quic.selector.useVirtualThreads", "default");
149+
USE_VT_FOR_SELECTOR = Stream.of(UseVTForSelector.values())
150+
.filter((v) -> v.name().equalsIgnoreCase(useVtForSelector))
151+
.findFirst().orElse(UseVTForSelector.DEFAULT);
145152
}
146153

147154
/**
@@ -821,7 +828,7 @@ void channelReadLoop() {
821828
// to the selector to process the event queue
822829
assert this instanceof QuicEndpoint.QuicSelectableEndpoint
823830
: "unexpected endpoint type: " + this.getClass() + "@[" + name + "]";
824-
assert Thread.currentThread() instanceof QuicSelector.QuicSelectorThread;
831+
assert QuicSelector.isSelectorThread();
825832
if (Log.quicRetransmit() || Log.quicTimer()) {
826833
Log.logQuic(name() + ": reschedule needed: " + Utils.debugDeadline(now, pending)
827834
+ ", totalpkt: " + totalpkt

src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
import jdk.internal.net.http.common.Utils;
4848
import jdk.internal.net.http.quic.QuicEndpoint.QuicVirtualThreadedEndpoint;
4949
import jdk.internal.net.http.quic.QuicEndpoint.QuicSelectableEndpoint;
50+
import jdk.internal.net.http.quic.QuicEndpoint.UseVTForSelector;
51+
52+
import static jdk.internal.net.http.quic.QuicEndpoint.USE_VT_FOR_SELECTOR;
5053

5154

5255
/**
@@ -62,6 +65,9 @@ public abstract sealed class QuicSelector<T extends QuicEndpoint> implements Run
6265
public static final long IDLE_PERIOD_MS = 1500;
6366

6467
private static final TimeLine source = TimeSource.source();
68+
private static final ScopedValue<Boolean> IS_SELECTOR =
69+
ScopedValue.newInstance();
70+
6571
final Logger debug = Utils.getDebugLogger(this::name);
6672

6773
private final String name;
@@ -74,7 +80,11 @@ private QuicSelector(QuicInstance instance, String name) {
7480
this.instance = instance;
7581
this.name = name;
7682
this.timerQueue = new QuicTimerQueue(this::wakeup, debug);
77-
this.thread = new QuicSelectorThread(this);
83+
this.thread = QuicSelectorThread.of(USE_VT_FOR_SELECTOR, this);
84+
}
85+
86+
public static boolean isSelectorThread() {
87+
return IS_SELECTOR.orElse(Boolean.FALSE);
7888
}
7989

8090
public String name() {
@@ -95,6 +105,13 @@ public QuicTimerQueue timer() {
95105
return timerQueue;
96106
}
97107

108+
@Override
109+
public final void run() {
110+
ScopedValue.where(IS_SELECTOR, true).run(this::runSelector);
111+
}
112+
113+
abstract void runSelector();
114+
98115
/**
99116
* A {@link QuicSelector} implementation based on blocking
100117
* {@linkplain DatagramChannel Datagram Channels} and using a
@@ -116,6 +133,7 @@ static final class EndpointTask implements Runnable {
116133
this.endpoints = endpoints;
117134
}
118135

136+
@Override
119137
public void run() {
120138
try {
121139
endpoint.channelReadLoop();
@@ -200,7 +218,7 @@ public void wakeup() {
200218
}
201219

202220
@Override
203-
public void run() {
221+
void runSelector() {
204222
try {
205223
if (debug.on()) debug.log("started");
206224
long waited = 0;
@@ -321,7 +339,7 @@ public void shutdown() {
321339
}
322340

323341
@Override
324-
public void run() {
342+
void runSelector() {
325343
try {
326344
if (debug.on()) debug.log("started");
327345
while (!done()) {
@@ -455,7 +473,8 @@ boolean done() {
455473
* @param unit the timeout unit
456474
*/
457475
public void awaitTermination(long timeout, TimeUnit unit) {
458-
if (Thread.currentThread() == thread) {
476+
if (isSelectorThread()) {
477+
assert Thread.currentThread() == thread.thread();
459478
return;
460479
}
461480
try {
@@ -488,12 +507,37 @@ public void abort(Throwable t) {
488507
shutdown();
489508
}
490509

491-
static class QuicSelectorThread extends Thread {
492-
QuicSelectorThread(QuicSelector<?> selector) {
493-
super(null, selector,
494-
"Thread(%s)".formatted(selector.name()),
495-
0, false);
496-
this.setDaemon(true);
510+
private record QuicSelectorThread(Thread thread) {
511+
void start() {
512+
thread.start();
513+
}
514+
void join(long millis) throws InterruptedException {
515+
thread.join(millis);
516+
}
517+
static QuicSelectorThread ofPlatform(QuicSelector<?> selector) {
518+
Thread thread = Thread.ofPlatform()
519+
.name("Thread(%s)".formatted(selector.name()))
520+
.stackSize(0)
521+
.inheritInheritableThreadLocals(false)
522+
.daemon()
523+
.unstarted(selector);
524+
return new QuicSelectorThread(thread);
525+
}
526+
static QuicSelectorThread ofVirtual(QuicSelector<?> selector) {
527+
Thread thread = Thread.ofVirtual()
528+
.name("Thread(%s)".formatted(selector.name()))
529+
.inheritInheritableThreadLocals(false)
530+
.unstarted(selector);
531+
return new QuicSelectorThread(thread);
532+
}
533+
static QuicSelectorThread of(UseVTForSelector config, QuicSelector<?> selector) {
534+
return switch (config) {
535+
case ALWAYS -> ofVirtual(selector);
536+
case NEVER -> ofPlatform(selector);
537+
default -> selector instanceof QuicNioSelector
538+
? ofPlatform(selector)
539+
: ofVirtual(selector);
540+
};
497541
}
498542
}
499543

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
import java.net.URI;
25+
import java.net.http.HttpClient;
26+
import java.net.http.HttpRequest;
27+
import java.net.http.HttpRequest.BodyPublishers;
28+
import java.net.http.HttpResponse;
29+
import java.net.http.HttpResponse.BodyHandlers;
30+
import java.util.HashSet;
31+
import java.util.Objects;
32+
import java.util.Set;
33+
import java.util.stream.Stream;
34+
import javax.net.ssl.SSLContext;
35+
36+
import jdk.httpclient.test.lib.common.HttpServerAdapters;
37+
import jdk.test.lib.Platform;
38+
import jdk.test.lib.net.SimpleSSLContext;
39+
40+
import org.junit.jupiter.api.AfterAll;
41+
import org.junit.jupiter.api.BeforeAll;
42+
import org.junit.jupiter.api.Test;
43+
import org.junit.jupiter.api.Assertions;
44+
45+
import static java.net.http.HttpClient.Builder.NO_PROXY;
46+
import static java.net.http.HttpClient.Version.HTTP_3;
47+
import static java.net.http.HttpOption.H3_DISCOVERY;
48+
import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY;
49+
50+
/*
51+
* @test id=default
52+
* @bug 8369920
53+
* @summary Verifies whether `QuicSelector` uses virtual threads
54+
* as expected when no explicit configuration is provided
55+
* @library /test/lib /test/jdk/java/net/httpclient/lib
56+
* @build jdk.test.lib.net.SimpleSSLContext
57+
* jdk.httpclient.test.lib.common.HttpServerAdapters
58+
* @run junit/othervm
59+
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors,http3
60+
* H3QuicVTTest
61+
*/
62+
/*
63+
* @test id=never
64+
* @bug 8369920
65+
* @summary Verifies that `QuicSelector` does *not* use virtual threads
66+
when explicitly configured to "never" use them
67+
* @library /test/lib /test/jdk/java/net/httpclient/lib
68+
* @build jdk.test.lib.net.SimpleSSLContext
69+
* jdk.httpclient.test.lib.common.HttpServerAdapters
70+
* @run junit/othervm
71+
* -Djdk.internal.httpclient.quic.selector.useVirtualThreads=never
72+
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors,http3
73+
* H3QuicVTTest
74+
*/
75+
/*
76+
* @test id=always
77+
* @bug 8369920
78+
* @summary Verifies that `QuicSelector` does *always* use virtual threads
79+
when explicitly configured to "always" use them
80+
* @library /test/lib /test/jdk/java/net/httpclient/lib
81+
* @build jdk.test.lib.net.SimpleSSLContext
82+
* jdk.httpclient.test.lib.common.HttpServerAdapters
83+
* @run junit/othervm
84+
* -Djdk.internal.httpclient.quic.selector.useVirtualThreads=always
85+
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors,http3
86+
* H3QuicVTTest
87+
*/
88+
/*
89+
* @test id=explicit-default
90+
* @bug 8369920
91+
* @summary Verifies whether `QuicSelector` uses virtual threads
92+
* as expected when `default` is explicitly configured
93+
* @library /test/lib /test/jdk/java/net/httpclient/lib
94+
* @build jdk.test.lib.net.SimpleSSLContext
95+
* jdk.httpclient.test.lib.common.HttpServerAdapters
96+
* @run junit/othervm
97+
* -Djdk.internal.httpclient.quic.selector.useVirtualThreads=default
98+
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors,http3
99+
* H3QuicVTTest
100+
*/
101+
/*
102+
* @test id=garbage
103+
* @bug 8369920
104+
* @summary Verifies whether `QuicSelector` uses virtual threads when
105+
it is configured using an invalid value
106+
* @library /test/lib /test/jdk/java/net/httpclient/lib
107+
* @build jdk.test.lib.net.SimpleSSLContext
108+
* jdk.httpclient.test.lib.common.HttpServerAdapters
109+
* @run junit/othervm
110+
* -Djdk.internal.httpclient.quic.selector.useVirtualThreads=garbage
111+
* -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors,http3
112+
* H3QuicVTTest
113+
*/
114+
// -Djava.security.debug=all
115+
class H3QuicVTTest implements HttpServerAdapters {
116+
117+
private static SSLContext sslContext;
118+
private static HttpTestServer h3Server;
119+
private static String requestURI;
120+
121+
enum UseVTForSelector { ALWAYS, NEVER, DEFAULT }
122+
private static final String PROP_NAME = "jdk.internal.httpclient.quic.selector.useVirtualThreads";
123+
private static final UseVTForSelector USE_VT_FOR_SELECTOR;
124+
static {
125+
String useVtForSelector =
126+
System.getProperty(PROP_NAME, "default");
127+
USE_VT_FOR_SELECTOR = Stream.of(UseVTForSelector.values())
128+
.filter((v) -> v.name().equalsIgnoreCase(useVtForSelector))
129+
.findFirst().orElse(UseVTForSelector.DEFAULT);
130+
}
131+
132+
private static boolean isQuicSelectorThreadVirtual() {
133+
return switch (USE_VT_FOR_SELECTOR) {
134+
case ALWAYS -> true;
135+
case NEVER -> false;
136+
default -> !Platform.isWindows();
137+
};
138+
}
139+
140+
@BeforeAll
141+
static void beforeClass() throws Exception {
142+
sslContext = new SimpleSSLContext().get();
143+
if (sslContext == null) {
144+
throw new AssertionError("Unexpected null sslContext");
145+
}
146+
// create an H3 only server
147+
h3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext);
148+
h3Server.addHandler((exchange) -> exchange.sendResponseHeaders(200, 0), "/hello");
149+
h3Server.start();
150+
System.out.println("Server started at " + h3Server.getAddress());
151+
requestURI = "https://" + h3Server.serverAuthority() + "/hello";
152+
}
153+
154+
@AfterAll
155+
static void afterClass() throws Exception {
156+
if (h3Server != null) {
157+
System.out.println("Stopping server " + h3Server.getAddress());
158+
h3Server.stop();
159+
}
160+
}
161+
162+
/**
163+
* Issues various HTTP3 requests and verifies the responses are received
164+
*/
165+
@Test
166+
void testBasicRequests() throws Exception {
167+
try (final HttpClient client = newClientBuilderForH3()
168+
.proxy(NO_PROXY)
169+
.version(HTTP_3)
170+
.sslContext(sslContext).build()) {
171+
final URI reqURI = new URI(requestURI);
172+
final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI)
173+
.version(HTTP_3)
174+
.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY);
175+
176+
// GET
177+
final HttpRequest req1 = reqBuilder.copy().GET().build();
178+
System.out.println("\nIssuing request: " + req1);
179+
final HttpResponse<Void> resp1 = client.send(req1, BodyHandlers.discarding());
180+
Assertions.assertEquals(200, resp1.statusCode(), "unexpected response code for GET request");
181+
assertSelectorThread(client);
182+
183+
// POST
184+
final HttpRequest req2 = reqBuilder.copy().POST(BodyPublishers.ofString("foo")).build();
185+
System.out.println("\nIssuing request: " + req2);
186+
final HttpResponse<Void> resp2 = client.send(req2, BodyHandlers.discarding());
187+
Assertions.assertEquals(200, resp2.statusCode(), "unexpected response code for POST request");
188+
assertSelectorThread(client);
189+
190+
// HEAD
191+
final HttpRequest req3 = reqBuilder.copy().HEAD().build();
192+
System.out.println("\nIssuing request: " + req3);
193+
final HttpResponse<Void> resp3 = client.send(req3, BodyHandlers.discarding());
194+
Assertions.assertEquals(200, resp3.statusCode(), "unexpected response code for HEAD request");
195+
assertSelectorThread(client);
196+
}
197+
}
198+
199+
private static void assertSelectorThread(HttpClient client) {
200+
String clientId = client.toString().substring(client.toString().indexOf('('));
201+
String name = "Thread(QuicSelector(HttpClientImpl" + clientId + "))";
202+
Set<String> threads = new HashSet<>(Thread.getAllStackTraces().keySet().stream()
203+
.map(Thread::getName)
204+
.toList());
205+
boolean found = threads.contains(name);
206+
String status = found == isQuicSelectorThreadVirtual() ? "ERROR" : "SUCCESS";
207+
String propval = System.getProperty(PROP_NAME);
208+
if (propval == null) {
209+
System.out.printf("%s not defined, virtual=%s, thread found=%s%n",
210+
PROP_NAME, isQuicSelectorThreadVirtual(), found);
211+
} else {
212+
System.out.printf("%s=%s, virtual=%s, thread found=%s%n",
213+
PROP_NAME, propval, isQuicSelectorThreadVirtual(), found);
214+
}
215+
final String msg;
216+
if (found) {
217+
msg = "%s found in %s".formatted(name, threads);
218+
System.out.printf("%s: %s%n", status, msg);
219+
} else {
220+
msg = "%s not found in %s".formatted(name, threads);
221+
System.out.printf("%s: %s%n", status, msg);
222+
}
223+
Assertions.assertEquals(!isQuicSelectorThreadVirtual(), found, msg);
224+
}
225+
}

0 commit comments

Comments
 (0)