Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl;

import java.lang.reflect.Method;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadFactory;

import org.apache.hc.core5.annotation.Internal;

/**
* Utilities for working with JDK 21 virtual threads without introducing a hard runtime dependency.
*
* <p>
* <p>
* All methods use reflection to detect and construct virtual-thread components so that the client
* <p>
* remains source- and binary-compatible with earlier JDKs. On runtimes where virtual threads are
* <p>
* unavailable, the helpers either return {@code false} (for detection) or throw
* <p>
* {@link UnsupportedOperationException} (for construction).
* </p>
*
**/
@Internal
public final class VirtualThreadSupport {

private VirtualThreadSupport() {
}

public static boolean isAvailable() {
try {
Class.forName("java.lang.Thread$Builder$OfVirtual", false,
VirtualThreadSupport.class.getClassLoader());
Class.forName("java.lang.Thread").getMethod("ofVirtual");
return true;
} catch (final Throwable t) {
return false;
}
}

/**
* Prefer JDK’s per-task executors when present; otherwise fail.
*/
public static ExecutorService newVirtualThreadPerTaskExecutor(final String namePrefix) {
if (!isAvailable()) {
throw new UnsupportedOperationException("Virtual threads are not available on this runtime");
}
try {
final Class<?> executors = Class.forName("java.util.concurrent.Executors");
try {
final Method m = executors.getMethod("newThreadPerTaskExecutor", ThreadFactory.class);
final ThreadFactory vtFactory = newVirtualThreadFactory(namePrefix);
return (ExecutorService) m.invoke(null, vtFactory);
} catch (final NoSuchMethodException ignore) {
final Method m = executors.getMethod("newVirtualThreadPerTaskExecutor");
return (ExecutorService) m.invoke(null);
}
} catch (final Throwable t) {
throw new UnsupportedOperationException("Failed to initialize virtual thread per-task executor", t);
}
}

public static ThreadFactory newVirtualThreadFactory(final String ignored) {
if (!isAvailable()) {
throw new UnsupportedOperationException("Virtual threads are not available on this runtime");
}
try {
final Class<?> threadClass = Class.forName("java.lang.Thread");
final Object builder = threadClass.getMethod("ofVirtual").invoke(null);
final Class<?> ofVirtualClass = Class.forName("java.lang.Thread$Builder$OfVirtual");
return (ThreadFactory) ofVirtualClass.getMethod("factory").invoke(builder);
} catch (final Throwable t) {
throw new UnsupportedOperationException("Failed to initialize virtual thread factory", t);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ExecutorService;
import java.util.function.Function;
import java.util.function.UnaryOperator;

Expand Down Expand Up @@ -69,6 +70,7 @@
import org.apache.hc.client5.http.impl.DefaultUserTokenHandler;
import org.apache.hc.client5.http.impl.IdleConnectionEvictor;
import org.apache.hc.client5.http.impl.NoopUserTokenHandler;
import org.apache.hc.client5.http.impl.VirtualThreadSupport;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory;
import org.apache.hc.client5.http.impl.auth.BearerSchemeFactory;
Expand Down Expand Up @@ -240,6 +242,14 @@ private ExecInterceptorEntry(

private List<Closeable> closeables;

private boolean useVirtualThreads;
private String virtualThreadNamePrefix = "hc-vt-";
private ExecutorService virtualThreadExecutor;
private boolean shutdownVirtualThreadExecutor = true;
private TimeValue virtualThreadShutdownWait = TimeValue.ofSeconds(2);

private boolean virtualThreadRunHandler;

public static HttpClientBuilder create() {
return new HttpClientBuilder();
}
Expand Down Expand Up @@ -808,6 +818,130 @@ public final HttpClientBuilder setProxySelector(final ProxySelector proxySelecto
return this;
}

/**
* Enables or disables execution of the transport layer on virtual threads (JDK&nbsp;21+).
* <p>
* When enabled and no custom executor is supplied via
* {@link #virtualThreadExecutor(java.util.concurrent.ExecutorService) virtualThreadExecutor(..)},
* the builder will create a per-task virtual-thread executor at build time.
* </p>
* <p>
* If virtual threads are not available at runtime and no custom executor is provided,
* {@link #build()} may throw {@link UnsupportedOperationException} depending on configuration.
* </p>
*
* @return this instance.
* @since 5.6
*/
public HttpClientBuilder useVirtualThreads() {
this.useVirtualThreads = true;
return this;
}

/**
* Sets the thread name prefix for virtual threads created by this builder.
* <p>
* This prefix is only applied when the builder creates the virtual-thread executor itself.
* If a custom executor is supplied via {@link #virtualThreadExecutor(java.util.concurrent.ExecutorService)},
* the prefix is ignored.
* </p>
*
* @param prefix the desired name prefix; if {@code null}, {@code "hc-vt-"} is used.
* @return this instance.
* @since 5.6
*/
public HttpClientBuilder virtualThreadNamePrefix(final String prefix) {
this.virtualThreadNamePrefix = prefix != null ? prefix : "hc-vt-";
return this;
}

/**
* Supplies a custom executor to run transport work (typically a per-task virtual-thread executor).
* <p>
* Passing a custom executor automatically enables virtual-thread execution. Ownership semantics are
* controlled by {@code shutdownOnClose}:
* </p>
* <ul>
* <li>{@code true}: the client will shut down the executor during {@code close()}.</li>
* <li>{@code false}: the executor is treated as shared and will <em>not</em> be shut down by the client.</li>
* </ul>
* <p>
* This method does not validate that the supplied executor actually creates virtual threads; callers are
* responsible for providing an appropriate executor.
* </p>
*
* @param exec the executor to use for transport work.
* @param shutdownOnClose whether the client should shut down the executor on close.
* @return this instance.
* @since 5.6
*/
public HttpClientBuilder virtualThreadExecutor(final ExecutorService exec, final boolean shutdownOnClose) {
this.virtualThreadExecutor = exec;
this.shutdownVirtualThreadExecutor = shutdownOnClose;
this.useVirtualThreads = true; // ensure VT path is active
return this;
}

/**
* Supplies a custom executor to run transport work (typically a per-task virtual-thread executor).
* <p>
* Passing a custom executor automatically enables virtual-thread execution and treats the executor as
* <em>shared</em> (it will not be shut down by the client). To change ownership semantics, use
* {@link #virtualThreadExecutor(java.util.concurrent.ExecutorService, boolean)}.
* </p>
* <p>
* This method does not validate that the supplied executor actually creates virtual threads; callers are
* responsible for providing an appropriate executor.
* </p>
*
* @param exec the executor to use for transport work.
* @return this instance.
* @since 5.6
*/
public final HttpClientBuilder virtualThreadExecutor(final ExecutorService exec) {
return virtualThreadExecutor(exec, false);
}

/**
* Configures the maximum time to wait for the virtual-thread executor to terminate during
* a graceful {@link CloseableHttpClient#close() close()} (i.e., {@code CloseMode.GRACEFUL}).
* <p>
* This value is only used when a virtual-thread executor is in use and the client owns it
* (i.e., {@code shutdownVirtualThreadExecutor == true}). For immediate close, the executor
* is shut down without waiting.
* </p>
*
* @param waitTime the time to await executor termination; may be {@code null} to use the default.
* @return this instance.
* @since 5.6
*/
public final HttpClientBuilder virtualThreadShutdownWait(final TimeValue waitTime) {
this.virtualThreadShutdownWait = waitTime;
return this;
}


/**
* Configures the client to run the user-supplied {@link org.apache.hc.core5.http.io.HttpClientResponseHandler}
* <p>
* on a virtual thread as well as the transport layer. By default, the response handler runs on the caller thread.
* <p>
* <p>
* This has an effect only when virtual threads are enabled via {@link #useVirtualThreads()} or
* <p>
* {@link #virtualThreadExecutor(java.util.concurrent.ExecutorService)}.
* </p>
*
* @return this builder
* @since 5.6
*/
public HttpClientBuilder virtualThreadsRunHandler() {
this.virtualThreadRunHandler = true;
return this;
}



/**
* Request exec chain customization and extension.
* <p>
Expand Down Expand Up @@ -1126,7 +1260,7 @@ public CloseableHttpClient build() {
closeablesCopy.add(connManagerCopy);
}

return new InternalHttpClient(
final CloseableHttpClient base = new InternalHttpClient(
connManagerCopy,
requestExecCopy,
execChain,
Expand All @@ -1138,6 +1272,20 @@ public CloseableHttpClient build() {
contextAdaptor(),
defaultRequestConfig != null ? defaultRequestConfig : RequestConfig.DEFAULT,
closeablesCopy);

// VT on? wrap, otherwise return base
if (useVirtualThreads) {
final ExecutorService vtExecToUse = virtualThreadExecutor != null
? virtualThreadExecutor
: (VirtualThreadSupport.isAvailable()
? VirtualThreadSupport.newVirtualThreadPerTaskExecutor(virtualThreadNamePrefix)
: null);
if (vtExecToUse != null) {
return new VirtualThreadCloseableHttpClient(base, vtExecToUse, shutdownVirtualThreadExecutor, virtualThreadShutdownWait, virtualThreadRunHandler);
}
}
return base;

}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.core5.util.TimeValue;

/**
* Factory methods for {@link CloseableHttpClient} instances.
Expand Down Expand Up @@ -81,4 +82,75 @@ public static MinimalHttpClient createMinimal(final HttpClientConnectionManager
return new MinimalHttpClient(connManager);
}

/**
* Creates a client with default configuration executing transport on virtual threads (JDK 21+).
* <p>Response handlers run on the caller thread. If virtual threads are unavailable at runtime,
* this method falls back to classic execution (same as {@link #createDefault()}).</p>
* @since 5.6
*/
public static CloseableHttpClient createVirtualThreadDefault() {
return HttpClientBuilder.create()
.useVirtualThreads()
.build();
}

/**
* Same as {@link #createVirtualThreadDefault()} but honors system properties.
* <p>If virtual threads are unavailable at runtime, falls back to classic execution.</p>
* @since 5.6
*/
public static CloseableHttpClient createVirtualThreadSystem() {
return HttpClientBuilder.create()
.useSystemProperties()
.useVirtualThreads()
.build();
}

/**
* Returns a builder preconfigured to execute transport on virtual threads (JDK 21+).
* <p>If virtual threads are unavailable at runtime, the built client falls back to classic execution.</p>
* @since 5.6
*/
public static HttpClientBuilder customVirtualThreads() {
return HttpClientBuilder.create()
.useVirtualThreads();
}

/**
* Returns a builder preconfigured to execute transport on virtual threads with a custom thread name prefix.
* <p>If virtual threads are unavailable at runtime, the built client falls back to classic execution and the
* prefix is ignored.</p>
* @since 5.6
*/
public static HttpClientBuilder customVirtualThreads(final String namePrefix) {
return HttpClientBuilder.create()
.useVirtualThreads()
.virtualThreadNamePrefix(namePrefix);
}

/**
* Creates a virtual-thread client with a custom thread name prefix.
* <p>If virtual threads are unavailable at runtime, falls back to classic execution and the prefix is ignored.</p>
* @since 5.6
*/
public static CloseableHttpClient createVirtualThreadDefault(final String namePrefix) {
return HttpClientBuilder.create()
.useVirtualThreads()
.virtualThreadNamePrefix(namePrefix)
.build();
}

/**
* Creates a virtual-thread client with a custom graceful-shutdown wait.
* @since 5.6
*/
public static CloseableHttpClient createVirtualThreadDefault(final TimeValue shutdownWait) {
return HttpClientBuilder.create()
.useVirtualThreads()
.virtualThreadShutdownWait(shutdownWait)
.build();
}



}
Loading
Loading