diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java index 6ea196a4d1cc3..af9fd3b96ba51 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java @@ -98,6 +98,7 @@ import jdk.internal.net.http.common.OperationTrackers.Tracker; import jdk.internal.net.http.common.Utils.SafeExecutor; import jdk.internal.net.http.common.Utils.SafeExecutorService; +import jdk.internal.net.http.common.Utils.UseVTForSelector; import jdk.internal.net.http.websocket.BuilderImpl; import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; @@ -126,6 +127,16 @@ final class HttpClientImpl extends HttpClient implements Trackable { static final long IDLE_CONNECTION_TIMEOUT_H2 = getTimeoutProp("jdk.httpclient.keepalive.timeout.h2", KEEP_ALIVE_TIMEOUT); static final long IDLE_CONNECTION_TIMEOUT_H3 = getTimeoutProp("jdk.httpclient.keepalive.timeout.h3", IDLE_CONNECTION_TIMEOUT_H2); + static final UseVTForSelector USE_VT_FOR_SELECTOR = + Utils.useVTForSelector("jdk.internal.httpclient.tcp.selector.useVirtualThreads", "default"); + private static boolean useVtForSelector() { + return switch (USE_VT_FOR_SELECTOR) { + case ALWAYS -> true; + case NEVER -> false; + default -> true; + }; + } + // Define the default factory as a static inner class // that embeds all the necessary logic to avoid // the risk of using a lambda that might keep a reference on the @@ -293,7 +304,6 @@ static CompletableFuture registerPending(PendingRequest pending, Completa if (pending.cf.isDone()) return res; var client = pending.client; - var cf = pending.cf; var id = pending.id; boolean added = client.pendingRequests.add(pending); // this may immediately remove `pending` from the set is the cf is already completed @@ -342,6 +352,7 @@ static void abortPendingRequests(HttpClientImpl client, Throwable reason) { private final boolean isDefaultExecutor; private final SSLContext sslContext; private final SSLParameters sslParams; + private final Thread selmgrThread; private final SelectorManager selmgr; private final FilterFactory filters; private final Http2ClientImpl client2; @@ -509,7 +520,11 @@ private HttpClientImpl(HttpClientBuilderImpl builder, // unlikely throw new UncheckedIOException(e); } - selmgr.setDaemon(true); + selmgrThread = useVtForSelector() + ? Thread.ofVirtual().name("HttpClient-" + id + "-SelectorManager") + .inheritInheritableThreadLocals(false).unstarted(selmgr) + : Thread.ofPlatform().name("HttpClient-" + id + "-SelectorManager") + .inheritInheritableThreadLocals(false).daemon().unstarted(selmgr); filters = new FilterFactory(); initFilters(); assert facadeRef.get() != null; @@ -528,7 +543,7 @@ void onSubmitFailure(Runnable command, Throwable failure) { private void start() { try { - selmgr.start(); + selmgrThread.start(); } catch (Throwable t) { isStarted.set(true); throw t; @@ -635,7 +650,7 @@ public void shutdownNow() { @Override public boolean awaitTermination(Duration duration) throws InterruptedException { // Implicit NPE will be thrown if duration is null - return selmgr.join(duration); + return selmgrThread.join(duration); } @Override @@ -927,7 +942,7 @@ void eventUpdated(AsyncEvent event) throws ClosedChannelException { } boolean isSelectorThread() { - return Thread.currentThread() == selmgr; + return Thread.currentThread() == selmgrThread; } AltServicesRegistry registry() { return registry; } @@ -1157,7 +1172,7 @@ private static CompletableFuture> translateSendAsyncExecFail } // Main loop for this client's selector - private static final class SelectorManager extends Thread { + private static final class SelectorManager implements Runnable { // For testing purposes we have an internal System property that // can control the frequency at which the selector manager will wake @@ -1196,9 +1211,6 @@ private static final class SelectorManager extends Thread { private final ReentrantLock lock = new ReentrantLock(); SelectorManager(HttpClientImpl ref) throws IOException { - super(null, null, - "HttpClient-" + ref.id + "-SelectorManager", - 0, false); owner = ref; debug = ref.debug; debugtimeout = ref.debugtimeout; @@ -1221,7 +1233,7 @@ IOException selectorClosedException() { } void eventUpdated(AsyncEvent e) throws ClosedChannelException { - if (Thread.currentThread() == this) { + if (owner.isSelectorThread()) { SelectionKey key = e.channel().keyFor(selector); if (key != null && key.isValid()) { SelectorAttachment sa = (SelectorAttachment) key.attachment(); @@ -1315,6 +1327,10 @@ void abort(Throwable t) { if (!inSelectorThread) selector.wakeup(); } + String getName() { + return owner.selmgrThread.getName(); + } + // Only called by the selector manager thread private void shutdown() { try { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java index a02506cff5c76..20b0338215c77 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java @@ -242,6 +242,15 @@ private static Set getDisallowedRedirectHeaders() { return true; }; + public enum UseVTForSelector { ALWAYS, NEVER, DEFAULT } + + public static UseVTForSelector useVTForSelector(String property, String defval) { + String useVtForSelector = System.getProperty(property, defval); + return Stream.of(UseVTForSelector.values()) + .filter((v) -> v.name().equalsIgnoreCase(useVtForSelector)) + .findFirst().orElse(UseVTForSelector.DEFAULT); + } + public static T addSuppressed(T x, Throwable suppressed) { if (x != suppressed && suppressed != null) { var sup = x.getSuppressed(); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java index 150d6233953c2..0f5aa4cb7ace8 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java @@ -143,8 +143,9 @@ void incomingStatelessReset() { Log.logError("{0}: stateless reset from peer ({1})", connection.logTag(), (peerIsServer ? "server" : "client")); } + var label = "quic:" + connection.uniqueId(); final SilentTermination st = forSilentTermination("stateless reset from peer (" - + (peerIsServer ? "server" : "client") + ")"); + + (peerIsServer ? "server" : "client") + ") on " + label); terminate(st); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java index a7469f18ed852..72ce0290038e1 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java @@ -346,8 +346,10 @@ private void idleTimedOut() { } } // silently close the connection and discard all its state - final TerminationCause cause = forSilentTermination("connection idle timed out (" - + timeoutMillis + " milli seconds)"); + var type = connection.isClientConnection() ? "client" : "server"; + var label = "quic:" + connection.uniqueId(); + final TerminationCause cause = forSilentTermination(type + " connection idle timed out (" + + timeoutMillis + " milli seconds) on " + label); connection.terminator.terminate(cause); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java index d9bf5fe6dcf18..b1de5ef4bfd4f 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java @@ -62,6 +62,7 @@ import jdk.internal.net.http.common.TimeLine; import jdk.internal.net.http.common.TimeSource; import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.common.Utils.UseVTForSelector; import jdk.internal.net.http.quic.QuicSelector.QuicNioSelector; import jdk.internal.net.http.quic.QuicSelector.QuicVirtualThreadPoller; import jdk.internal.net.http.quic.packets.QuicPacket.HeadersType; @@ -116,7 +117,6 @@ public abstract sealed class QuicEndpoint implements AutoCloseable static final boolean DGRAM_SEND_ASYNC; static final int MAX_BUFFERED_HIGH; static final int MAX_BUFFERED_LOW; - enum UseVTForSelector { ALWAYS, NEVER, DEFAULT } static final UseVTForSelector USE_VT_FOR_SELECTOR; static { // This default value is the maximum payload size of @@ -144,11 +144,8 @@ enum UseVTForSelector { ALWAYS, NEVER, DEFAULT } if (maxBufferLow >= maxBufferHigh) maxBufferLow = maxBufferHigh >> 1; MAX_BUFFERED_HIGH = maxBufferHigh; MAX_BUFFERED_LOW = maxBufferLow; - String useVtForSelector = - System.getProperty("jdk.internal.httpclient.quic.selector.useVirtualThreads", "default"); - USE_VT_FOR_SELECTOR = Stream.of(UseVTForSelector.values()) - .filter((v) -> v.name().equalsIgnoreCase(useVtForSelector)) - .findFirst().orElse(UseVTForSelector.DEFAULT); + var property = "jdk.internal.httpclient.quic.selector.useVirtualThreads"; + USE_VT_FOR_SELECTOR = Utils.useVTForSelector(property, "default"); } /** diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java index 9fa825459ff47..02db895d27cc0 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java @@ -45,9 +45,9 @@ import jdk.internal.net.http.common.TimeLine; import jdk.internal.net.http.common.TimeSource; import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.common.Utils.UseVTForSelector; import jdk.internal.net.http.quic.QuicEndpoint.QuicVirtualThreadedEndpoint; import jdk.internal.net.http.quic.QuicEndpoint.QuicSelectableEndpoint; -import jdk.internal.net.http.quic.QuicEndpoint.UseVTForSelector; import static jdk.internal.net.http.quic.QuicEndpoint.USE_VT_FOR_SELECTOR; diff --git a/test/jdk/java/net/httpclient/ReferenceTracker.java b/test/jdk/java/net/httpclient/ReferenceTracker.java index 6ea97ab6ac289..f167a086fbab0 100644 --- a/test/jdk/java/net/httpclient/ReferenceTracker.java +++ b/test/jdk/java/net/httpclient/ReferenceTracker.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -390,12 +390,6 @@ private static void checkOutstandingOperations(StringBuilder warning, } } - private boolean isSelectorManager(Thread t) { - String name = t.getName(); - if (name == null) return false; - return name.contains("SelectorManager"); - } - // This is a slightly more permissive check than the default checks, // it only verifies that all CFs returned by send/sendAsync have been // completed, and that all opened channels have been closed, and that diff --git a/test/jdk/java/net/httpclient/http2/H2SelectorVTTest.java b/test/jdk/java/net/httpclient/http2/H2SelectorVTTest.java new file mode 100644 index 0000000000000..b980df991387c --- /dev/null +++ b/test/jdk/java/net/httpclient/http2/H2SelectorVTTest.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Stream; +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions; + +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpClient.Version.HTTP_2; + +/* + * @test id=default + * @bug 8372159 + * @summary Verifies whether `SelectorManager` uses virtual threads + * as expected when no explicit configuration is provided + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run junit/othervm + * -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors + * H2SelectorVTTest + */ +/* + * @test id=never + * @bug 8372159 + * @summary Verifies that `SelectorManager` does *not* use virtual threads + when explicitly configured to "never" use them + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run junit/othervm + * -Djdk.internal.httpclient.tcp.selector.useVirtualThreads=never + * -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors + * H2SelectorVTTest + */ +/* + * @test id=always + * @bug 8372159 + * @summary Verifies that `SelectorManager` does *always* use virtual threads + when explicitly configured to "always" use them + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run junit/othervm + * -Djdk.internal.httpclient.tcp.selector.useVirtualThreads=always + * -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors + * H2SelectorVTTest + */ +/* + * @test id=explicit-default + * @bug 8372159 + * @summary Verifies whether `SelectorManager` uses virtual threads + * as expected when `default` is explicitly configured + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run junit/othervm + * -Djdk.internal.httpclient.tcp.selector.useVirtualThreads=default + * -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors + * H2SelectorVTTest + */ +/* + * @test id=garbage + * @bug 8372159 + * @summary Verifies whether `SelectorManager` uses virtual threads when + it is configured using an invalid value + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run junit/othervm + * -Djdk.internal.httpclient.tcp.selector.useVirtualThreads=garbage + * -Djdk.httpclient.HttpClient.log=requests,responses,headers,errors + * H2SelectorVTTest + */ +// -Djava.security.debug=all +class H2SelectorVTTest implements HttpServerAdapters { + + private static SSLContext sslContext; + private static HttpTestServer h2Server; + private static String requestURI; + + enum UseVTForSelector { ALWAYS, NEVER, DEFAULT } + private static final String PROP_NAME = "jdk.internal.httpclient.tcp.selector.useVirtualThreads"; + private static final UseVTForSelector USE_VT_FOR_SELECTOR; + static { + String useVtForSelector = + System.getProperty(PROP_NAME, "default"); + USE_VT_FOR_SELECTOR = Stream.of(UseVTForSelector.values()) + .filter((v) -> v.name().equalsIgnoreCase(useVtForSelector)) + .findFirst().orElse(UseVTForSelector.DEFAULT); + } + + private static boolean isTCPSelectorThreadVirtual() { + return switch (USE_VT_FOR_SELECTOR) { + case ALWAYS -> true; + case NEVER -> false; + default -> true; + }; + } + + @BeforeAll + static void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + // create a h2 server + h2Server = HttpTestServer.create(HTTP_2, sslContext); + h2Server.addHandler((exchange) -> exchange.sendResponseHeaders(200, 0), "/hello"); + h2Server.start(); + System.out.println("Server started at " + h2Server.getAddress()); + requestURI = "https://" + h2Server.serverAuthority() + "/hello"; + } + + @AfterAll + static void afterClass() throws Exception { + if (h2Server != null) { + System.out.println("Stopping server " + h2Server.getAddress()); + h2Server.stop(); + } + } + + /** + * Issues various HTTP/2 requests and verifies the responses are received + */ + @Test + void testBasicRequests() throws Exception { + try (final HttpClient client = HttpClient.newBuilder() + .proxy(NO_PROXY) + .sslContext(sslContext).build()) { + final URI reqURI = new URI(requestURI); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI); + + // GET + final HttpRequest req1 = reqBuilder.copy().GET().build(); + System.out.println("\nIssuing request: " + req1); + final HttpResponse resp1 = client.send(req1, BodyHandlers.ofString()); + Assertions.assertEquals(200, resp1.statusCode(), "unexpected response code for GET request"); + assertSelectorThread(client); + + // POST + final HttpRequest req2 = reqBuilder.copy().POST(BodyPublishers.ofString("foo")).build(); + System.out.println("\nIssuing request: " + req2); + final HttpResponse resp2 = client.send(req2, BodyHandlers.ofString()); + Assertions.assertEquals(200, resp2.statusCode(), "unexpected response code for POST request"); + assertSelectorThread(client); + + // HEAD + final HttpRequest req3 = reqBuilder.copy().HEAD().build(); + System.out.println("\nIssuing request: " + req3); + final HttpResponse resp3 = client.send(req3, BodyHandlers.ofString()); + Assertions.assertEquals(200, resp3.statusCode(), "unexpected response code for HEAD request"); + assertSelectorThread(client); + } + } + + // This method attempts to determine whether the selector thread + // is a platform thread or a virtual thread, and throws if expectations + // ar not met. + // Since we don't have access to the selector thread, the method + // uses a roundabout way to figure this out: it enumerates all + // platform threads, and if it finds a thread whose name matches + // the expected name of the selector thread it concludes that the + // selector thread is a platform thread. Otherwise, it assumes + // that the thread is virtual. + private static void assertSelectorThread(HttpClient client) { + String cname = client.toString(); + String clientId = cname.substring(cname.indexOf('(') + 1, cname.length() -1); + String name = "HttpClient-" + clientId + "-SelectorManager"; + Set threads = new HashSet<>(Thread.getAllStackTraces().keySet().stream() + .map(Thread::getName) + .toList()); + boolean found = threads.contains(name); + String status = found == isTCPSelectorThreadVirtual() ? "ERROR" : "SUCCESS"; + String propval = System.getProperty(PROP_NAME); + if (propval == null) { + System.out.printf("%s not defined, virtual=%s, thread found=%s%n", + PROP_NAME, isTCPSelectorThreadVirtual(), found); + } else { + System.out.printf("%s=%s, virtual=%s, thread found=%s%n", + PROP_NAME, propval, isTCPSelectorThreadVirtual(), found); + } + final String msg; + if (found) { + msg = "%s found in %s".formatted(name, threads); + System.out.printf("%s: %s%n", status, msg); + } else { + msg = "%s not found in %s".formatted(name, threads); + System.out.printf("%s: %s%n", status, msg); + } + Assertions.assertEquals(!isTCPSelectorThreadVirtual(), found, msg); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3QuicVTTest.java b/test/jdk/java/net/httpclient/http3/H3QuicVTTest.java index c9af069c728d7..19aa8f053a307 100644 --- a/test/jdk/java/net/httpclient/http3/H3QuicVTTest.java +++ b/test/jdk/java/net/httpclient/http3/H3QuicVTTest.java @@ -196,6 +196,15 @@ void testBasicRequests() throws Exception { } } + // This method attempts to determine whether the quic selector thread + // is a platform thread or a virtual thread, and throws if expectations + // ar not met. + // Since we don't have access to the quic selector thread, the method + // uses a roundabout way to figure this out: it enumerates all + // platform threads, and if it finds a thread whose name matches + // the expected name of the quic selector thread it concludes that the + // selector thread is a platform thread. Otherwise, it assumes + // that the thread is virtual. private static void assertSelectorThread(HttpClient client) { String clientId = client.toString().substring(client.toString().indexOf('(')); String name = "Thread(QuicSelector(HttpClientImpl" + clientId + "))";