Skip to content

Commit 567f83d

Browse files
fitekoneaarturobernalg
authored andcommitted
TTPCLIENT-2394: Virtual threads support for classic client
Add VirtualThreadCloseableHttpClient and builder options to run I/O (and optionally handler) on JDK 21 virtual threads. Default behavior unchanged; classic path used when VT unavailable. Include VirtualThreadSupport utility and example.
1 parent 72a00a0 commit 567f83d

File tree

6 files changed

+987
-2
lines changed

6 files changed

+987
-2
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.impl;
28+
29+
import java.lang.reflect.Method;
30+
import java.util.concurrent.ExecutorService;
31+
import java.util.concurrent.ThreadFactory;
32+
33+
import org.apache.hc.core5.annotation.Internal;
34+
35+
/**
36+
* Utilities for working with JDK 21 virtual threads without introducing a hard runtime dependency.
37+
*
38+
* <p>
39+
* <p>
40+
* All methods use reflection to detect and construct virtual-thread components so that the client
41+
* <p>
42+
* remains source- and binary-compatible with earlier JDKs. On runtimes where virtual threads are
43+
* <p>
44+
* unavailable, the helpers either return {@code false} (for detection) or throw
45+
* <p>
46+
* {@link UnsupportedOperationException} (for construction).
47+
* </p>
48+
*
49+
**/
50+
@Internal
51+
public final class VirtualThreadSupport {
52+
53+
private VirtualThreadSupport() {
54+
}
55+
56+
public static boolean isAvailable() {
57+
try {
58+
Class.forName("java.lang.Thread$Builder$OfVirtual", false,
59+
VirtualThreadSupport.class.getClassLoader());
60+
Class.forName("java.lang.Thread").getMethod("ofVirtual");
61+
return true;
62+
} catch (final Throwable t) {
63+
return false;
64+
}
65+
}
66+
67+
/**
68+
* Prefer JDK’s per-task executors when present; otherwise fail.
69+
*/
70+
public static ExecutorService newVirtualThreadPerTaskExecutor(final String namePrefix) {
71+
if (!isAvailable()) {
72+
throw new UnsupportedOperationException("Virtual threads are not available on this runtime");
73+
}
74+
try {
75+
final Class<?> executors = Class.forName("java.util.concurrent.Executors");
76+
try {
77+
final Method m = executors.getMethod("newThreadPerTaskExecutor", ThreadFactory.class);
78+
final ThreadFactory vtFactory = newVirtualThreadFactory(namePrefix);
79+
return (ExecutorService) m.invoke(null, vtFactory);
80+
} catch (final NoSuchMethodException ignore) {
81+
final Method m = executors.getMethod("newVirtualThreadPerTaskExecutor");
82+
return (ExecutorService) m.invoke(null);
83+
}
84+
} catch (final Throwable t) {
85+
throw new UnsupportedOperationException("Failed to initialize virtual thread per-task executor", t);
86+
}
87+
}
88+
89+
public static ThreadFactory newVirtualThreadFactory(final String ignored) {
90+
if (!isAvailable()) {
91+
throw new UnsupportedOperationException("Virtual threads are not available on this runtime");
92+
}
93+
try {
94+
final Class<?> threadClass = Class.forName("java.lang.Thread");
95+
final Object builder = threadClass.getMethod("ofVirtual").invoke(null);
96+
final Class<?> ofVirtualClass = Class.forName("java.lang.Thread$Builder$OfVirtual");
97+
return (ThreadFactory) ofVirtualClass.getMethod("factory").invoke(builder);
98+
} catch (final Throwable t) {
99+
throw new UnsupportedOperationException("Failed to initialize virtual thread factory", t);
100+
}
101+
}
102+
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.LinkedList;
3838
import java.util.List;
3939
import java.util.Map;
40+
import java.util.concurrent.ExecutorService;
4041
import java.util.function.Function;
4142
import java.util.function.UnaryOperator;
4243

@@ -68,6 +69,7 @@
6869
import org.apache.hc.client5.http.impl.DefaultUserTokenHandler;
6970
import org.apache.hc.client5.http.impl.IdleConnectionEvictor;
7071
import org.apache.hc.client5.http.impl.NoopUserTokenHandler;
72+
import org.apache.hc.client5.http.impl.VirtualThreadSupport;
7173
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
7274
import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory;
7375
import org.apache.hc.client5.http.impl.auth.BearerSchemeFactory;
@@ -237,6 +239,14 @@ private ExecInterceptorEntry(
237239

238240
private List<Closeable> closeables;
239241

242+
private boolean useVirtualThreads;
243+
private String virtualThreadNamePrefix = "hc-vt-";
244+
private ExecutorService virtualThreadExecutor;
245+
private boolean shutdownVirtualThreadExecutor = true;
246+
private TimeValue virtualThreadShutdownWait = TimeValue.ofSeconds(2);
247+
248+
private boolean virtualThreadRunHandler;
249+
240250
public static HttpClientBuilder create() {
241251
return new HttpClientBuilder();
242252
}
@@ -805,6 +815,130 @@ public final HttpClientBuilder setProxySelector(final ProxySelector proxySelecto
805815
return this;
806816
}
807817

818+
/**
819+
* Enables or disables execution of the transport layer on virtual threads (JDK&nbsp;21+).
820+
* <p>
821+
* When enabled and no custom executor is supplied via
822+
* {@link #virtualThreadExecutor(java.util.concurrent.ExecutorService) virtualThreadExecutor(..)},
823+
* the builder will create a per-task virtual-thread executor at build time.
824+
* </p>
825+
* <p>
826+
* If virtual threads are not available at runtime and no custom executor is provided,
827+
* {@link #build()} may throw {@link UnsupportedOperationException} depending on configuration.
828+
* </p>
829+
*
830+
* @return this instance.
831+
* @since 5.6
832+
*/
833+
public HttpClientBuilder useVirtualThreads() {
834+
this.useVirtualThreads = true;
835+
return this;
836+
}
837+
838+
/**
839+
* Sets the thread name prefix for virtual threads created by this builder.
840+
* <p>
841+
* This prefix is only applied when the builder creates the virtual-thread executor itself.
842+
* If a custom executor is supplied via {@link #virtualThreadExecutor(java.util.concurrent.ExecutorService)},
843+
* the prefix is ignored.
844+
* </p>
845+
*
846+
* @param prefix the desired name prefix; if {@code null}, {@code "hc-vt-"} is used.
847+
* @return this instance.
848+
* @since 5.6
849+
*/
850+
public HttpClientBuilder virtualThreadNamePrefix(final String prefix) {
851+
this.virtualThreadNamePrefix = prefix != null ? prefix : "hc-vt-";
852+
return this;
853+
}
854+
855+
/**
856+
* Supplies a custom executor to run transport work (typically a per-task virtual-thread executor).
857+
* <p>
858+
* Passing a custom executor automatically enables virtual-thread execution. Ownership semantics are
859+
* controlled by {@code shutdownOnClose}:
860+
* </p>
861+
* <ul>
862+
* <li>{@code true}: the client will shut down the executor during {@code close()}.</li>
863+
* <li>{@code false}: the executor is treated as shared and will <em>not</em> be shut down by the client.</li>
864+
* </ul>
865+
* <p>
866+
* This method does not validate that the supplied executor actually creates virtual threads; callers are
867+
* responsible for providing an appropriate executor.
868+
* </p>
869+
*
870+
* @param exec the executor to use for transport work.
871+
* @param shutdownOnClose whether the client should shut down the executor on close.
872+
* @return this instance.
873+
* @since 5.6
874+
*/
875+
public HttpClientBuilder virtualThreadExecutor(final ExecutorService exec, final boolean shutdownOnClose) {
876+
this.virtualThreadExecutor = exec;
877+
this.shutdownVirtualThreadExecutor = shutdownOnClose;
878+
this.useVirtualThreads = true; // ensure VT path is active
879+
return this;
880+
}
881+
882+
/**
883+
* Supplies a custom executor to run transport work (typically a per-task virtual-thread executor).
884+
* <p>
885+
* Passing a custom executor automatically enables virtual-thread execution and treats the executor as
886+
* <em>shared</em> (it will not be shut down by the client). To change ownership semantics, use
887+
* {@link #virtualThreadExecutor(java.util.concurrent.ExecutorService, boolean)}.
888+
* </p>
889+
* <p>
890+
* This method does not validate that the supplied executor actually creates virtual threads; callers are
891+
* responsible for providing an appropriate executor.
892+
* </p>
893+
*
894+
* @param exec the executor to use for transport work.
895+
* @return this instance.
896+
* @since 5.6
897+
*/
898+
public final HttpClientBuilder virtualThreadExecutor(final ExecutorService exec) {
899+
return virtualThreadExecutor(exec, false);
900+
}
901+
902+
/**
903+
* Configures the maximum time to wait for the virtual-thread executor to terminate during
904+
* a graceful {@link CloseableHttpClient#close() close()} (i.e., {@code CloseMode.GRACEFUL}).
905+
* <p>
906+
* This value is only used when a virtual-thread executor is in use and the client owns it
907+
* (i.e., {@code shutdownVirtualThreadExecutor == true}). For immediate close, the executor
908+
* is shut down without waiting.
909+
* </p>
910+
*
911+
* @param waitTime the time to await executor termination; may be {@code null} to use the default.
912+
* @return this instance.
913+
* @since 5.6
914+
*/
915+
public final HttpClientBuilder virtualThreadShutdownWait(final TimeValue waitTime) {
916+
this.virtualThreadShutdownWait = waitTime;
917+
return this;
918+
}
919+
920+
921+
/**
922+
* Configures the client to run the user-supplied {@link org.apache.hc.core5.http.io.HttpClientResponseHandler}
923+
* <p>
924+
* on a virtual thread as well as the transport layer. By default, the response handler runs on the caller thread.
925+
* <p>
926+
* <p>
927+
* This has an effect only when virtual threads are enabled via {@link #useVirtualThreads()} or
928+
* <p>
929+
* {@link #virtualThreadExecutor(java.util.concurrent.ExecutorService)}.
930+
* </p>
931+
*
932+
* @return this builder
933+
* @since 5.6
934+
*/
935+
public HttpClientBuilder virtualThreadsRunHandler() {
936+
this.virtualThreadRunHandler = true;
937+
return this;
938+
}
939+
940+
941+
808942
/**
809943
* Request exec chain customization and extension.
810944
* <p>
@@ -1121,7 +1255,7 @@ public CloseableHttpClient build() {
11211255
closeablesCopy.add(connManagerCopy);
11221256
}
11231257

1124-
return new InternalHttpClient(
1258+
final CloseableHttpClient base = new InternalHttpClient(
11251259
connManagerCopy,
11261260
requestExecCopy,
11271261
execChain,
@@ -1133,6 +1267,20 @@ public CloseableHttpClient build() {
11331267
contextAdaptor(),
11341268
defaultRequestConfig != null ? defaultRequestConfig : RequestConfig.DEFAULT,
11351269
closeablesCopy);
1270+
1271+
// VT on? wrap, otherwise return base
1272+
if (useVirtualThreads) {
1273+
final ExecutorService vtExecToUse = virtualThreadExecutor != null
1274+
? virtualThreadExecutor
1275+
: (VirtualThreadSupport.isAvailable()
1276+
? VirtualThreadSupport.newVirtualThreadPerTaskExecutor(virtualThreadNamePrefix)
1277+
: null);
1278+
if (vtExecToUse != null) {
1279+
return new VirtualThreadCloseableHttpClient(base, vtExecToUse, shutdownVirtualThreadExecutor, virtualThreadShutdownWait, virtualThreadRunHandler);
1280+
}
1281+
}
1282+
return base;
1283+
11361284
}
11371285

1138-
}
1286+
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClients.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
3131
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
32+
import org.apache.hc.core5.util.TimeValue;
3233

3334
/**
3435
* Factory methods for {@link CloseableHttpClient} instances.
@@ -81,4 +82,75 @@ public static MinimalHttpClient createMinimal(final HttpClientConnectionManager
8182
return new MinimalHttpClient(connManager);
8283
}
8384

85+
/**
86+
* Creates a client with default configuration executing transport on virtual threads (JDK 21+).
87+
* <p>Response handlers run on the caller thread. If virtual threads are unavailable at runtime,
88+
* this method falls back to classic execution (same as {@link #createDefault()}).</p>
89+
* @since 5.6
90+
*/
91+
public static CloseableHttpClient createVirtualThreadDefault() {
92+
return HttpClientBuilder.create()
93+
.useVirtualThreads()
94+
.build();
95+
}
96+
97+
/**
98+
* Same as {@link #createVirtualThreadDefault()} but honors system properties.
99+
* <p>If virtual threads are unavailable at runtime, falls back to classic execution.</p>
100+
* @since 5.6
101+
*/
102+
public static CloseableHttpClient createVirtualThreadSystem() {
103+
return HttpClientBuilder.create()
104+
.useSystemProperties()
105+
.useVirtualThreads()
106+
.build();
107+
}
108+
109+
/**
110+
* Returns a builder preconfigured to execute transport on virtual threads (JDK 21+).
111+
* <p>If virtual threads are unavailable at runtime, the built client falls back to classic execution.</p>
112+
* @since 5.6
113+
*/
114+
public static HttpClientBuilder customVirtualThreads() {
115+
return HttpClientBuilder.create()
116+
.useVirtualThreads();
117+
}
118+
119+
/**
120+
* Returns a builder preconfigured to execute transport on virtual threads with a custom thread name prefix.
121+
* <p>If virtual threads are unavailable at runtime, the built client falls back to classic execution and the
122+
* prefix is ignored.</p>
123+
* @since 5.6
124+
*/
125+
public static HttpClientBuilder customVirtualThreads(final String namePrefix) {
126+
return HttpClientBuilder.create()
127+
.useVirtualThreads()
128+
.virtualThreadNamePrefix(namePrefix);
129+
}
130+
131+
/**
132+
* Creates a virtual-thread client with a custom thread name prefix.
133+
* <p>If virtual threads are unavailable at runtime, falls back to classic execution and the prefix is ignored.</p>
134+
* @since 5.6
135+
*/
136+
public static CloseableHttpClient createVirtualThreadDefault(final String namePrefix) {
137+
return HttpClientBuilder.create()
138+
.useVirtualThreads()
139+
.virtualThreadNamePrefix(namePrefix)
140+
.build();
141+
}
142+
143+
/**
144+
* Creates a virtual-thread client with a custom graceful-shutdown wait.
145+
* @since 5.6
146+
*/
147+
public static CloseableHttpClient createVirtualThreadDefault(final TimeValue shutdownWait) {
148+
return HttpClientBuilder.create()
149+
.useVirtualThreads()
150+
.virtualThreadShutdownWait(shutdownWait)
151+
.build();
152+
}
153+
154+
155+
84156
}

0 commit comments

Comments
 (0)