Skip to content

Commit 085d8e4

Browse files
author
Bennett Lynch
committed
Add "ChannelAcquireDuration" metric for apache-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). The same metric was recently added for netty-nio-client: aws#2903
1 parent 51d067e commit 085d8e4

File tree

5 files changed

+145
-20
lines changed

5 files changed

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

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,13 @@ public final class HttpMetric {
109109
* The time taken to acquire a channel from the connection pool.
110110
*
111111
* <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:
112+
* an HTTP/2 stream channel. For both protocols, the time to acquire a new channel may include the following:
113113
* <ol>
114114
* <li>Awaiting a concurrency permit, as restricted by the client's max concurrency configuration.</li>
115115
* <li>The time to establish a new connection, depending on whether an existing connection is available in the pool or
116116
* not.</li>
117117
* <li>The time taken to perform a TLS handshake/negotiation, if TLS is enabled.</li>
118118
* </ol>
119-
*
120-
* <p>Note: This metric is currently only supported in 'netty-nio-client'.
121119
*/
122120
public static final SdkMetric<Duration> CONCURRENCY_ACQUIRE_DURATION =
123121
metric("ConcurrencyAcquireDuration", Duration.class, MetricLevel.INFO);

http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/ApacheHttpClient.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
import software.amazon.awssdk.http.apache.internal.DefaultConfiguration;
8282
import software.amazon.awssdk.http.apache.internal.SdkProxyRoutePlanner;
8383
import software.amazon.awssdk.http.apache.internal.conn.ClientConnectionManagerFactory;
84+
import software.amazon.awssdk.http.apache.internal.conn.ClientConnectionRequestFactory;
8485
import software.amazon.awssdk.http.apache.internal.conn.IdleConnectionReaper;
8586
import software.amazon.awssdk.http.apache.internal.conn.SdkConnectionKeepAliveStrategy;
8687
import software.amazon.awssdk.http.apache.internal.conn.SdkTlsSocketFactory;
@@ -230,7 +231,7 @@ public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) {
230231
return new ExecutableHttpRequest() {
231232
@Override
232233
public HttpExecuteResponse call() throws IOException {
233-
HttpExecuteResponse executeResponse = execute(apacheRequest);
234+
HttpExecuteResponse executeResponse = execute(apacheRequest, metricCollector);
234235
collectPoolMetric(metricCollector);
235236
return executeResponse;
236237
}
@@ -249,10 +250,15 @@ public void close() {
249250
cm.shutdown();
250251
}
251252

252-
private HttpExecuteResponse execute(HttpRequestBase apacheRequest) throws IOException {
253+
private HttpExecuteResponse execute(HttpRequestBase apacheRequest, MetricCollector metricCollector) throws IOException {
253254
HttpClientContext localRequestContext = ApacheUtils.newClientContext(requestConfig.proxyConfiguration());
254-
HttpResponse httpResponse = httpClient.execute(apacheRequest, localRequestContext);
255-
return createResponse(httpResponse, apacheRequest);
255+
ClientConnectionRequestFactory.THREAD_LOCAL_REQUEST_METRIC_COLLECTOR.set(metricCollector);
256+
try {
257+
HttpResponse httpResponse = httpClient.execute(apacheRequest, localRequestContext);
258+
return createResponse(httpResponse, apacheRequest);
259+
} finally {
260+
ClientConnectionRequestFactory.THREAD_LOCAL_REQUEST_METRIC_COLLECTOR.remove();
261+
}
256262
}
257263

258264
private HttpRequestBase toApacheRequest(HttpExecuteRequest request) {

http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/conn/ClientConnectionRequestFactory.java

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,24 @@
2020
import java.lang.reflect.InvocationTargetException;
2121
import java.lang.reflect.Method;
2222
import java.lang.reflect.Proxy;
23+
import java.time.Duration;
24+
import java.time.Instant;
2325
import org.apache.http.conn.ConnectionRequest;
2426
import org.slf4j.Logger;
2527
import org.slf4j.LoggerFactory;
2628
import software.amazon.awssdk.annotations.SdkInternalApi;
29+
import software.amazon.awssdk.http.HttpMetric;
30+
import software.amazon.awssdk.http.apache.ApacheHttpClient;
31+
import software.amazon.awssdk.metrics.MetricCollector;
2732

2833
@SdkInternalApi
29-
final class ClientConnectionRequestFactory {
34+
public final class ClientConnectionRequestFactory {
35+
36+
/**
37+
* {@link ThreadLocal}, request-level {@link MetricCollector}, set and removed by {@link ApacheHttpClient}.
38+
*/
39+
public static final ThreadLocal<MetricCollector> THREAD_LOCAL_REQUEST_METRIC_COLLECTOR = new ThreadLocal<>();
40+
3041
private static final Logger log = LoggerFactory.getLogger(ClientConnectionRequestFactory.class);
3142
private static final Class<?>[] INTERFACES = {
3243
ConnectionRequest.class,
@@ -69,22 +80,27 @@ private static class Handler implements InvocationHandler {
6980
@Override
7081
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
7182
try {
72-
// TODO v2 service metrics
73-
// if ("get".equals(method.getName())) {
74-
// ServiceLatencyProvider latencyProvider = new ServiceLatencyProvider(
75-
// AWSServiceMetrics.HttpClientGetConnectionTime);
76-
// try {
77-
// return method.invoke(orig, args);
78-
// } finally {
79-
// AwsSdkMetrics.getServiceMetricCollector()
80-
// .collectLatency(latencyProvider.endTiming());
81-
// }
82-
// }
83-
return method.invoke(orig, args);
83+
Instant startGet = null;
84+
if ("get".equals(method.getName())) {
85+
startGet = Instant.now();
86+
}
87+
try {
88+
return method.invoke(orig, args);
89+
} finally {
90+
if (startGet != null) {
91+
recordGetTime(startGet);
92+
}
93+
}
8494
} catch (InvocationTargetException e) {
8595
log.debug("", e);
8696
throw e.getCause();
8797
}
8898
}
99+
100+
private void recordGetTime(Instant startGet) {
101+
Duration elapsed = Duration.between(startGet, Instant.now());
102+
MetricCollector metricCollector = THREAD_LOCAL_REQUEST_METRIC_COLLECTOR.get();
103+
metricCollector.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, elapsed);
104+
}
89105
}
90106
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.http.apache;
17+
18+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
19+
import static com.github.tomakehurst.wiremock.client.WireMock.any;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
import static software.amazon.awssdk.http.HttpMetric.CONCURRENCY_ACQUIRE_DURATION;
23+
24+
import com.github.tomakehurst.wiremock.WireMockServer;
25+
import java.io.IOException;
26+
import org.junit.After;
27+
import org.junit.AfterClass;
28+
import org.junit.Before;
29+
import org.junit.BeforeClass;
30+
import org.junit.Rule;
31+
import org.junit.Test;
32+
import org.junit.rules.ExpectedException;
33+
import software.amazon.awssdk.http.HttpExecuteRequest;
34+
import software.amazon.awssdk.http.HttpExecuteResponse;
35+
import software.amazon.awssdk.http.SdkHttpClient;
36+
import software.amazon.awssdk.http.SdkHttpFullRequest;
37+
import software.amazon.awssdk.http.SdkHttpMethod;
38+
import software.amazon.awssdk.http.SdkHttpRequest;
39+
import software.amazon.awssdk.metrics.MetricCollection;
40+
import software.amazon.awssdk.metrics.MetricCollector;
41+
42+
43+
public class ApacheMetricsTest {
44+
private static WireMockServer wireMockServer;
45+
private SdkHttpClient client;
46+
47+
@Rule
48+
public ExpectedException thrown = ExpectedException.none();
49+
50+
@BeforeClass
51+
public static void setUp() throws IOException {
52+
wireMockServer = new WireMockServer();
53+
wireMockServer.start();
54+
}
55+
56+
@Before
57+
public void methodSetup() {
58+
wireMockServer.stubFor(any(urlMatching(".*")).willReturn(aResponse().withStatus(200).withBody("{}")));
59+
}
60+
61+
@AfterClass
62+
public static void teardown() throws IOException {
63+
wireMockServer.stop();
64+
}
65+
66+
@After
67+
public void methodTeardown() {
68+
if (client != null) {
69+
client.close();
70+
}
71+
client = null;
72+
}
73+
74+
@Test
75+
public void defaultTlsKeyManagersProviderIsSystemPropertyProvider() throws IOException {
76+
client = ApacheHttpClient.create();
77+
MetricCollector collector = MetricCollector.create("test");
78+
makeRequestWithMetrics(client, collector);
79+
80+
MetricCollection collection = collector.collect();
81+
82+
assertThat(collection.metricValues(CONCURRENCY_ACQUIRE_DURATION).get(0)).isPositive();
83+
}
84+
85+
private HttpExecuteResponse makeRequestWithMetrics(SdkHttpClient httpClient, MetricCollector metricCollector) throws IOException {
86+
SdkHttpRequest httpRequest = SdkHttpFullRequest.builder()
87+
.method(SdkHttpMethod.GET)
88+
.protocol("http")
89+
.host("localhost:" + wireMockServer.port())
90+
.build();
91+
92+
HttpExecuteRequest request = HttpExecuteRequest.builder()
93+
.request(httpRequest)
94+
.metricCollector(metricCollector)
95+
.build();
96+
97+
return httpClient.prepareRequest(request).call();
98+
}
99+
}

0 commit comments

Comments
 (0)