Skip to content

Commit 3b697c3

Browse files
authored
Merge CRT H2 Support (#6729)
* Add CRT HTTP/2 support (#6702) * Add CRT HTTP/2 support * Update CRT version and re-add back comments # Conflicts: # pom.xml * Fix version * Trigger import * Fix builds * Close tlsContextOptions properly (#6730) * Add CRT HTTP/2 stability tests and JMH benchmarks (#6723) * Add stability tests and JMH tests # Conflicts: # services/kinesis/src/it/java/software/amazon/awssdk/services/kinesis/AbstractTestCase.java * Fix tests * Fix checkstyle errors * Address feedback * Minor refactoring to rename variables (#6763) * Set maxConcurrentStreams on HTTP/2 stream manager so that the MAX_CON… (#6812) * Set maxConcurrentStreams on HTTP/2 stream manager so that the MAX_CONNECTIONS setting is respected for HTTP/2. Also improved the error message when HTTP/2 is configured with the sync client to clarify available options. * Update http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java * Fix checkstyle errors * Add changelog entry * Retry health check failure
1 parent 435922a commit 3b697c3

File tree

38 files changed

+1560
-964
lines changed

38 files changed

+1560
-964
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS CRT Async HTTP Client",
4+
"contributor": "",
5+
"description": "Add HTTP/2 support in the AWS CRT Async HTTP Client."
6+
}

http-clients/aws-crt-client/pom.xml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,31 @@
173173
<artifactId>mockito-junit-jupiter</artifactId>
174174
<scope>test</scope>
175175
</dependency>
176+
<dependency>
177+
<groupId>io.netty</groupId>
178+
<artifactId>netty-codec-http2</artifactId>
179+
<scope>test</scope>
180+
</dependency>
181+
<dependency>
182+
<groupId>io.netty</groupId>
183+
<artifactId>netty-common</artifactId>
184+
<scope>test</scope>
185+
</dependency>
186+
<dependency>
187+
<groupId>io.netty</groupId>
188+
<artifactId>netty-transport</artifactId>
189+
<scope>test</scope>
190+
</dependency>
191+
<dependency>
192+
<groupId>io.netty</groupId>
193+
<artifactId>netty-codec-http</artifactId>
194+
<scope>test</scope>
195+
</dependency>
196+
<dependency>
197+
<groupId>io.netty</groupId>
198+
<artifactId>netty-handler</artifactId>
199+
<scope>test</scope>
200+
</dependency>
176201
</dependencies>
177202

178203
<build>

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
import java.util.concurrent.CompletableFuture;
2323
import java.util.function.Consumer;
2424
import software.amazon.awssdk.annotations.SdkPublicApi;
25-
import software.amazon.awssdk.crt.http.HttpClientConnectionManager;
25+
import software.amazon.awssdk.crt.http.HttpStreamManager;
26+
import software.amazon.awssdk.http.Protocol;
2627
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
2728
import software.amazon.awssdk.http.async.AsyncExecuteRequest;
2829
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
@@ -91,15 +92,15 @@ public CompletableFuture<Void> execute(AsyncExecuteRequest asyncRequest) {
9192
* we have a pool and no one can destroy it underneath us until we've finished submitting the
9293
* request)
9394
*/
94-
try (HttpClientConnectionManager crtConnPool = getOrCreateConnectionPool(poolKey(asyncRequest.request()))) {
95-
CrtAsyncRequestContext context = CrtAsyncRequestContext.builder()
96-
.crtConnPool(crtConnPool)
97-
.readBufferSize(this.readBufferSize)
98-
.request(asyncRequest)
99-
.build();
100-
101-
return new CrtAsyncRequestExecutor().execute(context);
102-
}
95+
HttpStreamManager streamManager = getOrCreateConnectionPool(poolKey(asyncRequest.request()));
96+
CrtAsyncRequestContext context = CrtAsyncRequestContext.builder()
97+
.streamManager(streamManager)
98+
.readBufferSize(this.readBufferSize)
99+
.request(asyncRequest)
100+
.protocol(this.protocol)
101+
.build();
102+
103+
return new CrtAsyncRequestExecutor().execute(context);
103104
}
104105

105106
/**
@@ -224,6 +225,14 @@ AwsCrtAsyncHttpClient.Builder connectionHealthConfiguration(Consumer<ConnectionH
224225
AwsCrtAsyncHttpClient.Builder tcpKeepAliveConfiguration(Consumer<TcpKeepAliveConfiguration.Builder>
225226
tcpKeepAliveConfigurationBuilder);
226227

228+
/**
229+
* Configure the HTTP protocol version to use for connections.
230+
*
231+
* @param protocol the HTTP protocol version
232+
* @return The builder for method chaining.
233+
*/
234+
AwsCrtAsyncHttpClient.Builder protocol(Protocol protocol);
235+
227236
/**
228237
* Configure whether to enable a hybrid post-quantum key exchange option for the Transport Layer Security (TLS) network
229238
* encryption protocol when communicating with services that support Post Quantum TLS. If Post Quantum cipher suites are
@@ -248,6 +257,13 @@ AwsCrtAsyncHttpClient.Builder tcpKeepAliveConfiguration(Consumer<TcpKeepAliveCon
248257
private static final class DefaultAsyncBuilder
249258
extends AwsCrtClientBuilderBase<AwsCrtAsyncHttpClient.Builder> implements Builder {
250259

260+
261+
@Override
262+
public Builder protocol(Protocol protocol) {
263+
getAttributeMap().put(SdkHttpConfigurationOption.PROTOCOL, protocol);
264+
return this;
265+
}
266+
251267
@Override
252268
public SdkAsyncHttpClient build() {
253269
return new AwsCrtAsyncHttpClient(this, getAttributeMap().build()
@@ -260,5 +276,6 @@ public SdkAsyncHttpClient buildWithDefaults(AttributeMap serviceDefaults) {
260276
.merge(serviceDefaults)
261277
.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS));
262278
}
279+
263280
}
264281
}

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@
2323
import java.util.concurrent.CompletionException;
2424
import java.util.function.Consumer;
2525
import software.amazon.awssdk.annotations.SdkPublicApi;
26-
import software.amazon.awssdk.crt.http.HttpClientConnectionManager;
2726
import software.amazon.awssdk.crt.http.HttpException;
27+
import software.amazon.awssdk.crt.http.HttpStreamManager;
2828
import software.amazon.awssdk.http.ExecutableHttpRequest;
2929
import software.amazon.awssdk.http.HttpExecuteRequest;
3030
import software.amazon.awssdk.http.HttpExecuteResponse;
31+
import software.amazon.awssdk.http.Protocol;
3132
import software.amazon.awssdk.http.SdkHttpClient;
3233
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
3334
import software.amazon.awssdk.http.SdkHttpFullResponse;
@@ -56,6 +57,11 @@ public final class AwsCrtHttpClient extends AwsCrtHttpClientBase implements SdkH
5657

5758
private AwsCrtHttpClient(DefaultBuilder builder, AttributeMap config) {
5859
super(builder, config);
60+
if (this.protocol == Protocol.HTTP2) {
61+
throw new UnsupportedOperationException(
62+
"HTTP/2 is not supported for sync HTTP clients. Either use HTTP/1.1 (the default) or use an async "
63+
+ "HTTP client (e.g., AwsCrtAsyncHttpClient).");
64+
}
5965
}
6066

6167
public static AwsCrtHttpClient.Builder builder() {
@@ -91,14 +97,13 @@ public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) {
9197
* we have a pool and no one can destroy it underneath us until we've finished submitting the
9298
* request)
9399
*/
94-
try (HttpClientConnectionManager crtConnPool = getOrCreateConnectionPool(poolKey(request.httpRequest()))) {
95-
CrtRequestContext context = CrtRequestContext.builder()
96-
.crtConnPool(crtConnPool)
97-
.readBufferSize(this.readBufferSize)
98-
.request(request)
99-
.build();
100-
return new CrtHttpRequest(context);
101-
}
100+
HttpStreamManager streamManager = getOrCreateConnectionPool(poolKey(request.httpRequest()));
101+
CrtRequestContext context = CrtRequestContext.builder()
102+
.streamManager(streamManager)
103+
.readBufferSize(this.readBufferSize)
104+
.request(request)
105+
.build();
106+
return new CrtHttpRequest(context);
102107
}
103108

104109
private static final class CrtHttpRequest implements ExecutableHttpRequest {
@@ -140,7 +145,7 @@ public HttpExecuteResponse call() throws IOException {
140145
@Override
141146
public void abort() {
142147
if (responseFuture != null) {
143-
responseFuture.completeExceptionally(new IOException("Request ws cancelled"));
148+
responseFuture.completeExceptionally(new IOException("Request was cancelled"));
144149
}
145150
}
146151
}

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@
2929
import java.util.concurrent.ConcurrentHashMap;
3030
import software.amazon.awssdk.annotations.SdkProtectedApi;
3131
import software.amazon.awssdk.crt.CrtResource;
32-
import software.amazon.awssdk.crt.http.HttpClientConnectionManager;
32+
import software.amazon.awssdk.crt.http.Http2StreamManagerOptions;
3333
import software.amazon.awssdk.crt.http.HttpClientConnectionManagerOptions;
3434
import software.amazon.awssdk.crt.http.HttpMonitoringOptions;
3535
import software.amazon.awssdk.crt.http.HttpProxyOptions;
36+
import software.amazon.awssdk.crt.http.HttpStreamManager;
37+
import software.amazon.awssdk.crt.http.HttpStreamManagerOptions;
38+
import software.amazon.awssdk.crt.http.HttpVersion;
3639
import software.amazon.awssdk.crt.io.ClientBootstrap;
3740
import software.amazon.awssdk.crt.io.SocketOptions;
3841
import software.amazon.awssdk.crt.io.TlsContext;
@@ -58,46 +61,48 @@ abstract class AwsCrtHttpClientBase implements SdkAutoCloseable {
5861
private static final long DEFAULT_STREAM_WINDOW_SIZE = 16L * 1024L * 1024L; // 16 MB
5962

6063
protected final long readBufferSize;
61-
private final Map<URI, HttpClientConnectionManager> connectionPools = new ConcurrentHashMap<>();
64+
protected final Protocol protocol;
65+
private final Map<URI, HttpStreamManager> connectionPools = new ConcurrentHashMap<>();
6266
private final LinkedList<CrtResource> ownedSubResources = new LinkedList<>();
6367
private final ClientBootstrap bootstrap;
6468
private final SocketOptions socketOptions;
6569
private final TlsContext tlsContext;
6670
private final HttpProxyOptions proxyOptions;
6771
private final HttpMonitoringOptions monitoringOptions;
6872
private final long maxConnectionIdleInMilliseconds;
69-
private final int maxConnectionsPerEndpoint;
73+
private final int maxStreamsPerEndpoint;
7074
private final long connectionAcquisitionTimeout;
75+
private final TlsContextOptions tlsContextOptions;
7176
private boolean isClosed = false;
7277

7378
AwsCrtHttpClientBase(AwsCrtClientBuilderBase builder, AttributeMap config) {
74-
if (config.get(PROTOCOL) == Protocol.HTTP2) {
75-
throw new UnsupportedOperationException("HTTP/2 is not supported in AwsCrtHttpClient yet. Use "
76-
+ "NettyNioAsyncHttpClient instead.");
79+
ClientBootstrap clientBootstrap = new ClientBootstrap(null, null);
80+
SocketOptions clientSocketOptions = buildSocketOptions(builder.getTcpKeepAliveConfiguration(),
81+
config.get(SdkHttpConfigurationOption.CONNECTION_TIMEOUT));
82+
TlsContextOptions clientTlsContextOptions =
83+
TlsContextOptions.createDefaultClient()
84+
.withCipherPreference(resolveCipherPreference(builder.getPostQuantumTlsEnabled()))
85+
.withVerifyPeer(!config.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES));
86+
this.protocol = config.get(PROTOCOL);
87+
if (protocol == Protocol.HTTP2) {
88+
clientTlsContextOptions = clientTlsContextOptions.withAlpnList("h2");
7789
}
7890

79-
try (ClientBootstrap clientBootstrap = new ClientBootstrap(null, null);
80-
SocketOptions clientSocketOptions = buildSocketOptions(builder.getTcpKeepAliveConfiguration(),
81-
config.get(SdkHttpConfigurationOption.CONNECTION_TIMEOUT));
82-
TlsContextOptions clientTlsContextOptions =
83-
TlsContextOptions.createDefaultClient()
84-
.withCipherPreference(resolveCipherPreference(builder.getPostQuantumTlsEnabled()))
85-
.withVerifyPeer(!config.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES));
86-
TlsContext clientTlsContext = new TlsContext(clientTlsContextOptions)) {
87-
88-
this.bootstrap = registerOwnedResource(clientBootstrap);
89-
this.socketOptions = registerOwnedResource(clientSocketOptions);
90-
this.tlsContext = registerOwnedResource(clientTlsContext);
91-
this.readBufferSize = builder.getReadBufferSizeInBytes() == null ?
92-
DEFAULT_STREAM_WINDOW_SIZE : builder.getReadBufferSizeInBytes();
93-
this.maxConnectionsPerEndpoint = config.get(SdkHttpConfigurationOption.MAX_CONNECTIONS);
94-
this.monitoringOptions =
95-
resolveHttpMonitoringOptions(builder.getConnectionHealthConfiguration())
96-
.orElseGet(() -> defaultConnectionHealthConfiguration(config));
97-
this.maxConnectionIdleInMilliseconds = config.get(SdkHttpConfigurationOption.CONNECTION_MAX_IDLE_TIMEOUT).toMillis();
98-
this.connectionAcquisitionTimeout = config.get(SdkHttpConfigurationOption.CONNECTION_ACQUIRE_TIMEOUT).toMillis();
99-
this.proxyOptions = resolveProxy(builder.getProxyConfiguration(), tlsContext).orElse(null);
100-
}
91+
this.tlsContextOptions = registerOwnedResource(clientTlsContextOptions);
92+
TlsContext clientTlsContext = new TlsContext(clientTlsContextOptions);
93+
94+
this.bootstrap = registerOwnedResource(clientBootstrap);
95+
this.socketOptions = registerOwnedResource(clientSocketOptions);
96+
this.tlsContext = registerOwnedResource(clientTlsContext);
97+
this.readBufferSize = builder.getReadBufferSizeInBytes() == null ?
98+
DEFAULT_STREAM_WINDOW_SIZE : builder.getReadBufferSizeInBytes();
99+
this.maxStreamsPerEndpoint = config.get(SdkHttpConfigurationOption.MAX_CONNECTIONS);
100+
this.monitoringOptions =
101+
resolveHttpMonitoringOptions(builder.getConnectionHealthConfiguration())
102+
.orElseGet(() -> defaultConnectionHealthConfiguration(config));
103+
this.maxConnectionIdleInMilliseconds = config.get(SdkHttpConfigurationOption.CONNECTION_MAX_IDLE_TIMEOUT).toMillis();
104+
this.connectionAcquisitionTimeout = config.get(SdkHttpConfigurationOption.CONNECTION_ACQUIRE_TIMEOUT).toMillis();
105+
this.proxyOptions = resolveProxy(builder.getProxyConfiguration(), tlsContext).orElse(null);
101106
}
102107

103108
/**
@@ -109,7 +114,6 @@ abstract class AwsCrtHttpClientBase implements SdkAutoCloseable {
109114
*/
110115
private <T extends CrtResource> T registerOwnedResource(T subresource) {
111116
if (subresource != null) {
112-
subresource.addRef();
113117
ownedSubResources.push(subresource);
114118
}
115119
return subresource;
@@ -119,23 +123,46 @@ String clientName() {
119123
return AWS_COMMON_RUNTIME;
120124
}
121125

122-
private HttpClientConnectionManager createConnectionPool(URI uri) {
123-
log.debug(() -> "Creating ConnectionPool for: URI:" + uri + ", MaxConns: " + maxConnectionsPerEndpoint);
126+
private HttpStreamManager createConnectionPool(URI uri) {
127+
log.debug(() ->
128+
String.format("Creating ConnectionPool for: URI:%s, MaxConns: %d, MaxStreams: %d",
129+
uri, maxStreamsPerEndpoint, maxStreamsPerEndpoint));
130+
131+
boolean isHttps = "https".equalsIgnoreCase(uri.getScheme());
132+
TlsContext poolTlsContext = isHttps ? tlsContext : null;
124133

125-
HttpClientConnectionManagerOptions options = new HttpClientConnectionManagerOptions()
134+
HttpClientConnectionManagerOptions h1Options = new HttpClientConnectionManagerOptions()
126135
.withClientBootstrap(bootstrap)
127136
.withSocketOptions(socketOptions)
128-
.withTlsContext(tlsContext)
137+
.withTlsContext(poolTlsContext)
129138
.withUri(uri)
130139
.withWindowSize(readBufferSize)
131-
.withMaxConnections(maxConnectionsPerEndpoint)
140+
.withMaxConnections(maxStreamsPerEndpoint)
132141
.withManualWindowManagement(true)
133142
.withProxyOptions(proxyOptions)
134143
.withMonitoringOptions(monitoringOptions)
135144
.withMaxConnectionIdleInMilliseconds(maxConnectionIdleInMilliseconds)
136145
.withConnectionAcquisitionTimeoutInMilliseconds(connectionAcquisitionTimeout);
137146

138-
return HttpClientConnectionManager.create(options);
147+
HttpStreamManagerOptions options = new HttpStreamManagerOptions()
148+
.withHTTP1ConnectionManagerOptions(h1Options);
149+
150+
if (protocol == Protocol.HTTP2) {
151+
Http2StreamManagerOptions h2Options = new Http2StreamManagerOptions()
152+
.withMaxConcurrentStreams(maxStreamsPerEndpoint)
153+
.withConnectionManagerOptions(h1Options);
154+
155+
if (!isHttps) {
156+
h2Options.withPriorKnowledge(true);
157+
}
158+
159+
options.withHTTP2StreamManagerOptions(h2Options);
160+
options.withExpectedProtocol(HttpVersion.HTTP_2);
161+
} else {
162+
options.withExpectedProtocol(HttpVersion.HTTP_1_1);
163+
}
164+
165+
return HttpStreamManager.create(options);
139166
}
140167

141168
/*
@@ -153,14 +180,13 @@ private HttpClientConnectionManager createConnectionPool(URI uri) {
153180
* existing pool. If we add all of execute() to the scope, we include, at minimum a JNI call to the native
154181
* pool implementation.
155182
*/
156-
HttpClientConnectionManager getOrCreateConnectionPool(URI uri) {
183+
HttpStreamManager getOrCreateConnectionPool(URI uri) {
157184
synchronized (this) {
158185
if (isClosed) {
159186
throw new IllegalStateException("Client is closed. No more requests can be made with this client.");
160187
}
161188

162-
HttpClientConnectionManager connPool = connectionPools.computeIfAbsent(uri, this::createConnectionPool);
163-
connPool.addRef();
189+
HttpStreamManager connPool = connectionPools.computeIfAbsent(uri, this::createConnectionPool);
164190
return connPool;
165191
}
166192
}

0 commit comments

Comments
 (0)