Skip to content

Commit 3f0ec56

Browse files
committed
Add the ability to configure connection timeout, keep-alive settings, and advanced SocketOptions on the AwsCrtAsyncHttpClient. This is necessary for any clients with long-running connections that exceed default socket timeouts of services along the call path, and need to enable keep-alive settings which the CRT client supports, but the Java client wasn't exposing to callers
1 parent d6b02d5 commit 3f0ec56

File tree

7 files changed

+535
-8
lines changed

7 files changed

+535
-8
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 SDK for Java v2",
4+
"contributor": "nikp",
5+
"description": "Add the ability to configure connection timeout, keep-alive settings, and other advanced SocketOptions on the AwsCrtAsyncHttpClient to support long-running connections"
6+
}

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

Lines changed: 160 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,27 @@ public final class AwsCrtAsyncHttpClient implements SdkAsyncHttpClient {
7676
private final int maxConnectionsPerEndpoint;
7777
private boolean isClosed = false;
7878

79+
private static final Duration CRT_SDK_DEFAULT_CONNECTION_TIMEOUT = Duration.ofSeconds(3);
80+
// Override default connection timeout for Crt client to be in line with the CRT default:
81+
// https://github.com/awslabs/aws-crt-java/blob/main/src/main/java/software/amazon/awssdk/crt/io/SocketOptions.java#L79
82+
private static final AttributeMap CRT_HTTP_DEFAULTS =
83+
AttributeMap.builder()
84+
.put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, CRT_SDK_DEFAULT_CONNECTION_TIMEOUT)
85+
.build();
86+
7987
private AwsCrtAsyncHttpClient(DefaultBuilder builder, AttributeMap config) {
8088
int maxConns = config.get(SdkHttpConfigurationOption.MAX_CONNECTIONS);
8189

8290
Validate.isPositive(maxConns, "maxConns");
8391
Validate.notNull(builder.cipherPreference, "cipherPreference");
8492
Validate.isPositive(builder.readBufferSize, "readBufferSize");
8593

94+
if (Boolean.TRUE.equals(builder.standardOptions.get(SdkHttpConfigurationOption.TCP_KEEPALIVE))) {
95+
Validate.notNull(builder.tcpKeepAliveConfiguration, "tcpKeepAliveConfiguration must be provided when tcpKeepAlive is enabled");
96+
}
97+
8698
try (ClientBootstrap clientBootstrap = new ClientBootstrap(null, null);
87-
SocketOptions clientSocketOptions = new SocketOptions();
99+
SocketOptions clientSocketOptions = buildSocketOptions(builder, config);
88100
TlsContextOptions clientTlsContextOptions = TlsContextOptions.createDefaultClient() // NOSONAR
89101
.withCipherPreference(builder.cipherPreference)
90102
.withVerifyPeer(!config.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES));
@@ -138,6 +150,34 @@ private HttpProxyOptions buildProxyOptions(ProxyConfiguration proxyConfiguration
138150
return clientProxyOptions;
139151
}
140152

153+
private SocketOptions buildSocketOptions(DefaultBuilder builder, AttributeMap config) {
154+
SocketOptions clientSocketOptions = new SocketOptions();
155+
156+
Duration connectionTimeout = config.get(SdkHttpConfigurationOption.CONNECTION_TIMEOUT);
157+
if (connectionTimeout != null) {
158+
clientSocketOptions.connectTimeoutMs = (int) Long.min(connectionTimeout.toMillis(), Integer.MAX_VALUE);
159+
}
160+
161+
TcpKeepAliveConfiguration tcpKeepAliveConfiguration = builder.tcpKeepAliveConfiguration;
162+
if (tcpKeepAliveConfiguration != null) {
163+
long keepAliveIntervalSecs = tcpKeepAliveConfiguration.keepAliveInterval().getSeconds();
164+
long keepAliveTimeoutSecs = tcpKeepAliveConfiguration.keepAliveTimeout().getSeconds();
165+
166+
clientSocketOptions.keepAliveIntervalSecs = (int) Long.min(keepAliveIntervalSecs, Integer.MAX_VALUE);
167+
clientSocketOptions.keepAliveTimeoutSecs = (int) Long.min(keepAliveTimeoutSecs, Integer.MAX_VALUE);
168+
169+
}
170+
171+
SocketOptionsConfiguration socketOptionsConfiguration = builder.socketOptionsConfiguration;
172+
if (socketOptionsConfiguration != null) {
173+
clientSocketOptions.domain = socketOptionsConfiguration.domain();
174+
clientSocketOptions.type = socketOptionsConfiguration.type();
175+
}
176+
177+
178+
return clientSocketOptions;
179+
}
180+
141181
/**
142182
* Marks a Native CrtResource as owned by the current Java Object.
143183
*
@@ -312,10 +352,10 @@ public interface Builder extends SdkAsyncHttpClient.Builder<AwsCrtAsyncHttpClien
312352
Builder proxyConfiguration(Consumer<ProxyConfiguration.Builder> proxyConfigurationBuilderConsumer);
313353

314354
/**
315-
* Configure the health checks for for all connections established by this client.
355+
* Configure the health checks for all connections established by this client.
316356
*
317357
* <p>
318-
* eg: you can set a throughput threshold for the a connection to be considered healthy.
358+
* eg: you can set a throughput threshold for a connection to be considered healthy.
319359
* If the connection falls below this threshold for a configurable amount of time,
320360
* then the connection is considered unhealthy and will be shut down.
321361
*
@@ -325,10 +365,10 @@ public interface Builder extends SdkAsyncHttpClient.Builder<AwsCrtAsyncHttpClien
325365
Builder connectionHealthChecksConfiguration(ConnectionHealthChecksConfiguration healthChecksConfiguration);
326366

327367
/**
328-
* A convenience method to configure the health checks for for all connections established by this client.
368+
* A convenience method to configure the health checks for all connections established by this client.
329369
*
330370
* <p>
331-
* eg: you can set a throughput threshold for the a connection to be considered healthy.
371+
* eg: you can set a throughput threshold for a connection to be considered healthy.
332372
* If the connection falls below this threshold for a configurable amount of time,
333373
* then the connection is considered unhealthy and will be shut down.
334374
*
@@ -340,9 +380,75 @@ Builder connectionHealthChecksConfiguration(Consumer<ConnectionHealthChecksConfi
340380
healthChecksConfigurationBuilder);
341381

342382
/**
343-
* Configure the maximum amount of time that a connection should be allowed to remain open while idle.
383+
* The amount of time to wait when initially establishing a connection before giving up and timing out. The maximum
384+
* possible value, in ms, is the value of {@link Integer#MAX_VALUE}, any longer duration will be reduced to the maximum
385+
* possible value. If not specified, the connection timeout duration will be set to value defined in
386+
* {@link AwsCrtAsyncHttpClient#CRT_SDK_DEFAULT_CONNECTION_TIMEOUT}.
344387
*/
345388
Builder connectionMaxIdleTime(Duration connectionMaxIdleTime);
389+
390+
/**
391+
* Configure connection socket timeout
392+
*/
393+
Builder connectionTimeout(Duration connectionTimeout);
394+
395+
/**
396+
* Configure whether to enable or disable TCP KeepAlive.
397+
* The configuration will be passed to the socket option {@link java.net.SocketOptions#SO_KEEPALIVE}.
398+
* <p>
399+
* By default, this is disabled.
400+
* <p>
401+
* When enabled, the actual KeepAlive mechanism is dependent on the Operating System and therefore additional TCP
402+
* KeepAlive values (like timeout, number of packets, etc) must be configured via the Operating System (sysctl on
403+
* Linux/Mac, and Registry values on Windows).
404+
*
405+
* To enable, keepAlive interval and timeout must be additionally configured via {@link TcpKeepAliveConfiguration}
406+
*/
407+
Builder tcpKeepAlive(Boolean keepConnectionAlive);
408+
409+
/**
410+
* Configure TCP Keep-alive configuration for all connections established by this client.
411+
*
412+
* <p>
413+
* If tcpKeepAlive is enabled, this is required configuration
414+
* and specify periodic keepalive packet intervals and including timeouts
415+
* This may be required for certain connections for longer durations than default socket timeouts
416+
*
417+
* @param tcpKeepAliveConfiguration The TCP keep-alive configuration to use
418+
* @return The builder of the method chaining.
419+
*/
420+
Builder tcpKeepAliveConfiguration(TcpKeepAliveConfiguration tcpKeepAliveConfiguration);
421+
422+
/**
423+
* Configure TCP Keep-alive configuration for all connections established by this client.
424+
*
425+
* <p>
426+
* If tcpKeepAlive is enabled, this is required configuration
427+
* and specify periodic keepalive packet intervals and including timeouts
428+
* This may be required for certain connections for longer durations than default socket timeouts
429+
*
430+
* @param tcpKeepAliveConfigurationBuilder The TCP keep-alive configuration builder to use
431+
* @return The builder of the method chaining.
432+
*/
433+
Builder tcpKeepAliveConfiguration(Consumer<TcpKeepAliveConfiguration.Builder>
434+
tcpKeepAliveConfigurationBuilder);
435+
436+
/**
437+
* Configure socket options configuration for alternative transports via socket domains and ty pes
438+
*
439+
* @param socketOptionsConfiguration The socket configuration to use
440+
* @return The builder of the method chaining.
441+
*/
442+
Builder socketOptionsConfiguration(SocketOptionsConfiguration socketOptionsConfiguration);
443+
444+
/**
445+
* Configure socket options configuration for alternative transports via socket domains and ty pes
446+
*
447+
* @param socketOptionsConfigurationBuilder The socket configuration builder to use
448+
* @return The builder of the method chaining.
449+
*/
450+
Builder socketOptionsConfiguration(Consumer<SocketOptionsConfiguration.Builder>
451+
socketOptionsConfigurationBuilder);
346452
}
347453

348454
/**
@@ -355,20 +461,24 @@ private static final class DefaultBuilder implements Builder {
355461
private int readBufferSize = DEFAULT_STREAM_WINDOW_SIZE;
356462
private ProxyConfiguration proxyConfiguration;
357463
private ConnectionHealthChecksConfiguration connectionHealthChecksConfiguration;
464+
private TcpKeepAliveConfiguration tcpKeepAliveConfiguration;
465+
private SocketOptionsConfiguration socketOptionsConfiguration;
358466

359467
private DefaultBuilder() {
360468
}
361469

362470
@Override
363471
public SdkAsyncHttpClient build() {
364472
return new AwsCrtAsyncHttpClient(this, standardOptions.build()
473+
.merge(CRT_HTTP_DEFAULTS)
365474
.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS));
366475
}
367476

368477
@Override
369478
public SdkAsyncHttpClient buildWithDefaults(AttributeMap serviceDefaults) {
370479
return new AwsCrtAsyncHttpClient(this, standardOptions.build()
371480
.merge(serviceDefaults)
481+
.merge(CRT_HTTP_DEFAULTS)
372482
.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS));
373483
}
374484

@@ -417,15 +527,59 @@ public Builder connectionHealthChecksConfiguration(Consumer<ConnectionHealthChec
417527

418528
@Override
419529
public Builder connectionMaxIdleTime(Duration connectionMaxIdleTime) {
530+
Validate.isPositive(connectionMaxIdleTime, "connectionMaxIdleTime");
420531
standardOptions.put(SdkHttpConfigurationOption.CONNECTION_MAX_IDLE_TIMEOUT, connectionMaxIdleTime);
421532
return this;
422533
}
423534

535+
@Override
536+
public Builder connectionTimeout(Duration connectionTimeout) {
537+
Validate.isPositive(connectionTimeout, "connectionTimeout");
538+
standardOptions.put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, connectionTimeout);
539+
return this;
540+
}
541+
542+
@Override
543+
public Builder tcpKeepAlive(Boolean keepConnectionAlive) {
544+
Validate.notNull(keepConnectionAlive, "keepConnectionAlive");
545+
standardOptions.put(SdkHttpConfigurationOption.TCP_KEEPALIVE, keepConnectionAlive);
546+
return this;
547+
}
548+
549+
@Override
550+
public Builder tcpKeepAliveConfiguration(TcpKeepAliveConfiguration tcpKeepAliveConfiguration) {
551+
this.tcpKeepAliveConfiguration = tcpKeepAliveConfiguration;
552+
return this;
553+
}
554+
555+
@Override
556+
public Builder tcpKeepAliveConfiguration(Consumer<TcpKeepAliveConfiguration.Builder>
557+
tcpKeepAliveConfigurationBuilder) {
558+
TcpKeepAliveConfiguration.Builder builder = TcpKeepAliveConfiguration.builder();
559+
tcpKeepAliveConfigurationBuilder.accept(builder);
560+
return tcpKeepAliveConfiguration(builder.build());
561+
}
562+
424563
@Override
425564
public Builder proxyConfiguration(Consumer<ProxyConfiguration.Builder> proxyConfigurationBuilderConsumer) {
426565
ProxyConfiguration.Builder builder = ProxyConfiguration.builder();
427566
proxyConfigurationBuilderConsumer.accept(builder);
428567
return proxyConfiguration(builder.build());
429568
}
569+
570+
571+
@Override
572+
public Builder socketOptionsConfiguration(SocketOptionsConfiguration socketOptionsConfiguration) {
573+
this.socketOptionsConfiguration = socketOptionsConfiguration;
574+
return this;
575+
}
576+
577+
@Override
578+
public Builder socketOptionsConfiguration(Consumer<SocketOptionsConfiguration.Builder>
579+
socketOptionsConfigurationBuilder) {
580+
SocketOptionsConfiguration.Builder builder = SocketOptionsConfiguration.builder();
581+
socketOptionsConfigurationBuilder.accept(builder);
582+
return socketOptionsConfiguration(builder.build());
583+
}
430584
}
431585
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
import software.amazon.awssdk.utils.Validate;
2222

2323
/**
24-
* Configuration that defines health checks for for all connections established by
25-
* the{@link ConnectionHealthChecksConfiguration}.
24+
* Configuration that defines health checks for all connections established by
25+
* the {@link ConnectionHealthChecksConfiguration}.
2626
*
2727
* <b>NOTE:</b> This is a Preview API and is subject to change so it should not be used in production.
2828
*/
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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.crt;
17+
18+
import software.amazon.awssdk.annotations.SdkPreviewApi;
19+
import software.amazon.awssdk.annotations.SdkPublicApi;
20+
import software.amazon.awssdk.crt.io.SocketOptions;
21+
import software.amazon.awssdk.utils.Validate;
22+
23+
/**
24+
* Configuration that defines socket options for all connections established by
25+
* the {@link SocketOptionsConfiguration}.
26+
*
27+
* <b>NOTE:</b> This is a Preview API and is subject to change so it should not be used in production.
28+
*/
29+
@SdkPublicApi
30+
@SdkPreviewApi
31+
public final class SocketOptionsConfiguration {
32+
33+
private final SocketOptions.SocketDomain domain;
34+
private final SocketOptions.SocketType type;
35+
36+
private SocketOptionsConfiguration(DefaultSocketOptionsConfigurationBuilder builder) {
37+
this.domain = Validate.paramNotNull(builder.domain,
38+
"domain");
39+
this.type = Validate.paramNotNull(builder.type,
40+
"type");
41+
}
42+
43+
/**
44+
* @return socket domain
45+
*/
46+
public SocketOptions.SocketDomain domain() {
47+
return domain;
48+
}
49+
50+
/**
51+
* @return socket type
52+
*/
53+
public SocketOptions.SocketType type() {
54+
return type;
55+
}
56+
57+
public static Builder builder() {
58+
return new DefaultSocketOptionsConfigurationBuilder();
59+
}
60+
61+
/**
62+
* A builder for {@link SocketOptionsConfiguration}.
63+
*
64+
* <p>All implementations of this interface are mutable and not thread safe.</p>
65+
*/
66+
public interface Builder {
67+
68+
/**
69+
* Sets the socket domain
70+
* @param domain socket domain
71+
* @return Builder
72+
*/
73+
Builder domain(SocketOptions.SocketDomain domain);
74+
75+
/**
76+
* Sets the socket type
77+
* @param type socket type
78+
* @return Builder
79+
*/
80+
Builder type(SocketOptions.SocketType type);
81+
82+
SocketOptionsConfiguration build();
83+
}
84+
85+
/**
86+
* An SDK-internal implementation of {@link Builder}.
87+
*/
88+
private static final class DefaultSocketOptionsConfigurationBuilder implements Builder {
89+
90+
private SocketOptions.SocketDomain domain = SocketOptions.SocketDomain.IPv6;
91+
private SocketOptions.SocketType type = SocketOptions.SocketType.STREAM;
92+
93+
private DefaultSocketOptionsConfigurationBuilder() {
94+
}
95+
96+
/**
97+
* Sets the socket domain
98+
* Default: {@link SocketOptions.SocketDomain#IPv6}
99+
* @param domain socket domain
100+
* @return Builder
101+
*/
102+
@Override
103+
public Builder domain(SocketOptions.SocketDomain domain) {
104+
this.domain = domain;
105+
return this;
106+
}
107+
108+
/**
109+
* Sets the socket type
110+
* Default: {@link SocketOptions.SocketType#STREAM}
111+
* @param type socket type
112+
* @return Builder
113+
*/
114+
@Override
115+
public Builder type(SocketOptions.SocketType type) {
116+
this.type = type;
117+
return this;
118+
}
119+
120+
@Override
121+
public SocketOptionsConfiguration build() {
122+
return new SocketOptionsConfiguration(this);
123+
}
124+
}
125+
}

0 commit comments

Comments
 (0)