tlsConfigResolver;
+ private boolean messageMultiplexing;
public static PoolingAsyncClientConnectionManagerBuilder create() {
return new PoolingAsyncClientConnectionManagerBuilder();
@@ -254,6 +256,24 @@ public class PoolingAsyncClientConnectionManagerBuilder {
return this;
}
+ /**
+ * Use experimental connections pool implementation that acts as a caching facade
+ * in front of a standard connection pool and shares already leased connections
+ * to multiplex message exchanges over active HTTP/2 connections.
+ *
+ * Please note this flag has no effect on HTTP/1.1 and HTTP/1.0 connections.
+ *
+ * This feature is considered experimenal
+ *
+ * @since 5.5
+ * @return this instance.
+ */
+ @Experimental
+ public final PoolingAsyncClientConnectionManagerBuilder setMessageMultiplexing(final boolean messageMultiplexing) {
+ this.messageMultiplexing = messageMultiplexing;
+ return this;
+ }
+
@Internal
protected AsyncClientConnectionOperator createConnectionOperator(
final TlsStrategy tlsStrategy,
@@ -290,7 +310,8 @@ public class PoolingAsyncClientConnectionManagerBuilder {
createConnectionOperator(tlsStrategyCopy, schemePortResolver, dnsResolver),
poolConcurrencyPolicy,
poolReusePolicy,
- null);
+ null,
+ messageMultiplexing);
poolingmgr.setConnectionConfigResolver(connectionConfigResolver);
poolingmgr.setTlsConfigResolver(tlsConfigResolver);
if (maxConnTotal > 0) {
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientH2Multiplexing.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientH2Multiplexing.java
index 442dc669f..56a9e8018 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientH2Multiplexing.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientH2Multiplexing.java
@@ -28,7 +28,6 @@ package org.apache.hc.client5.http.examples;
import java.util.concurrent.CountDownLatch;
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;
@@ -36,84 +35,124 @@ 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.config.TlsConfig;
+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.async.MinimalHttpAsyncClient;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.config.Http1Config;
import org.apache.hc.core5.http.message.StatusLine;
-import org.apache.hc.core5.http.nio.AsyncClientEndpoint;
import org.apache.hc.core5.http2.HttpVersionPolicy;
-import org.apache.hc.core5.http2.config.H2Config;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.reactor.IOReactorConfig;
+import org.apache.hc.core5.util.Timeout;
/**
- * This example demonstrates concurrent (multiplexed) execution of multiple
- * HTTP/2 message exchanges.
+ * Example of asynchronous HTTP/1.1 request execution with message exchange multiplexing
+ * over HTTP/2 connections.
*/
public class AsyncClientH2Multiplexing {
public static void main(final String[] args) throws Exception {
- final MinimalHttpAsyncClient client = HttpAsyncClients.createMinimal(
- H2Config.DEFAULT,
- Http1Config.DEFAULT,
- IOReactorConfig.DEFAULT,
- PoolingAsyncClientConnectionManagerBuilder.create()
- .setDefaultTlsConfig(TlsConfig.custom()
- .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2)
- .build())
- .build());
+ final IOReactorConfig ioReactorConfig = IOReactorConfig.custom()
+ .setSoTimeout(Timeout.ofSeconds(5))
+ .build();
+
+ final PoolingAsyncClientConnectionManager connectionManager = PoolingAsyncClientConnectionManagerBuilder.create()
+ .setDefaultTlsConfig(TlsConfig.custom()
+ .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2)
+ .build())
+ .setMessageMultiplexing(true)
+ .build();
+
+ final CloseableHttpAsyncClient client = HttpAsyncClients.custom()
+ .setConnectionManager(connectionManager)
+ .setIOReactorConfig(ioReactorConfig)
+ .build();
client.start();
final HttpHost target = new HttpHost("https", "nghttp2.org");
- final Future leaseFuture = client.lease(target, null);
- final AsyncClientEndpoint endpoint = leaseFuture.get(30, TimeUnit.SECONDS);
- try {
- final String[] requestUris = new String[] {"/httpbin/ip", "/httpbin/user-agent", "/httpbin/headers"};
- final CountDownLatch latch = new CountDownLatch(requestUris.length);
- for (final String requestUri: requestUris) {
- final SimpleHttpRequest request = SimpleRequestBuilder.get()
- .setHttpHost(target)
- .setPath(requestUri)
- .build();
+ final SimpleHttpRequest warmup = SimpleRequestBuilder.get()
+ .setHttpHost(target)
+ .setPath("/httpbin")
+ .build();
- System.out.println("Executing request " + request);
- endpoint.execute(
- SimpleRequestProducer.create(request),
- SimpleResponseConsumer.create(),
- new FutureCallback() {
+ // Make sure there is an open HTTP/2 connection in the pool
+ System.out.println("Executing warm-up request " + warmup);
+ final Future future = client.execute(
+ SimpleRequestProducer.create(warmup),
+ SimpleResponseConsumer.create(),
+ new FutureCallback() {
- @Override
- public void completed(final SimpleHttpResponse response) {
- latch.countDown();
- System.out.println(request + "->" + new StatusLine(response));
- System.out.println(response.getBody());
- }
+ @Override
+ public void completed(final SimpleHttpResponse response) {
+ System.out.println(warmup + "->" + new StatusLine(response));
+ System.out.println(response.getBody());
+ }
- @Override
- public void failed(final Exception ex) {
- latch.countDown();
- System.out.println(request + "->" + ex);
- }
+ @Override
+ public void failed(final Exception ex) {
+ System.out.println(warmup + "->" + ex);
+ }
- @Override
- public void cancelled() {
- latch.countDown();
- System.out.println(request + " cancelled");
- }
+ @Override
+ public void cancelled() {
+ System.out.println(warmup + " cancelled");
+ }
- });
- }
- latch.await();
- } finally {
- endpoint.releaseAndReuse();
+ });
+ future.get();
+
+ Thread.sleep(1000);
+
+ System.out.println("Connection pool stats: " + connectionManager.getTotalStats());
+
+ // Execute multiple requests over the HTTP/2 connection from the pool
+ final String[] requestUris = new String[]{"/httpbin", "/httpbin/ip", "/httpbin/user-agent", "/httpbin/headers"};
+ final CountDownLatch countDownLatch = new CountDownLatch(requestUris.length);
+
+ for (final String requestUri : requestUris) {
+ final SimpleHttpRequest request = SimpleRequestBuilder.get()
+ .setHttpHost(target)
+ .setPath(requestUri)
+ .build();
+
+ System.out.println("Executing request " + request);
+ client.execute(
+ SimpleRequestProducer.create(request),
+ SimpleResponseConsumer.create(),
+ new FutureCallback() {
+
+ @Override
+ public void completed(final SimpleHttpResponse response) {
+ countDownLatch.countDown();
+ System.out.println(request + "->" + new StatusLine(response));
+ System.out.println(response.getBody());
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ countDownLatch.countDown();
+ System.out.println(request + "->" + ex);
+ }
+
+ @Override
+ public void cancelled() {
+ countDownLatch.countDown();
+ System.out.println(request + " cancelled");
+ }
+
+ });
}
+ countDownLatch.await();
+
+ // There still should be a single connection in the pool
+ System.out.println("Connection pool stats: " + connectionManager.getTotalStats());
+
System.out.println("Shutting down");
client.close(CloseMode.GRACEFUL);
}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingConnPoolTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingConnPoolTest.java
new file mode 100644
index 000000000..26fa2e1cb
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingConnPoolTest.java
@@ -0,0 +1,387 @@
+/*
+ * ====================================================================
+ * 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 java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.hc.core5.concurrent.BasicFuture;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpConnection;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.pool.ManagedConnPool;
+import org.apache.hc.core5.pool.PoolEntry;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+public class H2SharingConnPoolTest {
+
+ static final String DEFAULT_ROUTE = "DEFAULT_ROUTE";
+
+ @Mock
+ ManagedConnPool connPool;
+ @Mock
+ FutureCallback> callback;
+ @Mock
+ HttpConnection connection;
+ H2SharingConnPool h2SharingPool;
+
+ @BeforeEach
+ void setup() {
+ MockitoAnnotations.openMocks(this);
+ h2SharingPool = new H2SharingConnPool<>(connPool);
+ }
+
+ @Test
+ void testLeaseFutureReturned() throws Exception {
+ Mockito.when(connPool.lease(
+ Mockito.eq(DEFAULT_ROUTE),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any())).thenReturn(new BasicFuture<>(null));
+
+ final Future> result = h2SharingPool.lease(DEFAULT_ROUTE, null, Timeout.ONE_MILLISECOND, callback);
+ Assertions.assertNotNull(result);
+ Assertions.assertFalse(result.isDone());
+
+ Mockito.verify(connPool).lease(
+ Mockito.eq(DEFAULT_ROUTE),
+ Mockito.eq(null),
+ Mockito.eq(Timeout.ONE_MILLISECOND),
+ Mockito.any());
+ Mockito.verify(callback, Mockito.never()).completed(
+ Mockito.any());
+ }
+
+ @Test
+ void testLeaseExistingConnectionReturned() throws Exception {
+ final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
+ final H2SharingConnPool.PerRoutePool routePool = h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
+ routePool.track(poolEntry);
+
+ final Future> future = h2SharingPool.lease(DEFAULT_ROUTE, null, Timeout.ONE_MILLISECOND, callback);
+ Assertions.assertNotNull(future);
+ Assertions.assertSame(poolEntry, future.get());
+
+ Mockito.verify(connPool, Mockito.never()).lease(
+ Mockito.eq(DEFAULT_ROUTE),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any());
+ Mockito.verify(callback).completed(
+ Mockito.same(poolEntry));
+ }
+
+ @Test
+ void testLeaseWithStateCacheBypassed() throws Exception {
+ final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
+ final H2SharingConnPool.PerRoutePool routePool = h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
+ routePool.track(poolEntry);
+
+ Mockito.when(connPool.lease(
+ Mockito.eq(DEFAULT_ROUTE),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any())).thenReturn(new BasicFuture<>(null));
+
+ final Future> result = h2SharingPool.lease(DEFAULT_ROUTE, "stuff", Timeout.ONE_MILLISECOND, callback);
+ Assertions.assertNotNull(result);
+ Assertions.assertFalse(result.isDone());
+
+ Mockito.verify(connPool).lease(
+ Mockito.eq(DEFAULT_ROUTE),
+ Mockito.eq("stuff"),
+ Mockito.eq(Timeout.ONE_MILLISECOND),
+ Mockito.any());
+ Mockito.verify(callback, Mockito.never()).completed(
+ Mockito.any());
+ }
+
+ @Test
+ void testLeaseNewConnectionReturnedAndCached() throws Exception {
+ final AtomicReference>> futureRef = new AtomicReference<>();
+ Mockito.when(connPool.lease(
+ Mockito.eq(DEFAULT_ROUTE),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any())).thenAnswer(invocationOnMock -> {
+ final BasicFuture> future = new BasicFuture<>(invocationOnMock.getArgument(3));
+ futureRef.set(future);
+ return future;
+ });
+
+ final Future> result = h2SharingPool.lease(DEFAULT_ROUTE, null, Timeout.ONE_MILLISECOND, callback);
+ final BasicFuture> future = futureRef.get();
+ Assertions.assertNotNull(future);
+
+ final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
+ poolEntry.assignConnection(connection);
+ Mockito.when(connection.getProtocolVersion()).thenReturn(HttpVersion.HTTP_2);
+ future.completed(poolEntry);
+
+ Assertions.assertTrue(result.isDone());
+
+ Mockito.verify(connPool).lease(
+ Mockito.eq(DEFAULT_ROUTE),
+ Mockito.eq(null),
+ Mockito.eq(Timeout.ONE_MILLISECOND),
+ Mockito.any());
+ Mockito.verify(callback).completed(
+ Mockito.any());
+
+ final H2SharingConnPool.PerRoutePool routePool = h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
+ Assertions.assertEquals(1, routePool.getCount(poolEntry));
+ }
+
+ @Test
+ void testLeaseNewConnectionReturnedAndNotCached() throws Exception {
+ final AtomicReference>> futureRef = new AtomicReference<>();
+ Mockito.when(connPool.lease(
+ Mockito.eq(DEFAULT_ROUTE),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any())).thenAnswer(invocationOnMock -> {
+ final BasicFuture> future = new BasicFuture<>(invocationOnMock.getArgument(3));
+ futureRef.set(future);
+ return future;
+ });
+
+ final Future> result = h2SharingPool.lease(DEFAULT_ROUTE, null, Timeout.ONE_MILLISECOND, callback);
+ final BasicFuture> future = futureRef.get();
+ Assertions.assertNotNull(future);
+
+ final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
+ poolEntry.assignConnection(connection);
+ Mockito.when(connection.getProtocolVersion()).thenReturn(HttpVersion.HTTP_1_1);
+ future.completed(poolEntry);
+
+ Assertions.assertTrue(result.isDone());
+
+ Mockito.verify(connPool).lease(
+ Mockito.eq(DEFAULT_ROUTE),
+ Mockito.eq(null),
+ Mockito.eq(Timeout.ONE_MILLISECOND),
+ Mockito.any());
+ Mockito.verify(callback).completed(
+ Mockito.any());
+
+ final H2SharingConnPool.PerRoutePool routePool = h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
+ Assertions.assertEquals(0, routePool.getCount(poolEntry));
+ }
+
+ @Test
+ void testLeaseNoConnection() throws Exception {
+ final AtomicReference>> futureRef = new AtomicReference<>();
+ Mockito.when(connPool.lease(
+ Mockito.eq(DEFAULT_ROUTE),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any())).thenAnswer(invocationOnMock -> {
+ final BasicFuture> future = new BasicFuture<>(invocationOnMock.getArgument(3));
+ futureRef.set(future);
+ return future;
+ });
+
+ final Future> result = h2SharingPool.lease(DEFAULT_ROUTE, null, Timeout.ONE_MILLISECOND, callback);
+ final BasicFuture> future = futureRef.get();
+ Assertions.assertNotNull(future);
+
+ final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
+ poolEntry.discardConnection(CloseMode.IMMEDIATE);
+ future.completed(poolEntry);
+
+ Assertions.assertTrue(result.isDone());
+
+ Mockito.verify(connPool).lease(
+ Mockito.eq(DEFAULT_ROUTE),
+ Mockito.eq(null),
+ Mockito.eq(Timeout.ONE_MILLISECOND),
+ Mockito.any());
+ Mockito.verify(callback).completed(
+ Mockito.any());
+
+ final H2SharingConnPool.PerRoutePool routePool = h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
+ Assertions.assertEquals(0, routePool.getCount(poolEntry));
+ }
+
+ @Test
+ void testLeaseWithStateNewConnectionReturnedAndNotCached() throws Exception {
+ final AtomicReference>> futureRef = new AtomicReference<>();
+ Mockito.when(connPool.lease(
+ Mockito.eq(DEFAULT_ROUTE),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any())).thenAnswer(invocationOnMock -> {
+ final BasicFuture> future = new BasicFuture<>(invocationOnMock.getArgument(3));
+ futureRef.set(future);
+ return future;
+ });
+
+ final Future> result = h2SharingPool.lease(DEFAULT_ROUTE, "stuff", Timeout.ONE_MILLISECOND, callback);
+ final BasicFuture> future = futureRef.get();
+ Assertions.assertNotNull(future);
+
+ final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
+ poolEntry.assignConnection(connection);
+ Mockito.when(connection.getProtocolVersion()).thenReturn(HttpVersion.HTTP_2);
+ future.completed(poolEntry);
+
+ Assertions.assertTrue(result.isDone());
+
+ Mockito.verify(connPool).lease(
+ Mockito.eq(DEFAULT_ROUTE),
+ Mockito.eq("stuff"),
+ Mockito.eq(Timeout.ONE_MILLISECOND),
+ Mockito.any());
+ Mockito.verify(callback).completed(
+ Mockito.any());
+
+ final H2SharingConnPool.PerRoutePool routePool = h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
+ Assertions.assertEquals(0, routePool.getCount(poolEntry));
+ }
+
+ @Test
+ void testReleaseReusableNoCacheReturnedToPool() throws Exception {
+ final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
+ poolEntry.assignConnection(connection);
+ Mockito.when(connection.isOpen()).thenReturn(true);
+
+ h2SharingPool.release(poolEntry, true);
+
+ Mockito.verify(connPool).release(
+ Mockito.same(poolEntry),
+ Mockito.eq(true));
+ }
+
+ @Test
+ void testReleaseReusableNotInCacheReturnedToPool() throws Exception {
+ final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
+ poolEntry.assignConnection(connection);
+ Mockito.when(connection.isOpen()).thenReturn(true);
+ final H2SharingConnPool.PerRoutePool routePool = h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
+ routePool.track(poolEntry);
+
+ h2SharingPool.release(poolEntry, true);
+
+ Mockito.verify(connPool).release(
+ Mockito.same(poolEntry),
+ Mockito.eq(true));
+ }
+
+ @Test
+ void testReleaseReusableInCacheNotReturnedToPool() throws Exception {
+ final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
+ poolEntry.assignConnection(connection);
+ Mockito.when(connection.isOpen()).thenReturn(true);
+ final H2SharingConnPool.PerRoutePool routePool = h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
+ routePool.track(poolEntry);
+ routePool.track(poolEntry);
+
+ h2SharingPool.release(poolEntry, true);
+
+ Mockito.verify(connPool, Mockito.never()).release(
+ Mockito.same(poolEntry),
+ Mockito.anyBoolean());
+ }
+
+ @Test
+ void testReleaseNonReusableInCacheReturnedToPool() throws Exception {
+ final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
+ poolEntry.assignConnection(connection);
+ Mockito.when(connection.isOpen()).thenReturn(true);
+ final H2SharingConnPool.PerRoutePool routePool = h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
+ routePool.track(poolEntry);
+ routePool.track(poolEntry);
+
+ h2SharingPool.release(poolEntry, false);
+
+ Mockito.verify(connPool).release(
+ Mockito.same(poolEntry),
+ Mockito.eq(false));
+ }
+
+ @Test
+ void testReleaseReusableAndClosedInCacheReturnedToPool() throws Exception {
+ final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
+ poolEntry.assignConnection(connection);
+ Mockito.when(connection.isOpen()).thenReturn(false);
+ final H2SharingConnPool.PerRoutePool routePool = h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
+ routePool.track(poolEntry);
+ routePool.track(poolEntry);
+
+ h2SharingPool.release(poolEntry, true);
+
+ Mockito.verify(connPool).release(
+ Mockito.same(poolEntry),
+ Mockito.eq(true));
+ }
+
+ @Test
+ void testClose() throws Exception {
+ h2SharingPool.close();
+
+ Mockito.verify(connPool).close();
+ }
+
+ @Test
+ void testCloseMode() throws Exception {
+ h2SharingPool.close(CloseMode.IMMEDIATE);
+
+ Mockito.verify(connPool).close(CloseMode.IMMEDIATE);
+ }
+
+ @Test
+ void testLeasePoolClosed() throws Exception {
+ h2SharingPool.close();
+
+ Assertions.assertThrows(IllegalStateException.class, () -> h2SharingPool.lease(DEFAULT_ROUTE, null, Timeout.ONE_MILLISECOND, callback));
+ }
+
+ @Test
+ void testReleasePoolClosed() throws Exception {
+ final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
+ poolEntry.assignConnection(connection);
+ Mockito.when(connection.isOpen()).thenReturn(false);
+ final H2SharingConnPool.PerRoutePool routePool = h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
+ routePool.track(poolEntry);
+
+ h2SharingPool.close();
+
+ h2SharingPool.release(poolEntry, true);
+
+ Mockito.verify(connPool).release(
+ Mockito.same(poolEntry),
+ Mockito.eq(true));
+ }
+
+}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingPerRoutePoolTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingPerRoutePoolTest.java
new file mode 100644
index 000000000..dc1a573fa
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingPerRoutePoolTest.java
@@ -0,0 +1,130 @@
+/*
+ * ====================================================================
+ * 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 org.apache.hc.core5.http.HttpConnection;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.pool.PoolEntry;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class H2SharingPerRoutePoolTest {
+
+ static PoolEntry createMockEntry() {
+ final PoolEntry poolEntry = new PoolEntry<>("some route");
+ final HttpConnection conn = Mockito.mock(HttpConnection.class);
+ Mockito.when(conn.isOpen()).thenReturn(true);
+ poolEntry.assignConnection(conn);
+ return poolEntry;
+ }
+
+ H2SharingConnPool.PerRoutePool pool;
+ PoolEntry poolEntry1;
+ PoolEntry poolEntry2;
+
+ @BeforeEach
+ void setup() {
+ pool = new H2SharingConnPool.PerRoutePool<>();
+ poolEntry1 = createMockEntry();
+ poolEntry2 = createMockEntry();
+ }
+
+ @Test
+ void testKeep() {
+ Assertions.assertEquals(1, pool.track(poolEntry1));
+ Assertions.assertEquals(2, pool.track(poolEntry1));
+ Assertions.assertEquals(1, pool.track(poolEntry2));
+ Assertions.assertEquals(3, pool.track(poolEntry1));
+ }
+
+ @Test
+ void testLeaseLeastUsed() {
+ pool.track(poolEntry1);
+ pool.track(poolEntry1);
+ pool.track(poolEntry2);
+ Assertions.assertSame(poolEntry2, pool.lease());
+ Assertions.assertEquals(2, pool.getCount(poolEntry2));
+
+ final PoolEntry poolEntry = pool.lease();
+ Assertions.assertEquals(3, pool.getCount(poolEntry));
+ }
+
+ @Test
+ void testLeaseEmptyPool() {
+ Assertions.assertNull(pool.lease());
+ }
+
+ @Test
+ void testReleaseReusable() {
+ pool.track(poolEntry1);
+ pool.track(poolEntry1);
+ pool.track(poolEntry1);
+
+ Assertions.assertEquals(2, pool.release(poolEntry1, true));
+ Assertions.assertEquals(1, pool.release(poolEntry1, true));
+ Assertions.assertEquals(0, pool.release(poolEntry1, true));
+ Assertions.assertEquals(0, pool.release(poolEntry1, true));
+ }
+
+ @Test
+ void testReleaseNonReusable() {
+ pool.track(poolEntry1);
+ pool.track(poolEntry1);
+ pool.track(poolEntry1);
+
+ Assertions.assertEquals(0, pool.release(poolEntry1, false));
+ }
+
+ @Test
+ void testReleaseNonPresent() {
+ Assertions.assertEquals(0, pool.release(poolEntry1, true));
+ Assertions.assertEquals(0, pool.release(poolEntry2, true));
+ }
+
+ @Test
+ void testReleaseConnectionClosed() {
+ pool.track(poolEntry1);
+ pool.track(poolEntry1);
+ pool.track(poolEntry1);
+
+ Mockito.when(poolEntry1.getConnection().isOpen()).thenReturn(false);
+ Assertions.assertEquals(0, pool.release(poolEntry1, true));
+ }
+
+ @Test
+ void testReleaseConnectionMissing() {
+ pool.track(poolEntry1);
+ pool.track(poolEntry1);
+ pool.track(poolEntry1);
+
+ poolEntry1.discardConnection(CloseMode.IMMEDIATE);
+ Assertions.assertEquals(0, pool.release(poolEntry1, true));
+ }
+
+}