Skip to content

Commit a5cdcb8

Browse files
authored
Enable default connection health monitoring for CRT HTTP clients (#6818)
1 parent e51eb92 commit a5cdcb8

File tree

7 files changed

+181
-7
lines changed

7 files changed

+181
-7
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "AWS CRT HTTP Client",
4+
"contributor": "",
5+
"description": "Enabled default connection health monitoring for the AWS CRT HTTP client. Connections that remain stalled below 1 byte per second for the duration the read/write timeout (default 30 seconds) are now automatically terminated. This behavior can be overridden via ConnectionHealthConfiguration."
6+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,10 @@ public interface Builder extends SdkAsyncHttpClient.Builder<AwsCrtAsyncHttpClien
154154
* then the connection is considered unhealthy and will be shut down.
155155
*
156156
* <p>
157-
* By default, monitoring options are disabled. You can enable {@code healthChecks} by providing this configuration
158-
* and specifying the options for monitoring for the connection manager.
157+
* If not explicitly configured, a default health configuration is applied with a minimum throughput of 1 byte per
158+
* second and a throughput failure interval of 30 seconds. The failure interval is derived from the read/write timeout
159+
* settings and will change if those are overridden by service specific defaults.
160+
*
159161
* @param healthChecksConfiguration The health checks config to use
160162
* @return The builder of the method chaining.
161163
*/

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,10 @@ public interface Builder extends SdkHttpClient.Builder<AwsCrtHttpClient.Builder>
193193
* then the connection is considered unhealthy and will be shut down.
194194
*
195195
* <p>
196-
* By default, monitoring options are disabled. You can enable {@code healthChecks} by providing this configuration
197-
* and specifying the options for monitoring for the connection manager.
196+
* If not explicitly configured, a default health configuration is applied with a minimum throughput of 1 byte per
197+
* second and a throughput failure interval of 30 seconds. The failure interval is derived from the read/write timeout
198+
* settings and will change if those are overridden by service specific defaults.
199+
*
198200
* @param healthChecksConfiguration The health checks config to use
199201
* @return The builder of the method chaining.
200202
*/

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static software.amazon.awssdk.crtcore.CrtConfigurationUtils.resolveProxy;
2020
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.PROTOCOL;
2121
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildSocketOptions;
22+
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.defaultConnectionHealthConfiguration;
2223
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveCipherPreference;
2324
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
2425

@@ -90,7 +91,9 @@ abstract class AwsCrtHttpClientBase implements SdkAutoCloseable {
9091
this.readBufferSize = builder.getReadBufferSizeInBytes() == null ?
9192
DEFAULT_STREAM_WINDOW_SIZE : builder.getReadBufferSizeInBytes();
9293
this.maxConnectionsPerEndpoint = config.get(SdkHttpConfigurationOption.MAX_CONNECTIONS);
93-
this.monitoringOptions = resolveHttpMonitoringOptions(builder.getConnectionHealthConfiguration()).orElse(null);
94+
this.monitoringOptions =
95+
resolveHttpMonitoringOptions(builder.getConnectionHealthConfiguration())
96+
.orElseGet(() -> defaultConnectionHealthConfiguration(config));
9497
this.maxConnectionIdleInMilliseconds = config.get(SdkHttpConfigurationOption.CONNECTION_MAX_IDLE_TIMEOUT).toMillis();
9598
this.connectionAcquisitionTimeout = config.get(SdkHttpConfigurationOption.CONNECTION_ACQUIRE_TIMEOUT).toMillis();
9699
this.proxyOptions = resolveProxy(builder.getProxyConfiguration(), tlsContext).orElse(null);

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818

1919
import java.time.Duration;
2020
import software.amazon.awssdk.annotations.SdkInternalApi;
21+
import software.amazon.awssdk.crt.http.HttpMonitoringOptions;
2122
import software.amazon.awssdk.crt.io.SocketOptions;
2223
import software.amazon.awssdk.crt.io.TlsCipherPreference;
24+
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
2325
import software.amazon.awssdk.http.crt.AwsCrtAsyncHttpClient;
2426
import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration;
27+
import software.amazon.awssdk.utils.AttributeMap;
2528
import software.amazon.awssdk.utils.Logger;
2629
import software.amazon.awssdk.utils.NumericUtils;
2730

@@ -70,4 +73,14 @@ public static TlsCipherPreference resolveCipherPreference(Boolean postQuantumTls
7073
return pqTls;
7174
}
7275

76+
public static HttpMonitoringOptions defaultConnectionHealthConfiguration(AttributeMap config) {
77+
HttpMonitoringOptions httpMonitoringOptions = new HttpMonitoringOptions();
78+
httpMonitoringOptions.setMinThroughputBytesPerSecond(1);
79+
long readTimeout = config.get(SdkHttpConfigurationOption.READ_TIMEOUT).getSeconds();
80+
long writeTimeout = config.get(SdkHttpConfigurationOption.WRITE_TIMEOUT).getSeconds();
81+
int maxTimeout = NumericUtils.saturatedCast(Math.max(readTimeout, writeTimeout));
82+
httpMonitoringOptions.setAllowableThroughputFailureIntervalSeconds(maxTimeout);
83+
return httpMonitoringOptions;
84+
}
85+
7386
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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 static org.assertj.core.api.Assertions.assertThatThrownBy;
19+
import static software.amazon.awssdk.http.crt.CrtHttpClientTestUtils.createRequest;
20+
21+
import java.io.ByteArrayInputStream;
22+
import java.io.IOException;
23+
import java.net.ServerSocket;
24+
import java.net.Socket;
25+
import java.net.URI;
26+
import java.time.Duration;
27+
import java.util.concurrent.CompletionException;
28+
import java.util.concurrent.ExecutionException;
29+
import java.util.concurrent.TimeUnit;
30+
import java.util.concurrent.TimeoutException;
31+
import org.junit.jupiter.api.AfterEach;
32+
import org.junit.jupiter.api.BeforeEach;
33+
import org.junit.jupiter.api.Test;
34+
import software.amazon.awssdk.crt.Log;
35+
import software.amazon.awssdk.http.ExecutableHttpRequest;
36+
import software.amazon.awssdk.http.HttpExecuteRequest;
37+
import software.amazon.awssdk.http.RecordingResponseHandler;
38+
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
39+
import software.amazon.awssdk.http.SdkHttpRequest;
40+
import software.amazon.awssdk.http.async.AsyncExecuteRequest;
41+
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
42+
import software.amazon.awssdk.http.SdkHttpClient;
43+
import software.amazon.awssdk.utils.AttributeMap;
44+
45+
/**
46+
* Functional tests verifying that the default connection health configuration
47+
* (applied when no explicit {@link ConnectionHealthConfiguration} is set)
48+
* correctly terminates connections to non-responding servers.
49+
*/
50+
class NonResponsiveServerTest {
51+
52+
private static final Duration SHORT_TIMEOUT = Duration.ofSeconds(2);
53+
private static final AttributeMap SHORT_TIMEOUTS = AttributeMap.builder()
54+
.put(SdkHttpConfigurationOption.READ_TIMEOUT, SHORT_TIMEOUT)
55+
.put(SdkHttpConfigurationOption.WRITE_TIMEOUT, SHORT_TIMEOUT)
56+
.build();
57+
58+
private ServerSocket serverSocket;
59+
60+
@BeforeEach
61+
void setUp() throws IOException {
62+
Log.initLoggingToStdout(Log.LogLevel.Warn);
63+
serverSocket = new ServerSocket(0);
64+
// Accept connections in a daemon thread but never respond
65+
Thread acceptThread = new Thread(() -> {
66+
while (!serverSocket.isClosed()) {
67+
try {
68+
Socket socket = serverSocket.accept();
69+
// Hold the connection open, never send a response
70+
Thread.sleep(Long.MAX_VALUE);
71+
} catch (Exception e) {
72+
// Server shutting down
73+
}
74+
}
75+
});
76+
acceptThread.setDaemon(true);
77+
acceptThread.start();
78+
}
79+
80+
@AfterEach
81+
void tearDown() throws IOException {
82+
if (serverSocket != null && !serverSocket.isClosed()) {
83+
serverSocket.close();
84+
}
85+
}
86+
87+
@Test
88+
void syncClient_noExplicitHealthConfig_serverNeverResponds_shouldThrow() {
89+
try (SdkHttpClient client = AwsCrtHttpClient.builder().buildWithDefaults(SHORT_TIMEOUTS)) {
90+
URI uri = URI.create("http://localhost:" + serverSocket.getLocalPort());
91+
SdkHttpRequest request = createRequest(uri);
92+
ExecutableHttpRequest executableRequest = client.prepareRequest(
93+
HttpExecuteRequest.builder().request(request)
94+
.contentStreamProvider(() -> new ByteArrayInputStream(new byte[0]))
95+
.build());
96+
assertThatThrownBy(executableRequest::call).isInstanceOf(IOException.class)
97+
.hasMessageContaining("failure to meet throughput minimum");
98+
}
99+
}
100+
101+
@Test
102+
void asyncClient_noExplicitHealthConfig_serverNeverResponds_shouldCompleteExceptionally()
103+
throws InterruptedException, TimeoutException {
104+
try (SdkAsyncHttpClient client = AwsCrtAsyncHttpClient.builder().buildWithDefaults(SHORT_TIMEOUTS)) {
105+
URI uri = URI.create("http://localhost:" + serverSocket.getLocalPort());
106+
SdkHttpRequest request = createRequest(uri);
107+
RecordingResponseHandler recorder = new RecordingResponseHandler();
108+
109+
client.execute(AsyncExecuteRequest.builder()
110+
.request(request)
111+
.requestContentPublisher(new EmptyPublisher())
112+
.responseHandler(recorder)
113+
.build());
114+
115+
assertThatThrownBy(() -> recorder.completeFuture().get(10, TimeUnit.SECONDS))
116+
.hasCauseInstanceOf(IOException.class)
117+
.hasMessageContaining("failure to meet throughput minimum");
118+
}
119+
}
120+
}

http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,17 @@
2121

2222
import java.time.Duration;
2323
import java.util.stream.Stream;
24-
import org.junit.jupiter.api.AfterAll;
2524
import org.junit.jupiter.api.Assumptions;
2625
import org.junit.jupiter.api.Test;
2726
import org.junit.jupiter.params.ParameterizedTest;
2827
import org.junit.jupiter.params.provider.Arguments;
2928
import org.junit.jupiter.params.provider.MethodSource;
30-
import software.amazon.awssdk.crt.CrtResource;
29+
import software.amazon.awssdk.crt.http.HttpMonitoringOptions;
3130
import software.amazon.awssdk.crt.io.SocketOptions;
3231
import software.amazon.awssdk.crt.io.TlsCipherPreference;
32+
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
3333
import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration;
34+
import software.amazon.awssdk.utils.AttributeMap;
3435

3536
class AwsCrtConfigurationUtilsTest {
3637
@ParameterizedTest
@@ -103,4 +104,31 @@ private static Stream<Arguments> tcpKeepAliveConfiguration() {
103104
);
104105
}
105106

107+
@ParameterizedTest
108+
@MethodSource("defaultConnectionHealthConfigurationCases")
109+
void defaultConnectionHealthConfiguration_shouldUseMaxOfReadWriteTimeout(Duration readTimeout,
110+
Duration writeTimeout,
111+
int expectedInterval) {
112+
AttributeMap config = AttributeMap.builder()
113+
.put(SdkHttpConfigurationOption.READ_TIMEOUT, readTimeout)
114+
.put(SdkHttpConfigurationOption.WRITE_TIMEOUT, writeTimeout)
115+
.build();
116+
117+
HttpMonitoringOptions result = AwsCrtConfigurationUtils.defaultConnectionHealthConfiguration(config);
118+
119+
assertThat(result.getMinThroughputBytesPerSecond()).isEqualTo(1);
120+
assertThat(result.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(expectedInterval);
121+
}
122+
123+
private static Stream<Arguments> defaultConnectionHealthConfigurationCases() {
124+
return Stream.of(
125+
Arguments.of(Duration.ofSeconds(30), Duration.ofSeconds(30), 30),
126+
Arguments.of(Duration.ofSeconds(60), Duration.ofSeconds(10), 60),
127+
Arguments.of(Duration.ofSeconds(10), Duration.ofSeconds(45), 45),
128+
// overflow: value exceeding Integer.MAX_VALUE should saturate
129+
Arguments.of(Duration.ofSeconds((long) Integer.MAX_VALUE + 1), Duration.ofSeconds(1), Integer.MAX_VALUE),
130+
Arguments.of(Duration.ofSeconds(1), Duration.ofSeconds((long) Integer.MAX_VALUE + 1), Integer.MAX_VALUE)
131+
);
132+
}
133+
106134
}

0 commit comments

Comments
 (0)