Skip to content

Commit 2af287f

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 2af287f

File tree

5 files changed

+322
-8
lines changed

5 files changed

+322
-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: 118 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import software.amazon.awssdk.utils.AttributeMap;
4747
import software.amazon.awssdk.utils.IoUtils;
4848
import software.amazon.awssdk.utils.Logger;
49+
import software.amazon.awssdk.utils.NumericUtils;
4950
import software.amazon.awssdk.utils.Validate;
5051

5152
/**
@@ -76,15 +77,27 @@ public final class AwsCrtAsyncHttpClient implements SdkAsyncHttpClient {
7677
private final int maxConnectionsPerEndpoint;
7778
private boolean isClosed = false;
7879

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

8291
Validate.isPositive(maxConns, "maxConns");
8392
Validate.notNull(builder.cipherPreference, "cipherPreference");
8493
Validate.isPositive(builder.readBufferSize, "readBufferSize");
8594

95+
if (Boolean.TRUE.equals(builder.standardOptions.get(SdkHttpConfigurationOption.TCP_KEEPALIVE))) {
96+
Validate.notNull(builder.tcpKeepAliveConfiguration, "tcpKeepAliveConfiguration must be provided when tcpKeepAlive is enabled");
97+
}
98+
8699
try (ClientBootstrap clientBootstrap = new ClientBootstrap(null, null);
87-
SocketOptions clientSocketOptions = new SocketOptions();
100+
SocketOptions clientSocketOptions = buildSocketOptions(builder, config);
88101
TlsContextOptions clientTlsContextOptions = TlsContextOptions.createDefaultClient() // NOSONAR
89102
.withCipherPreference(builder.cipherPreference)
90103
.withVerifyPeer(!config.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES));
@@ -138,6 +151,24 @@ private HttpProxyOptions buildProxyOptions(ProxyConfiguration proxyConfiguration
138151
return clientProxyOptions;
139152
}
140153

154+
private SocketOptions buildSocketOptions(DefaultBuilder builder, AttributeMap config) {
155+
SocketOptions clientSocketOptions = new SocketOptions();
156+
157+
Duration connectionTimeout = config.get(SdkHttpConfigurationOption.CONNECTION_TIMEOUT);
158+
if (connectionTimeout != null) {
159+
clientSocketOptions.connectTimeoutMs = NumericUtils.saturatedCast(connectionTimeout.toMillis());
160+
}
161+
162+
TcpKeepAliveConfiguration tcpKeepAliveConfiguration = builder.tcpKeepAliveConfiguration;
163+
if (tcpKeepAliveConfiguration != null) {
164+
clientSocketOptions.keepAliveIntervalSecs = NumericUtils.saturatedCast(tcpKeepAliveConfiguration.keepAliveInterval().getSeconds());
165+
clientSocketOptions.keepAliveTimeoutSecs = NumericUtils.saturatedCast(tcpKeepAliveConfiguration.keepAliveTimeout().getSeconds());
166+
167+
}
168+
169+
return clientSocketOptions;
170+
}
171+
141172
/**
142173
* Marks a Native CrtResource as owned by the current Java Object.
143174
*
@@ -312,10 +343,10 @@ public interface Builder extends SdkAsyncHttpClient.Builder<AwsCrtAsyncHttpClien
312343
Builder proxyConfiguration(Consumer<ProxyConfiguration.Builder> proxyConfigurationBuilderConsumer);
313344

314345
/**
315-
* Configure the health checks for for all connections established by this client.
346+
* Configure the health checks for all connections established by this client.
316347
*
317348
* <p>
318-
* eg: you can set a throughput threshold for the a connection to be considered healthy.
349+
* eg: you can set a throughput threshold for a connection to be considered healthy.
319350
* If the connection falls below this threshold for a configurable amount of time,
320351
* then the connection is considered unhealthy and will be shut down.
321352
*
@@ -325,10 +356,10 @@ public interface Builder extends SdkAsyncHttpClient.Builder<AwsCrtAsyncHttpClien
325356
Builder connectionHealthChecksConfiguration(ConnectionHealthChecksConfiguration healthChecksConfiguration);
326357

327358
/**
328-
* A convenience method to configure the health checks for for all connections established by this client.
359+
* A convenience method to configure the health checks for all connections established by this client.
329360
*
330361
* <p>
331-
* eg: you can set a throughput threshold for the a connection to be considered healthy.
362+
* eg: you can set a throughput threshold for a connection to be considered healthy.
332363
* If the connection falls below this threshold for a configurable amount of time,
333364
* then the connection is considered unhealthy and will be shut down.
334365
*
@@ -340,9 +371,58 @@ Builder connectionHealthChecksConfiguration(Consumer<ConnectionHealthChecksConfi
340371
healthChecksConfigurationBuilder);
341372

342373
/**
343-
* Configure the maximum amount of time that a connection should be allowed to remain open while idle.
374+
* The amount of time to wait when initially establishing a connection before giving up and timing out. The maximum
375+
* possible value, in ms, is the value of {@link Integer#MAX_VALUE}, any longer duration will be reduced to the maximum
376+
* possible value. If not specified, the connection timeout duration will be set to value defined in
377+
* {@link AwsCrtAsyncHttpClient#CRT_SDK_DEFAULT_CONNECTION_TIMEOUT}.
344378
*/
345379
Builder connectionMaxIdleTime(Duration connectionMaxIdleTime);
380+
381+
/**
382+
* Configure connection socket timeout
383+
*/
384+
Builder connectionTimeout(Duration connectionTimeout);
385+
386+
/**
387+
* Configure whether to enable or disable TCP KeepAlive.
388+
* The configuration will be passed to the socket option {@link java.net.SocketOptions#SO_KEEPALIVE}.
389+
* <p>
390+
* By default, this is disabled.
391+
* <p>
392+
* When enabled, the actual KeepAlive mechanism is dependent on the Operating System and therefore additional TCP
393+
* KeepAlive values (like timeout, number of packets, etc) must be configured via the Operating System (sysctl on
394+
* Linux/Mac, and Registry values on Windows).
395+
*
396+
* To enable, keepAlive interval and timeout must be additionally configured via {@link TcpKeepAliveConfiguration}
397+
*/
398+
Builder tcpKeepAlive(Boolean keepConnectionAlive);
399+
400+
/**
401+
* Configure TCP Keep-alive configuration for all connections established by this client.
402+
*
403+
* <p>
404+
* If tcpKeepAlive is enabled, this is required configuration
405+
* and specify periodic keepalive packet intervals and including timeouts
406+
* This may be required for certain connections for longer durations than default socket timeouts
407+
*
408+
* @param tcpKeepAliveConfiguration The TCP keep-alive configuration to use
409+
* @return The builder of the method chaining.
410+
*/
411+
Builder tcpKeepAliveConfiguration(TcpKeepAliveConfiguration tcpKeepAliveConfiguration);
412+
413+
/**
414+
* Configure TCP Keep-alive configuration for all connections established by this client.
415+
*
416+
* <p>
417+
* If tcpKeepAlive is enabled, this is required configuration
418+
* and specify periodic keepalive packet intervals and including timeouts
419+
* This may be required for certain connections for longer durations than default socket timeouts
420+
*
421+
* @param tcpKeepAliveConfigurationBuilder The TCP keep-alive configuration builder to use
422+
* @return The builder of the method chaining.
423+
*/
424+
Builder tcpKeepAliveConfiguration(Consumer<TcpKeepAliveConfiguration.Builder>
425+
tcpKeepAliveConfigurationBuilder);
346426
}
347427

348428
/**
@@ -355,20 +435,23 @@ private static final class DefaultBuilder implements Builder {
355435
private int readBufferSize = DEFAULT_STREAM_WINDOW_SIZE;
356436
private ProxyConfiguration proxyConfiguration;
357437
private ConnectionHealthChecksConfiguration connectionHealthChecksConfiguration;
438+
private TcpKeepAliveConfiguration tcpKeepAliveConfiguration;
358439

359440
private DefaultBuilder() {
360441
}
361442

362443
@Override
363444
public SdkAsyncHttpClient build() {
364445
return new AwsCrtAsyncHttpClient(this, standardOptions.build()
446+
.merge(CRT_HTTP_DEFAULTS)
365447
.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS));
366448
}
367449

368450
@Override
369451
public SdkAsyncHttpClient buildWithDefaults(AttributeMap serviceDefaults) {
370452
return new AwsCrtAsyncHttpClient(this, standardOptions.build()
371453
.merge(serviceDefaults)
454+
.merge(CRT_HTTP_DEFAULTS)
372455
.merge(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS));
373456
}
374457

@@ -417,10 +500,39 @@ public Builder connectionHealthChecksConfiguration(Consumer<ConnectionHealthChec
417500

418501
@Override
419502
public Builder connectionMaxIdleTime(Duration connectionMaxIdleTime) {
503+
Validate.isPositive(connectionMaxIdleTime, "connectionMaxIdleTime");
420504
standardOptions.put(SdkHttpConfigurationOption.CONNECTION_MAX_IDLE_TIMEOUT, connectionMaxIdleTime);
421505
return this;
422506
}
423507

508+
@Override
509+
public Builder connectionTimeout(Duration connectionTimeout) {
510+
Validate.isPositive(connectionTimeout, "connectionTimeout");
511+
standardOptions.put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, connectionTimeout);
512+
return this;
513+
}
514+
515+
@Override
516+
public Builder tcpKeepAlive(Boolean keepConnectionAlive) {
517+
Validate.notNull(keepConnectionAlive, "keepConnectionAlive");
518+
standardOptions.put(SdkHttpConfigurationOption.TCP_KEEPALIVE, keepConnectionAlive);
519+
return this;
520+
}
521+
522+
@Override
523+
public Builder tcpKeepAliveConfiguration(TcpKeepAliveConfiguration tcpKeepAliveConfiguration) {
524+
this.tcpKeepAliveConfiguration = tcpKeepAliveConfiguration;
525+
return this;
526+
}
527+
528+
@Override
529+
public Builder tcpKeepAliveConfiguration(Consumer<TcpKeepAliveConfiguration.Builder>
530+
tcpKeepAliveConfigurationBuilder) {
531+
TcpKeepAliveConfiguration.Builder builder = TcpKeepAliveConfiguration.builder();
532+
tcpKeepAliveConfigurationBuilder.accept(builder);
533+
return tcpKeepAliveConfiguration(builder.build());
534+
}
535+
424536
@Override
425537
public Builder proxyConfiguration(Consumer<ProxyConfiguration.Builder> proxyConfigurationBuilderConsumer) {
426538
ProxyConfiguration.Builder builder = ProxyConfiguration.builder();

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: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 java.time.Duration;
19+
import software.amazon.awssdk.annotations.SdkPreviewApi;
20+
import software.amazon.awssdk.annotations.SdkPublicApi;
21+
import software.amazon.awssdk.utils.Validate;
22+
23+
/**
24+
* Configuration that defines keep-alive options for all connections established by
25+
* the {@link TcpKeepAliveConfiguration}.
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 TcpKeepAliveConfiguration {
32+
33+
private final Duration keepAliveInterval;
34+
private final Duration keepAliveTimeout;
35+
36+
private TcpKeepAliveConfiguration(DefaultTcpKeepAliveConfigurationBuilder builder) {
37+
this.keepAliveInterval = Validate.isPositive(builder.keepAliveInterval,
38+
"keepAliveInterval");
39+
this.keepAliveTimeout = Validate.isPositive(builder.keepAliveTimeout,
40+
"keepAliveTimeout");
41+
}
42+
43+
/**
44+
* @return number of seconds between TCP keepalive packets being sent to the peer
45+
*/
46+
public Duration keepAliveInterval() {
47+
return keepAliveInterval;
48+
}
49+
50+
/**
51+
* @return number of seconds to wait for a keepalive response before considering the connection timed out
52+
*/
53+
public Duration keepAliveTimeout() {
54+
return keepAliveTimeout;
55+
}
56+
57+
public static Builder builder() {
58+
return new DefaultTcpKeepAliveConfigurationBuilder();
59+
}
60+
61+
/**
62+
* A builder for {@link TcpKeepAliveConfiguration}.
63+
*
64+
* <p>All implementations of this interface are mutable and not thread safe.</p>
65+
*/
66+
public interface Builder {
67+
/**
68+
* Sets the Duration between TCP keepalive packets being sent to the peer
69+
* @param keepAliveInterval Duration between TCP keepalive packets being sent to the peer
70+
* @return Builder
71+
*/
72+
Builder keepAliveInterval(Duration keepAliveInterval);
73+
74+
/**
75+
* Sets the Duration to wait for a keepalive response before considering the connection timed out
76+
* @param keepAliveTimeout Duration to wait for a keepalive response before considering the connection timed out
77+
* @return Builder
78+
*/
79+
Builder keepAliveTimeout(Duration keepAliveTimeout);
80+
81+
TcpKeepAliveConfiguration build();
82+
}
83+
84+
/**
85+
* An SDK-internal implementation of {@link Builder}.
86+
*/
87+
private static final class DefaultTcpKeepAliveConfigurationBuilder implements Builder {
88+
private Duration keepAliveInterval;
89+
private Duration keepAliveTimeout;
90+
91+
private DefaultTcpKeepAliveConfigurationBuilder() {
92+
}
93+
94+
/**
95+
* Sets the Duration between TCP keepalive packets being sent to the peer
96+
* @param keepAliveInterval Duration between TCP keepalive packets being sent to the peer
97+
* @return Builder
98+
*/
99+
@Override
100+
public Builder keepAliveInterval(Duration keepAliveInterval) {
101+
this.keepAliveInterval = keepAliveInterval;
102+
return this;
103+
}
104+
105+
/**
106+
* Sets the Duration to wait for a keepalive response before considering the connection timed out
107+
* @param keepAliveTimeout Duration to wait for a keepalive response before considering the connection timed out
108+
* @return Builder
109+
*/
110+
@Override
111+
public Builder keepAliveTimeout(Duration keepAliveTimeout) {
112+
this.keepAliveTimeout = keepAliveTimeout;
113+
return this;
114+
}
115+
116+
@Override
117+
public TcpKeepAliveConfiguration build() {
118+
return new TcpKeepAliveConfiguration(this);
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)