diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManager.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManager.java index 8d509209a5..25b4fb1d68 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManager.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManager.java @@ -820,5 +820,60 @@ public boolean isClosed() { return this.closed.get(); } + /** + * Performs a warm-up of connections to the specified target host synchronously. + * This method initializes a number of connections to the target host as defined + * by the maximum connections per route configuration and ensures they are ready + * for use before actual requests are made. + * + * @param host the target {@link HttpHost} for which connections are to be warmed up. + * @param timeout the timeout for each lease operation during the warm-up process. + * @since 5.5 + */ + public void warmUp(final HttpHost host, final Timeout timeout) { + final int count = pool.getMaxPerRoute(new HttpRoute(host)); + if (count <= 0) { + if (LOG.isWarnEnabled()) { + LOG.warn("No connections to warm up for route: {}", host); + } + return; + } + + int successCount = 0; + int failureCount = 0; + + for (int i = 0; i < count; i++) { + try { + final PoolEntry entry = pool.lease( + new HttpRoute(host), + null, + timeout, + null + ).get(timeout.getDuration(), timeout.getTimeUnit()); + + // Release the leased connection + pool.release(entry, true); + successCount++; + + if (LOG.isDebugEnabled()) { + LOG.debug("Warm-up connection leased and released: {}", entry.getRoute()); + } + } catch (final Exception ex) { + failureCount++; + if (LOG.isDebugEnabled()) { + LOG.debug("Warm-up connection failed: {}", ex.getMessage()); + } + } + } + + if (LOG.isInfoEnabled()) { + LOG.info("Warm-up completed. Successes: {}, Failures: {}", successCount, failureCount); + } + + if (failureCount > 0) { + throw new IllegalStateException("Warm-up failed for some connections. Successes: " + + successCount + ", Failures: " + failureCount); + } + } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java index cd5473f2f1..cd9c69f67f 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java @@ -33,6 +33,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.apache.hc.client5.http.DnsResolver; @@ -791,4 +792,95 @@ public boolean isClosed() { return this.closed.get(); } + + /** + * Performs a warm-up of connections to the specified target host asynchronously. + * This method initializes a number of connections to the target host as defined + * by the maximum connections per route configuration and ensures they are ready + * for use before actual requests are made. + * + *

The warm-up process helps in reducing the connection establishment time + * for subsequent requests by pre-establishing and validating connections.

+ * + * @param host the target {@link HttpHost} for which connections are to be warmed up. + * @param timeout the timeout for each lease operation during the warm-up process. + * @param callback the {@link FutureCallback} to notify upon the completion of the warm-up process. + * The callback is triggered when all connections are warmed up, or if there are + * failures during the process. + * @since 5.5 + */ + public void warmUp(final HttpHost host, final Timeout timeout, final FutureCallback callback) { + final int count = pool.getMaxPerRoute(new HttpRoute(host)); + + if (count <= 0) { + if (LOG.isWarnEnabled()) { + LOG.warn("No connections to warm up for route: {}", host); + } + if (callback != null) { + callback.completed(null); + } + return; + } + + final AtomicInteger completedCount = new AtomicInteger(0); + final AtomicInteger successCount = new AtomicInteger(0); + final AtomicInteger failureCount = new AtomicInteger(0); + + for (int i = 0; i < count; i++) { + pool.lease(new HttpRoute(host), null, timeout, new FutureCallback>() { + @Override + public void completed(final PoolEntry result) { + try { + pool.release(result, true); + successCount.incrementAndGet(); + if (LOG.isDebugEnabled()) { + LOG.debug("Warm-up connection leased and released: {}", result.getRoute()); + } + } catch (final Exception ex) { + failureCount.incrementAndGet(); + if (LOG.isDebugEnabled()) { + LOG.debug("Warm-up connection release failed: {}", ex.getMessage()); + } + } finally { + checkCompletion(); + } + } + + @Override + public void failed(final Exception ex) { + failureCount.incrementAndGet(); + if (LOG.isDebugEnabled()) { + LOG.debug("Warm-up connection failed: {}", ex.getMessage()); + } + checkCompletion(); + } + + @Override + public void cancelled() { + failureCount.incrementAndGet(); + if (LOG.isDebugEnabled()) { + LOG.debug("Warm-up connection cancelled."); + } + checkCompletion(); + } + + private void checkCompletion() { + if (completedCount.incrementAndGet() == count) { + if (callback != null) { + if (failureCount.get() > 0) { + callback.failed(new Exception("Warm-up failed for some connections. Successes: " + + successCount.get() + ", Failures: " + failureCount.get())); + } else { + callback.completed(null); + } + } + if (LOG.isDebugEnabled()) { + LOG.debug("Warm-up completed. Successes: {}, Failures: {}", successCount.get(), failureCount.get()); + } + } + } + }); + } + } + } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncWarmUpTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncWarmUpTest.java new file mode 100644 index 0000000000..2e55b586f6 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncWarmUpTest.java @@ -0,0 +1,157 @@ +/* + * ==================================================================== + * 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 + * . + * + */ + + +package org.apache.hc.client5.http.examples; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.async.methods.SimpleRequestProducer; +import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.message.StatusLine; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.util.Timeout; + + +/** + * Demonstrates the usage of the {@link PoolingAsyncClientConnectionManager} to perform a warm-up + * of connections asynchronously and execute an HTTP request using the Apache HttpClient 5 async API. + * + *

The warm-up process initializes a specified number of connections to a target host asynchronously, + * ensuring they are ready for use before actual requests are made. The example then performs an + * HTTP GET request to a target server and logs the response details.

+ * + *

Key steps include:

+ *
    + *
  • Creating a {@link PoolingAsyncClientConnectionManager} instance with TLS configuration.
  • + *
  • Calling {@link PoolingAsyncClientConnectionManager#warmUp(HttpHost, Timeout, FutureCallback)} + * to prepare the connection pool for the specified target host.
  • + *
  • Waiting for the warm-up to complete using a {@link CompletableFuture}.
  • + *
  • Executing an HTTP GET request using {@link CloseableHttpAsyncClient}.
  • + *
  • Handling the HTTP response and logging protocol, SSL details, and response body.
  • + *
+ * + *

Usage:

+ *
+ * java AsyncWarmUpTest
+ * 
+ * + *

Dependencies: Ensure the required Apache HttpClient libraries are on the classpath.

+ */ + +public class AsyncWarmUpTest { + + public static void main(final String[] args) throws Exception { + final PoolingAsyncClientConnectionManager cm = PoolingAsyncClientConnectionManagerBuilder.create() + .build(); + + // Perform the warm-up directly using a Future + final CompletableFuture warmUpFuture = new CompletableFuture<>(); + cm.warmUp(new HttpHost("http", "httpbin.org", 80), Timeout.ofSeconds(10), new FutureCallback() { + @Override + public void completed(final Void result) { + warmUpFuture.complete(null); + } + + @Override + public void failed(final Exception ex) { + warmUpFuture.completeExceptionally(ex); + } + + @Override + public void cancelled() { + warmUpFuture.cancel(true); + } + }); + + // Wait for the warm-up to complete + try { + warmUpFuture.get(10, TimeUnit.SECONDS); + System.out.println("Warm-up completed successfully."); + } catch (final Exception ex) { + System.err.println("Warm-up failed: " + ex.getMessage()); + return; // Exit if warm-up fails + } + + try (final CloseableHttpAsyncClient client = HttpAsyncClients.custom() + .setConnectionManager(cm) + .build()) { + + client.start(); + + final HttpHost target = new HttpHost("https", "httpbin.org"); + final HttpClientContext clientContext = HttpClientContext.create(); + + final SimpleHttpRequest request = SimpleRequestBuilder.get() + .setHttpHost(target) + .setPath("/") + .build(); + + System.out.println("Executing request " + request); + final Future future = client.execute( + SimpleRequestProducer.create(request), + SimpleResponseConsumer.create(), + clientContext, + new FutureCallback() { + + @Override + public void completed(final SimpleHttpResponse response) { + System.out.println(request + "->" + new StatusLine(response)); + System.out.println("HTTP protocol " + clientContext.getProtocolVersion()); + System.out.println(response.getBody()); + } + + @Override + public void failed(final Exception ex) { + System.out.println(request + "->" + ex); + } + + @Override + public void cancelled() { + System.out.println(request + " cancelled"); + } + + }); + future.get(); + + System.out.println("Shutting down"); + client.close(CloseMode.GRACEFUL); + } + } +} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/WarmUpTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/WarmUpTest.java new file mode 100644 index 0000000000..65c55b6331 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/WarmUpTest.java @@ -0,0 +1,105 @@ +/* + * ==================================================================== + * 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 + * . + * + */ + +package org.apache.hc.client5.http.examples; + +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.message.StatusLine; +import org.apache.hc.core5.util.Timeout; + + +/** + * Demonstrates the usage of the {@link PoolingHttpClientConnectionManager} to perform a warm-up + * of connections synchronously and execute an HTTP request using the Apache HttpClient 5 sync API. + * + *

The warm-up process initializes a specified number of connections to a target host, + * ensuring they are ready for use before actual requests are made. The example then performs an + * HTTP GET request to a target server and logs the response details.

+ * + *

Key steps include:

+ *
    + *
  • Creating a {@link PoolingHttpClientConnectionManager} instance with TLS configuration.
  • + *
  • Calling {@link PoolingHttpClientConnectionManager#warmUp(HttpHost, Timeout)} to prepare + * the connection pool for the specified target host.
  • + *
  • Executing an HTTP GET request using {@link CloseableHttpClient}.
  • + *
  • Handling the HTTP response and logging protocol, SSL details, and response body.
  • + *
+ * + *

Usage:

+ *
+ * java WarmUpTest
+ * 
+ * + *

Dependencies: Ensure the required Apache HttpClient libraries are on the classpath.

+ * + * @since 5.5 + */ +public class WarmUpTest { + + public static void main(final String[] args) throws Exception { + + // Target host for warm-up and execution + final HttpHost targetHost = new HttpHost("http", "httpbin.org", 80); + final Timeout timeout = Timeout.ofSeconds(10); + + // Create a connection manager with warm-up support + final PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(10); + connectionManager.setDefaultMaxPerRoute(5); + + // Warm up connections to the target host + System.out.println("Warming up connections..."); + connectionManager.warmUp(targetHost, timeout); + System.out.println("Warm-up completed successfully."); + + // Create an HttpClient using the warmed-up connection manager + try (final CloseableHttpClient httpclient = HttpClients.custom() + .setConnectionManager(connectionManager) + .build()) { + + // Define the HTTP GET request + final HttpGet httpget = new HttpGet("http://httpbin.org/get"); + System.out.println("Executing request " + httpget.getMethod() + " " + httpget.getUri()); + + // Execute the request in a loop + for (int i = 0; i < 3; i++) { + httpclient.execute(httpget, response -> { + System.out.println("----------------------------------------"); + System.out.println(httpget + " -> " + new StatusLine(response)); + final String content = EntityUtils.toString(response.getEntity()); + System.out.println("Response content: " + content); + return null; + }); + } + } + } +} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestPoolingHttpClientConnectionManager.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestPoolingHttpClientConnectionManager.java index 72f3c7fe6e..32341c01aa 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestPoolingHttpClientConnectionManager.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestPoolingHttpClientConnectionManager.java @@ -406,4 +406,46 @@ void testConcurrentShutdown() throws InterruptedException { } + @Test + void testWarmUp() throws Exception { + final HttpHost target = new HttpHost("localhost", 80); + final HttpRoute route = new HttpRoute(target); + + // Create mock PoolEntry + final PoolEntry entry1 = + new PoolEntry<>(route, TimeValue.NEG_ONE_MILLISECOND); + entry1.assignConnection(conn); + + final PoolEntry entry2 = + new PoolEntry<>(route, TimeValue.NEG_ONE_MILLISECOND); + entry2.assignConnection(conn); + + // Mock the pool to return a max-per-route of 2 + Mockito.when(pool.getMaxPerRoute(Mockito.eq(route))).thenReturn(2); + + // Mock pool.lease() behavior to simulate leasing and returning futures + Mockito.when(pool.lease( + Mockito.eq(route), + Mockito.isNull(), + Mockito.any(), + Mockito.isNull())) + .thenReturn(future); + + // Simulate `Future.get()` to return the mocked PoolEntry objects + Mockito.when(future.get(Mockito.anyLong(), Mockito.any())).thenReturn(entry1, entry2); + + // Mock pool.release behavior + Mockito.doNothing().when(pool).release(Mockito.any(), Mockito.eq(true)); + + // Call the warm-up method synchronously + mgr.warmUp(target, Timeout.ofSeconds(10)); + + // Verify interactions with the pool + Mockito.verify(pool, Mockito.times(2)).lease( + Mockito.eq(route), + Mockito.isNull(), + Mockito.any(), + Mockito.isNull()); + Mockito.verify(pool, Mockito.times(2)).release(Mockito.any(), Mockito.eq(true)); + } } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/TestPoolingAsyncClientConnectionManager.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/TestPoolingAsyncClientConnectionManager.java new file mode 100644 index 0000000000..4efb0fed40 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/TestPoolingAsyncClientConnectionManager.java @@ -0,0 +1,82 @@ +/* + * ==================================================================== + * 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 + * . + * + */ + +package org.apache.hc.client5.http.impl.nio; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.util.Timeout; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TestPoolingAsyncClientConnectionManager { + + private PoolingAsyncClientConnectionManager connectionManager; + + @BeforeEach + void setUp() { + connectionManager = PoolingAsyncClientConnectionManagerBuilder.create() + .setDefaultConnectionConfig(ConnectionConfig.custom() + .setSocketTimeout(Timeout.ofSeconds(10)) + .build()) + .build(); + } + + @Test + void testWarmUpConnections() { + final HttpHost targetHost = new HttpHost("http", "httpbin.org", 80); + final CompletableFuture warmUpFuture = new CompletableFuture<>(); + + connectionManager.warmUp(targetHost, Timeout.ofSeconds(10), new FutureCallback() { + @Override + public void completed(final Object o) { + warmUpFuture.complete(null); + } + + @Override + public void failed(final Exception ex) { + warmUpFuture.completeExceptionally(ex); + } + + @Override + public void cancelled() { + warmUpFuture.cancel(true); + } + }); + + // Assert that the warm-up completes within a reasonable time + assertDoesNotThrow(() -> warmUpFuture.get(15, TimeUnit.SECONDS), "Warm-up should complete without exceptions."); + assertTrue(warmUpFuture.isDone() && !warmUpFuture.isCompletedExceptionally(), "Warm-up should complete successfully."); + } +} \ No newline at end of file