Skip to content

Commit 51d067e

Browse files
Add "ConcurrencyAcquireDuration" metric for netty-nio-client (#2903)
* Add "ConcurrencyAcquireDuration" metric for netty-nio-client The time taken to acquire a new channel from a channel pool can be both non-trivial and highly variable, depending upon whether a new connection needs to be established, and depending upon the overhead of new connection establishment (including TLS handshakes). Due to the high variability, having an explicit metric can be helpful to give a better picture of latency sources in impacted requests.
1 parent b1dbeff commit 51d067e

File tree

5 files changed

+59
-1
lines changed

5 files changed

+59
-1
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "Netty NIO HTTP Client",
3+
"contributor": "",
4+
"type": "feature",
5+
"description": "Add \"ConcurrencyAcquireDuration\" metric for netty-nio-client"
6+
}

http-client-spi/src/main/java/software/amazon/awssdk/http/HttpMetric.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
package software.amazon.awssdk.http;
1717

18+
import java.time.Duration;
1819
import software.amazon.awssdk.annotations.SdkPublicApi;
1920
import software.amazon.awssdk.metrics.MetricCategory;
2021
import software.amazon.awssdk.metrics.MetricLevel;
@@ -104,6 +105,23 @@ public final class HttpMetric {
104105
public static final SdkMetric<Integer> HTTP_STATUS_CODE =
105106
metric("HttpStatusCode", Integer.class, MetricLevel.TRACE);
106107

108+
/**
109+
* The time taken to acquire a channel from the connection pool.
110+
*
111+
* <p>For HTTP/1 operations, a channel is equivalent to a TCP connection. For HTTP/2 operations, a channel is equivalent to
112+
* an HTTP/2 stream channel. For both protocols, the time to acquire a new concurrency permit may include the following:
113+
* <ol>
114+
* <li>Awaiting a concurrency permit, as restricted by the client's max concurrency configuration.</li>
115+
* <li>The time to establish a new connection, depending on whether an existing connection is available in the pool or
116+
* not.</li>
117+
* <li>The time taken to perform a TLS handshake/negotiation, if TLS is enabled.</li>
118+
* </ol>
119+
*
120+
* <p>Note: This metric is currently only supported in 'netty-nio-client'.
121+
*/
122+
public static final SdkMetric<Duration> CONCURRENCY_ACQUIRE_DURATION =
123+
metric("ConcurrencyAcquireDuration", Duration.class, MetricLevel.INFO);
124+
107125
private HttpMetric() {
108126
}
109127

http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NettyRequestExecutor.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515

1616
package software.amazon.awssdk.http.nio.netty.internal;
1717

18+
import static software.amazon.awssdk.http.HttpMetric.CONCURRENCY_ACQUIRE_DURATION;
1819
import static software.amazon.awssdk.http.nio.netty.internal.ChannelAttributeKey.EXECUTE_FUTURE_KEY;
1920
import static software.amazon.awssdk.http.nio.netty.internal.ChannelAttributeKey.EXECUTION_ID_KEY;
2021
import static software.amazon.awssdk.http.nio.netty.internal.ChannelAttributeKey.IN_USE;
2122
import static software.amazon.awssdk.http.nio.netty.internal.ChannelAttributeKey.KEEP_ALIVE;
2223
import static software.amazon.awssdk.http.nio.netty.internal.ChannelAttributeKey.LAST_HTTP_CONTENT_RECEIVED_KEY;
2324
import static software.amazon.awssdk.http.nio.netty.internal.ChannelAttributeKey.REQUEST_CONTEXT_KEY;
2425
import static software.amazon.awssdk.http.nio.netty.internal.ChannelAttributeKey.RESPONSE_COMPLETE_KEY;
26+
import static software.amazon.awssdk.http.nio.netty.internal.NettyRequestMetrics.measureTimeTaken;
2527
import static software.amazon.awssdk.http.nio.netty.internal.utils.NettyUtils.CLOSED_CHANNEL_MESSAGE;
2628

2729
import io.netty.buffer.ByteBuf;
@@ -90,11 +92,20 @@ public NettyRequestExecutor(RequestContext context) {
9092
public CompletableFuture<Void> execute() {
9193
Promise<Channel> channelFuture = context.eventLoopGroup().next().newPromise();
9294
executeFuture = createExecutionFuture(channelFuture);
93-
context.channelPool().acquire(channelFuture);
95+
acquireChannel(channelFuture);
9496
channelFuture.addListener((GenericFutureListener) this::makeRequestListener);
9597
return executeFuture;
9698
}
9799

100+
private void acquireChannel(Promise<Channel> channelFuture) {
101+
NettyRequestMetrics.ifMetricsAreEnabled(context.metricCollector(), metrics -> {
102+
measureTimeTaken(channelFuture, duration -> {
103+
metrics.reportMetric(CONCURRENCY_ACQUIRE_DURATION, duration);
104+
});
105+
});
106+
context.channelPool().acquire(channelFuture);
107+
}
108+
98109
/**
99110
* Convenience method to create the execution future and set up the cancellation logic.
100111
*

http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NettyRequestMetrics.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
import io.netty.channel.Channel;
1919
import io.netty.handler.codec.http2.Http2Connection;
2020
import io.netty.handler.codec.http2.Http2Stream;
21+
import io.netty.util.concurrent.Future;
22+
import java.time.Duration;
23+
import java.time.Instant;
2124
import java.util.Optional;
25+
import java.util.function.Consumer;
2226
import software.amazon.awssdk.annotations.SdkInternalApi;
2327
import software.amazon.awssdk.http.Http2Metric;
2428
import software.amazon.awssdk.metrics.MetricCollector;
@@ -39,6 +43,12 @@ public static boolean metricsAreEnabled(MetricCollector metricCollector) {
3943
return metricCollector != null && !(metricCollector instanceof NoOpMetricCollector);
4044
}
4145

46+
public static void ifMetricsAreEnabled(MetricCollector metrics, Consumer<MetricCollector> metricsConsumer) {
47+
if (metricsAreEnabled(metrics)) {
48+
metricsConsumer.accept(metrics);
49+
}
50+
}
51+
4252
/**
4353
* Publish stream metrics for the provided stream channel to the provided collector. This should only be invoked after
4454
* the stream has been initialized. If the stream is not initialized when this is invoked, an exception will be thrown.
@@ -73,4 +83,15 @@ private static void writeHttp2RequestMetrics(MetricCollector metricCollector,
7383
metricCollector.reportMetric(Http2Metric.REMOTE_STREAM_WINDOW_SIZE_IN_BYTES,
7484
http2Connection.remote().flowController().windowSize(stream));
7585
}
86+
87+
/**
88+
* Measure the time taken for a {@link Future} to complete. Does NOT differentiate between success/failure.
89+
*/
90+
public static void measureTimeTaken(Future<?> future, Consumer<Duration> onDone) {
91+
Instant start = Instant.now();
92+
future.addListener(f -> {
93+
Duration elapsed = Duration.between(start, Instant.now());
94+
onDone.accept(elapsed);
95+
});
96+
}
7697
}

http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/Http2MetricsTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public void maxClientStreamsLowerThanServerMaxStreamsReportClientMaxStreams() {
8686
assertThat(metrics.metricValues(HttpMetric.LEASED_CONCURRENCY).get(0)).isBetween(0, 1);
8787
assertThat(metrics.metricValues(HttpMetric.PENDING_CONCURRENCY_ACQUIRES).get(0)).isBetween(0, 1);
8888
assertThat(metrics.metricValues(HttpMetric.AVAILABLE_CONCURRENCY)).containsExactly(0);
89+
assertThat(metrics.metricValues(HttpMetric.CONCURRENCY_ACQUIRE_DURATION).get(0)).isPositive();
8990
// The stream window doesn't get initialized with the connection
9091
// initial setting and the update appears to be asynchronous so
9192
// this may be the default window size just based on when the
@@ -113,6 +114,7 @@ public void maxClientStreamsHigherThanServerMaxStreamsReportServerMaxStreams() {
113114
assertThat(metrics.metricValues(HttpMetric.LEASED_CONCURRENCY).get(0)).isBetween(0, 1);
114115
assertThat(metrics.metricValues(HttpMetric.PENDING_CONCURRENCY_ACQUIRES).get(0)).isBetween(0, 1);
115116
assertThat(metrics.metricValues(HttpMetric.AVAILABLE_CONCURRENCY).get(0)).isIn(0, 2, 3);
117+
assertThat(metrics.metricValues(HttpMetric.CONCURRENCY_ACQUIRE_DURATION).get(0)).isPositive();
116118
// The stream window doesn't get initialized with the connection
117119
// initial setting and the update appears to be asynchronous so
118120
// this may be the default window size just based on when the

0 commit comments

Comments
 (0)