From a48c5c6e225bd19d1b0e89591f78c5e7154f1878 Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Wed, 28 Oct 2026 14:52:39 +0100 Subject: [PATCH] Meter pool leases automatically when CONN_POOL is on `HttpClientObservationSupport.enable(...)` with `MetricSet.CONN_POOL` only bound the pool gauges. The `.pool.lease` timer and the `.pool.leases` counter were recorded only if the caller wrapped the connection manager explicitly with `meteredConnectionManager()` or `meteredAsyncConnectionManager()`. This is not possible when a library creates the connection manager internally and only exposes the builder, for example `ApacheHttpClient5TransportBuilder` in `opensearch-java`. All `enable(...)` variants (classic, async and caching) now replace the connection manager of the builder with the metered wrapper and bind the pool gauges to it: - A manager that is already a `MeteredConnectionManager` or a `MeteredAsyncConnectionManager` is not wrapped again, so explicit wrapping and repeated `enable(...)` calls stay safe. - The connection manager shared flag is not changed. - Pool gauges are bound only if the original manager is a `ConnPoolControl`, as before. - If no connection manager is set on the builder, no pool metrics are installed, as before. The default manager that `build()` creates is not instrumented. - If `CONN_POOL` is disabled, nothing changes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../HttpClientObservationSupport.java | 99 +++++- .../HttpClientObservationSupportTest.java | 292 ++++++++++++++++++ 2 files changed, 384 insertions(+), 7 deletions(-) diff --git a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/HttpClientObservationSupport.java b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/HttpClientObservationSupport.java index 6880f594b3..e96823e78f 100644 --- a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/HttpClientObservationSupport.java +++ b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/HttpClientObservationSupport.java @@ -49,6 +49,7 @@ import org.apache.hc.client5.http.observation.interceptors.TimerExec; import org.apache.hc.client5.http.io.HttpClientConnectionManager; import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; +import org.apache.hc.core5.pool.ConnPoolControl; import org.apache.hc.core5.util.Args; import org.apache.hc.core5.http.nio.ssl.TlsStrategy; @@ -94,9 +95,37 @@ * that surrounds each execution with a start/stop span. *
  • Metric interceptors according to {@link ObservingOptions.MetricSet}: * BASIC (latency timer + response counter), IO (bytes in/out counters), - * and CONN_POOL (pool gauges; classic and async variants). Pool lease - * timing is available when using {@link #meteredConnectionManager} or - * {@link #meteredAsyncConnectionManager}.
  • + * and CONN_POOL (pool gauges and pool lease metrics; classic and async + * variants). + * + * + *

    Connection pool metrics

    + *

    When {@link ObservingOptions.MetricSet#CONN_POOL} is enabled, the + * {@code enable(...)} methods operate on the connection manager that is + * attached to the builder at the time of the call:

    + *
      + *
    • The connection manager is replaced with a metered wrapper (see + * {@link #meteredConnectionManager} and + * {@link #meteredAsyncConnectionManager}) that records the + * {@code .pool.lease} timer and the {@code .pool.leases} + * counter.
    • + *
    • If the connection manager is a connection pool + * ({@link ConnPoolControl}), the {@code .pool.leased}, + * {@code .pool.available} and {@code .pool.pending} + * gauges are bound to it.
    • + *
    • If the connection manager is already a {@link MeteredConnectionManager} + * or {@link MeteredAsyncConnectionManager} (for instance because it was + * wrapped explicitly or {@code enable(...)} was called before), it is not + * wrapped again. Lease metrics continue to be recorded by the existing + * wrapper.
    • + *
    • The connection manager shared flag + * ({@code setConnectionManagerShared(boolean)}) is not changed. The + * wrapper delegates {@code close} to the original connection manager.
    • + *
    • If no connection manager is attached to the builder, no pool metrics + * are installed. The connection manager that the builder creates by + * default in {@code build()} is not instrumented. Set the connection + * manager on the builder before calling {@code enable(...)} to get pool + * metrics.
    • *
    * *

    Thread safety: This class is stateless. Methods may be @@ -172,6 +201,10 @@ public static void enable(final HttpClientBuilder builder, * *

    Installs interceptors at the beginning of the execution chain.

    * + *

    If {@link ObservingOptions.MetricSet#CONN_POOL} is enabled, the connection manager + * attached to the builder is wrapped with pool-lease metrics and the pool gauges are + * bound to it. See the class documentation for details.

    + * * @param builder client builder to instrument * @param obsReg observation registry; if {@code null} no observations are installed * @param meterReg meter registry to register meters with (must not be {@code null}) @@ -204,7 +237,7 @@ public static void enable(final HttpClientBuilder builder, builder.addExecInterceptorFirst(IO_ID, new IoByteCounterExec(meterReg, o, config)); } if (o.metricSets.contains(ObservingOptions.MetricSet.CONN_POOL)) { - ConnPoolMeters.bindTo(builder, meterReg, config); + bindConnPoolMetrics(builder, meterReg, o, config); } } @@ -262,6 +295,10 @@ public static void enable(final CachingHttpClientBuilder builder, *

    Interceptors are installed after the caching element so that * metrics/observations reflect the actual exchange.

    * + *

    If {@link ObservingOptions.MetricSet#CONN_POOL} is enabled, the connection manager + * attached to the builder is wrapped with pool-lease metrics and the pool gauges are + * bound to it. See the class documentation for details.

    + * * @param builder caching client builder to instrument * @param obsReg observation registry; if {@code null} no observations are installed * @param meterReg meter registry to register meters with (must not be {@code null}) @@ -295,7 +332,7 @@ public static void enable(final CachingHttpClientBuilder builder, builder.addExecInterceptorAfter(ChainElement.CACHING.name(), IO_ID, new IoByteCounterExec(meterReg, o, config)); } if (o.metricSets.contains(ObservingOptions.MetricSet.CONN_POOL)) { - ConnPoolMeters.bindTo(builder, meterReg, config); + bindConnPoolMetrics(builder, meterReg, o, config); } } @@ -350,6 +387,10 @@ public static void enable(final HttpAsyncClientBuilder builder, * Enables observations and metrics on an async client builder using explicit * registries and {@link MetricConfig}. * + *

    If {@link ObservingOptions.MetricSet#CONN_POOL} is enabled, the connection manager + * attached to the builder is wrapped with pool-lease metrics and the pool gauges are + * bound to it. See the class documentation for details.

    + * * @param builder async client builder to instrument * @param obsReg observation registry; if {@code null} no observations are installed * @param meterReg meter registry to register meters with (must not be {@code null}) @@ -382,7 +423,7 @@ public static void enable(final HttpAsyncClientBuilder builder, builder.addExecInterceptorFirst(IO_ID, new AsyncIoByteCounterExec(meterReg, o, config)); } if (o.metricSets.contains(ObservingOptions.MetricSet.CONN_POOL)) { - ConnPoolMetersAsync.bindTo(builder, meterReg, config); + bindConnPoolMetrics(builder, meterReg, o, config); } } @@ -439,6 +480,10 @@ public static void enable(final CachingHttpAsyncClientBuilder builder, * *

    Interceptors are installed after the caching element.

    * + *

    If {@link ObservingOptions.MetricSet#CONN_POOL} is enabled, the connection manager + * attached to the builder is wrapped with pool-lease metrics and the pool gauges are + * bound to it. See the class documentation for details.

    + * * @param builder caching async client builder to instrument * @param obsReg observation registry; if {@code null} no observations are installed * @param meterReg meter registry to register meters with (must not be {@code null}) @@ -472,7 +517,7 @@ public static void enable(final CachingHttpAsyncClientBuilder builder, builder.addExecInterceptorAfter(ChainElement.CACHING.name(), IO_ID, new AsyncIoByteCounterExec(meterReg, o, config)); } if (o.metricSets.contains(ObservingOptions.MetricSet.CONN_POOL)) { - ConnPoolMetersAsync.bindTo(builder, meterReg, config); + bindConnPoolMetrics(builder, meterReg, o, config); } } @@ -578,6 +623,46 @@ public static TlsStrategy meteredTlsStrategy(final TlsStrategy delegate, return new MeteredTlsStrategy(delegate, meterReg, config, o); } + /** + * Wraps the classic connection manager of the builder with pool-lease metrics + * and binds the pool gauges to it. + */ + private static void bindConnPoolMetrics(final HttpClientBuilder builder, + final MeterRegistry meterReg, + final ObservingOptions opts, + final MetricConfig config) { + final HttpClientConnectionManager cm = builder.getConnManager(); + if (cm == null) { + return; + } + if (!(cm instanceof MeteredConnectionManager)) { + builder.setConnectionManager(meteredConnectionManager(cm, meterReg, opts, config)); + } + if (cm instanceof ConnPoolControl) { + ConnPoolMeters.bindTo(builder, meterReg, config); + } + } + + /** + * Wraps the async connection manager of the builder with pool-lease metrics + * and binds the pool gauges to it. + */ + private static void bindConnPoolMetrics(final HttpAsyncClientBuilder builder, + final MeterRegistry meterReg, + final ObservingOptions opts, + final MetricConfig config) { + final AsyncClientConnectionManager cm = builder.getConnManager(); + if (cm == null) { + return; + } + if (!(cm instanceof MeteredAsyncConnectionManager)) { + builder.setConnectionManager(meteredAsyncConnectionManager(cm, meterReg, opts, config)); + } + if (cm instanceof ConnPoolControl) { + ConnPoolMetersAsync.bindTo(builder, meterReg, config); + } + } + /** * No instantiation. * diff --git a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/HttpClientObservationSupportTest.java b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/HttpClientObservationSupportTest.java index 3a9d541979..ca43bdad7d 100644 --- a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/HttpClientObservationSupportTest.java +++ b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/HttpClientObservationSupportTest.java @@ -27,23 +27,39 @@ package org.apache.hc.client5.http.observation; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.InetAddress; import java.net.SocketAddress; import java.util.EnumSet; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.micrometer.observation.ObservationRegistry; import org.apache.hc.client5.http.DnsResolver; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.cache.CachingHttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.cache.CachingHttpClientBuilder; +import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; import org.apache.hc.client5.http.io.HttpClientConnectionManager; import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; @@ -238,5 +254,281 @@ void cachingAsyncRegistersConnPoolMeters() { assertNotNull(meters.find(mc.prefix + ".pool.leased").gauge()); assertNotNull(meters.find(mc.prefix + ".pool.available").gauge()); assertNotNull(meters.find(mc.prefix + ".pool.pending").gauge()); + assertInstanceOf(MeteredAsyncConnectionManager.class, builder.getConnManager()); + } + + private static final ObservingOptions CONN_POOL_ONLY = ObservingOptions.builder() + .metrics(EnumSet.of(ObservingOptions.MetricSet.CONN_POOL)) + .build(); + + private static final MetricConfig POOL_MC = MetricConfig.builder().prefix("pool-it").build(); + + private static final class CloseTrackingPoolingConnectionManager extends PoolingHttpClientConnectionManager { + private final AtomicBoolean closed = new AtomicBoolean(); + + @Override + public void close() { + closed.set(true); + super.close(); + } + + @Override + public void close(final CloseMode closeMode) { + closed.set(true); + super.close(closeMode); + } + } + + private static final class CloseTrackingPoolingAsyncConnectionManager extends PoolingAsyncClientConnectionManager { + private final AtomicBoolean closed = new AtomicBoolean(); + + @Override + public void close() { + closed.set(true); + super.close(); + } + + @Override + public void close(final CloseMode closeMode) { + closed.set(true); + super.close(closeMode); + } + } + + private HttpHost startServer() throws Exception { + server = ServerBootstrap.bootstrap() + .setLocalAddress(InetAddress.getLoopbackAddress()) + .setListenerPort(0) + .register("localhost", "/get", (request, response, context) -> { + response.setCode(HttpStatus.SC_OK); + response.setEntity(new StringEntity("{\"ok\":true}", ContentType.APPLICATION_JSON)); + }) + .create(); + server.start(); + return new HttpHost("http", "localhost", server.getLocalPort()); + } + + private static void executeClassic(final HttpClientBuilder builder, final HttpHost target) throws Exception { + try (final CloseableHttpClient client = builder.build()) { + try (final ClassicHttpResponse resp = client.executeOpen(target, ClassicRequestBuilder.get("/get").build(), null)) { + assertEquals(200, resp.getCode()); + } + } + } + + private static void executeAsync(final HttpAsyncClientBuilder builder, final HttpHost target) throws Exception { + try (final CloseableHttpAsyncClient client = builder.build()) { + client.start(); + final SimpleHttpResponse resp = client.execute( + SimpleRequestBuilder.get().setHttpHost(target).setPath("/get").build(), null) + .get(30, TimeUnit.SECONDS); + assertEquals(200, resp.getCode()); + } + } + + private static void assertPoolMetricsRecorded(final MeterRegistry meters, final long expectedLeases) { + final Timer lease = meters.find(POOL_MC.prefix + ".pool.lease").tag("result", "ok").timer(); + assertNotNull(lease); + assertEquals(expectedLeases, lease.count()); + final Counter leases = meters.find(POOL_MC.prefix + ".pool.leases").tag("result", "ok").counter(); + assertNotNull(leases); + assertEquals(expectedLeases, (long) leases.count()); + assertNotNull(meters.find(POOL_MC.prefix + ".pool.leased").gauge()); + assertNotNull(meters.find(POOL_MC.prefix + ".pool.available").gauge()); + assertNotNull(meters.find(POOL_MC.prefix + ".pool.pending").gauge()); + } + + private static void assertNoPoolMetrics(final MeterRegistry meters) { + assertNull(meters.find(POOL_MC.prefix + ".pool.lease").timer()); + assertNull(meters.find(POOL_MC.prefix + ".pool.leases").counter()); + assertNull(meters.find(POOL_MC.prefix + ".pool.leased").gauge()); + assertNull(meters.find(POOL_MC.prefix + ".pool.available").gauge()); + assertNull(meters.find(POOL_MC.prefix + ".pool.pending").gauge()); + } + + @Test + void classicEnableWrapsConnectionManagerAndRecordsPoolMetrics() throws Exception { + final HttpHost target = startServer(); + final MeterRegistry meters = new SimpleMeterRegistry(); + final HttpClientBuilder b = HttpClients.custom() + .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create().build()); + + HttpClientObservationSupport.enable(b, null, meters, CONN_POOL_ONLY, POOL_MC); + + assertInstanceOf(MeteredConnectionManager.class, b.getConnManager()); + executeClassic(b, target); + assertPoolMetricsRecorded(meters, 1); + } + + @Test + void asyncEnableWrapsConnectionManagerAndRecordsPoolMetrics() throws Exception { + final HttpHost target = startServer(); + final MeterRegistry meters = new SimpleMeterRegistry(); + final HttpAsyncClientBuilder b = HttpAsyncClients.custom() + .setConnectionManager(PoolingAsyncClientConnectionManagerBuilder.create().build()); + + HttpClientObservationSupport.enable(b, null, meters, CONN_POOL_ONLY, POOL_MC); + + assertInstanceOf(MeteredAsyncConnectionManager.class, b.getConnManager()); + executeAsync(b, target); + assertPoolMetricsRecorded(meters, 1); + } + + @Test + void cachingClassicEnableWrapsConnectionManager() { + final MeterRegistry meters = new SimpleMeterRegistry(); + final CachingHttpClientBuilder b = CachingHttpClientBuilder.create(); + b.setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create().build()); + + HttpClientObservationSupport.enable(b, null, meters, CONN_POOL_ONLY, POOL_MC); + + assertInstanceOf(MeteredConnectionManager.class, b.getConnManager()); + assertNotNull(meters.find(POOL_MC.prefix + ".pool.leased").gauge()); + } + + @Test + void classicEnableTwiceDoesNotDoubleWrap() throws Exception { + final HttpHost target = startServer(); + final MeterRegistry meters = new SimpleMeterRegistry(); + final HttpClientBuilder b = HttpClients.custom() + .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create().build()); + + HttpClientObservationSupport.enable(b, null, meters, CONN_POOL_ONLY, POOL_MC); + final HttpClientConnectionManager first = b.getConnManager(); + HttpClientObservationSupport.enable(b, null, meters, CONN_POOL_ONLY, POOL_MC); + + assertSame(first, b.getConnManager()); + executeClassic(b, target); + assertPoolMetricsRecorded(meters, 1); + } + + @Test + void asyncEnableTwiceDoesNotDoubleWrap() throws Exception { + final HttpHost target = startServer(); + final MeterRegistry meters = new SimpleMeterRegistry(); + final HttpAsyncClientBuilder b = HttpAsyncClients.custom() + .setConnectionManager(PoolingAsyncClientConnectionManagerBuilder.create().build()); + + HttpClientObservationSupport.enable(b, null, meters, CONN_POOL_ONLY, POOL_MC); + final AsyncClientConnectionManager first = b.getConnManager(); + HttpClientObservationSupport.enable(b, null, meters, CONN_POOL_ONLY, POOL_MC); + + assertSame(first, b.getConnManager()); + executeAsync(b, target); + assertPoolMetricsRecorded(meters, 1); + } + + @Test + void explicitlyMeteredConnectionManagersAreKept() { + final MeterRegistry meters = new SimpleMeterRegistry(); + + final HttpClientConnectionManager classic = HttpClientObservationSupport.meteredConnectionManager( + PoolingHttpClientConnectionManagerBuilder.create().build(), meters, CONN_POOL_ONLY, POOL_MC); + final HttpClientBuilder classicBuilder = HttpClients.custom().setConnectionManager(classic); + HttpClientObservationSupport.enable(classicBuilder, null, meters, CONN_POOL_ONLY, POOL_MC); + assertSame(classic, classicBuilder.getConnManager()); + + final AsyncClientConnectionManager async = HttpClientObservationSupport.meteredAsyncConnectionManager( + PoolingAsyncClientConnectionManagerBuilder.create().build(), meters, CONN_POOL_ONLY, POOL_MC); + final HttpAsyncClientBuilder asyncBuilder = HttpAsyncClients.custom().setConnectionManager(async); + HttpClientObservationSupport.enable(asyncBuilder, null, meters, CONN_POOL_ONLY, POOL_MC); + assertSame(async, asyncBuilder.getConnManager()); + + assertNotNull(meters.find(POOL_MC.prefix + ".pool.leased").gauge()); + } + + @Test + void enableWithoutConnectionManagerInstallsNoPoolMetrics() throws Exception { + final HttpHost target = startServer(); + final MeterRegistry meters = new SimpleMeterRegistry(); + + final HttpClientBuilder classicBuilder = HttpClients.custom(); + HttpClientObservationSupport.enable(classicBuilder, null, meters, CONN_POOL_ONLY, POOL_MC); + assertNull(classicBuilder.getConnManager()); + executeClassic(classicBuilder, target); + + final HttpAsyncClientBuilder asyncBuilder = HttpAsyncClients.custom(); + HttpClientObservationSupport.enable(asyncBuilder, null, meters, CONN_POOL_ONLY, POOL_MC); + assertNull(asyncBuilder.getConnManager()); + executeAsync(asyncBuilder, target); + + assertNoPoolMetrics(meters); + } + + @Test + void connPoolDisabledLeavesConnectionManagerUnchanged() throws Exception { + final HttpHost target = startServer(); + final MeterRegistry meters = new SimpleMeterRegistry(); + final ObservingOptions basicOnly = ObservingOptions.builder() + .metrics(EnumSet.of(ObservingOptions.MetricSet.BASIC)) + .build(); + + final HttpClientConnectionManager classic = PoolingHttpClientConnectionManagerBuilder.create().build(); + final HttpClientBuilder classicBuilder = HttpClients.custom().setConnectionManager(classic); + HttpClientObservationSupport.enable(classicBuilder, null, meters, basicOnly, POOL_MC); + assertSame(classic, classicBuilder.getConnManager()); + executeClassic(classicBuilder, target); + + final AsyncClientConnectionManager async = PoolingAsyncClientConnectionManagerBuilder.create().build(); + final HttpAsyncClientBuilder asyncBuilder = HttpAsyncClients.custom().setConnectionManager(async); + HttpClientObservationSupport.enable(asyncBuilder, null, meters, basicOnly, POOL_MC); + assertSame(async, asyncBuilder.getConnManager()); + executeAsync(asyncBuilder, target); + + assertNoPoolMetrics(meters); + } + + @Test + void nonPoolingConnectionManagerIsWrappedWithoutGauges() { + final MeterRegistry meters = new SimpleMeterRegistry(); + final HttpClientBuilder b = HttpClients.custom() + .setConnectionManager(new BasicHttpClientConnectionManager()); + + HttpClientObservationSupport.enable(b, null, meters, CONN_POOL_ONLY, POOL_MC); + + assertInstanceOf(MeteredConnectionManager.class, b.getConnManager()); + assertNull(meters.find(POOL_MC.prefix + ".pool.leased").gauge()); + } + + @Test + void classicSharedConnectionManagerFlagIsPreserved() throws Exception { + final HttpHost target = startServer(); + final MeterRegistry meters = new SimpleMeterRegistry(); + + final CloseTrackingPoolingConnectionManager shared = new CloseTrackingPoolingConnectionManager(); + final HttpClientBuilder sharedBuilder = HttpClients.custom() + .setConnectionManager(shared) + .setConnectionManagerShared(true); + HttpClientObservationSupport.enable(sharedBuilder, null, meters, CONN_POOL_ONLY, POOL_MC); + executeClassic(sharedBuilder, target); + assertFalse(shared.closed.get()); + shared.close(); + + final CloseTrackingPoolingConnectionManager owned = new CloseTrackingPoolingConnectionManager(); + final HttpClientBuilder ownedBuilder = HttpClients.custom().setConnectionManager(owned); + HttpClientObservationSupport.enable(ownedBuilder, null, meters, CONN_POOL_ONLY, POOL_MC); + executeClassic(ownedBuilder, target); + assertTrue(owned.closed.get()); + } + + @Test + void asyncSharedConnectionManagerFlagIsPreserved() throws Exception { + final HttpHost target = startServer(); + final MeterRegistry meters = new SimpleMeterRegistry(); + + final CloseTrackingPoolingAsyncConnectionManager shared = new CloseTrackingPoolingAsyncConnectionManager(); + final HttpAsyncClientBuilder sharedBuilder = HttpAsyncClients.custom() + .setConnectionManager(shared) + .setConnectionManagerShared(true); + HttpClientObservationSupport.enable(sharedBuilder, null, meters, CONN_POOL_ONLY, POOL_MC); + executeAsync(sharedBuilder, target); + assertFalse(shared.closed.get()); + shared.close(); + + final CloseTrackingPoolingAsyncConnectionManager owned = new CloseTrackingPoolingAsyncConnectionManager(); + final HttpAsyncClientBuilder ownedBuilder = HttpAsyncClients.custom().setConnectionManager(owned); + HttpClientObservationSupport.enable(ownedBuilder, null, meters, CONN_POOL_ONLY, POOL_MC); + executeAsync(ownedBuilder, target); + assertTrue(owned.closed.get()); } }