From 96b08f52ded499f8137a074f2ea5ca4bf5bf7969 Mon Sep 17 00:00:00 2001 From: Nik Pinski Date: Sun, 2 Oct 2022 21:39:05 -0700 Subject: [PATCH] Add the ability to configure 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 --- .../feature-AWSSDKforJavav2-c2b1dfc.json | 6 + .../http/crt/AwsCrtAsyncHttpClient.java | 84 ++++++- .../ConnectionHealthChecksConfiguration.java | 4 +- .../http/crt/SocketOptionsConfiguration.java | 215 ++++++++++++++++++ .../crt/SocketOptionsConfigurationTest.java | 101 ++++++++ 5 files changed, 396 insertions(+), 14 deletions(-) create mode 100644 .changes/next-release/feature-AWSSDKforJavav2-c2b1dfc.json create mode 100644 http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/SocketOptionsConfiguration.java create mode 100644 http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/SocketOptionsConfigurationTest.java diff --git a/.changes/next-release/feature-AWSSDKforJavav2-c2b1dfc.json b/.changes/next-release/feature-AWSSDKforJavav2-c2b1dfc.json new file mode 100644 index 000000000000..80a314cc373b --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-c2b1dfc.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "nikp", + "description": "Add the ability to configure SocketOptions on the AwsCrtAsyncHttpClient to allow explicit enabling of keep-alive signals for long-running connections" +} diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java index 2e0985070340..83ee2e4f10dd 100644 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java @@ -84,7 +84,7 @@ private AwsCrtAsyncHttpClient(DefaultBuilder builder, AttributeMap config) { Validate.isPositive(builder.readBufferSize, "readBufferSize"); try (ClientBootstrap clientBootstrap = new ClientBootstrap(null, null); - SocketOptions clientSocketOptions = new SocketOptions(); + SocketOptions clientSocketOptions = buildSocketOptions(builder.socketOptionsConfiguration); TlsContextOptions clientTlsContextOptions = TlsContextOptions.createDefaultClient() // NOSONAR .withCipherPreference(builder.cipherPreference) .withVerifyPeer(!config.get(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES)); @@ -138,13 +138,29 @@ private HttpProxyOptions buildProxyOptions(ProxyConfiguration proxyConfiguration return clientProxyOptions; } - /** - * Marks a Native CrtResource as owned by the current Java Object. - * - * @param subresource The Resource to own. - * @param The CrtResource Type - * @return The CrtResource passed in - */ + private SocketOptions buildSocketOptions(SocketOptionsConfiguration socketOptionsConfiguration) { + SocketOptions clientSocketOptions = new SocketOptions(); + + if (socketOptionsConfiguration == null) { + return clientSocketOptions; + } + + clientSocketOptions.domain = socketOptionsConfiguration.domain(); + clientSocketOptions.type = socketOptionsConfiguration.type(); + clientSocketOptions.connectTimeoutMs = socketOptionsConfiguration.connectTimeoutMs(); + clientSocketOptions.keepAliveIntervalSecs = socketOptionsConfiguration.keepAliveIntervalSecs(); + clientSocketOptions.keepAliveTimeoutSecs = socketOptionsConfiguration.keepAliveTimeoutSecs(); + + return clientSocketOptions; + } + + /** + * Marks a Native CrtResource as owned by the current Java Object. + * + * @param subresource The Resource to own. + * @param The CrtResource Type + * @return The CrtResource passed in + */ private T registerOwnedResource(T subresource) { if (subresource != null) { subresource.addRef(); @@ -312,10 +328,10 @@ public interface Builder extends SdkAsyncHttpClient.Builder proxyConfigurationBuilderConsumer); /** - * Configure the health checks for for all connections established by this client. + * Configure the health checks for all connections established by this client. * *

- * eg: you can set a throughput threshold for the a connection to be considered healthy. + * eg: you can set a throughput threshold for a connection to be considered healthy. * If the connection falls below this threshold for a configurable amount of time, * then the connection is considered unhealthy and will be shut down. * @@ -325,10 +341,10 @@ public interface Builder extends SdkAsyncHttpClient.Builder - * eg: you can set a throughput threshold for the a connection to be considered healthy. + * eg: you can set a throughput threshold for a connection to be considered healthy. * If the connection falls below this threshold for a configurable amount of time, * then the connection is considered unhealthy and will be shut down. * @@ -343,6 +359,34 @@ Builder connectionHealthChecksConfiguration(Consumer + * eg: you can override socket domains and types, connection timeouts, + * and enable periodic keepalive packets which are disabled out of the box, including keepalive timeouts + * This may be required for certain connections for longer durations than default socket timeouts + * + * @param socketOptionsConfiguration The socket configuration to use + * @return The builder of the method chaining. + */ + Builder socketOptionsConfiguration(SocketOptionsConfiguration socketOptionsConfiguration); + + /** + * A convenience method to configure socket options configuration for all connections established by this client. + * + *

+ * eg: you can override socket domains and types, connection timeouts, + * and enable periodic keepalive packets which are disabled out of the box, including keepalive timeouts + * This may be required for certain connections for longer durations than default socket timeouts + * + * @param socketOptionsConfigurationBuilder The socket configuration builder to use + * @return The builder of the method chaining. + * @see #socketOptionsConfiguration(SocketOptionsConfiguration) + */ + Builder socketOptionsConfiguration(Consumer + socketOptionsConfigurationBuilder); } /** @@ -355,6 +399,7 @@ private static final class DefaultBuilder implements Builder { private int readBufferSize = DEFAULT_STREAM_WINDOW_SIZE; private ProxyConfiguration proxyConfiguration; private ConnectionHealthChecksConfiguration connectionHealthChecksConfiguration; + private SocketOptionsConfiguration socketOptionsConfiguration; private DefaultBuilder() { } @@ -427,5 +472,20 @@ public Builder proxyConfiguration(Consumer proxyConf proxyConfigurationBuilderConsumer.accept(builder); return proxyConfiguration(builder.build()); } + + + @Override + public Builder socketOptionsConfiguration(SocketOptionsConfiguration socketOptionsConfiguration) { + this.socketOptionsConfiguration = socketOptionsConfiguration; + return this; + } + + @Override + public Builder socketOptionsConfiguration(Consumer + socketOptionsConfigurationBuilder) { + SocketOptionsConfiguration.Builder builder = SocketOptionsConfiguration.builder(); + socketOptionsConfigurationBuilder.accept(builder); + return socketOptionsConfiguration(builder.build()); + } } } diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/ConnectionHealthChecksConfiguration.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/ConnectionHealthChecksConfiguration.java index f8b14366cdfa..e9df57bb44db 100644 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/ConnectionHealthChecksConfiguration.java +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/ConnectionHealthChecksConfiguration.java @@ -21,8 +21,8 @@ import software.amazon.awssdk.utils.Validate; /** - * Configuration that defines health checks for for all connections established by - * the{@link ConnectionHealthChecksConfiguration}. + * Configuration that defines health checks for all connections established by + * the {@link ConnectionHealthChecksConfiguration}. * * NOTE: This is a Preview API and is subject to change so it should not be used in production. */ diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/SocketOptionsConfiguration.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/SocketOptionsConfiguration.java new file mode 100644 index 000000000000..ab0783f624fc --- /dev/null +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/SocketOptionsConfiguration.java @@ -0,0 +1,215 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import software.amazon.awssdk.annotations.SdkPreviewApi; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.crt.io.SocketOptions; +import software.amazon.awssdk.utils.Validate; + +/** + * Configuration that defines socket options for all connections established by + * the {@link SocketOptionsConfiguration}. + * + * NOTE: This is a Preview API and is subject to change so it should not be used in production. + */ +@SdkPublicApi +@SdkPreviewApi +public final class SocketOptionsConfiguration { + + private final SocketOptions.SocketDomain domain; + private final SocketOptions.SocketType type; + private final int connectTimeoutMs; + private final int keepAliveIntervalSecs; + private final int keepAliveTimeoutSecs; + + private SocketOptionsConfiguration(DefaultSocketOptionsConfigurationBuilder builder) { + this.domain = Validate.paramNotNull(builder.domain, + "domain"); + this.type = Validate.paramNotNull(builder.type, + "type"); + this.connectTimeoutMs = Validate.isPositive(builder.connectTimeoutMs, + "connectTimeoutMs"); + this.keepAliveIntervalSecs = Validate.isNotNegative(builder.keepAliveIntervalSecs, + "keepAliveIntervalSecs"); + this.keepAliveTimeoutSecs = Validate.isNotNegative(builder.keepAliveTimeoutSecs, + "keepAliveTimeoutSecs"); + } + + /** + * @return socket domain + */ + public SocketOptions.SocketDomain domain() { + return domain; + } + + /** + * @return socket type + */ + public SocketOptions.SocketType type() { + return type; + } + + /** + * @return number of milliseconds before a connection will be considered timed out + */ + public int connectTimeoutMs() { + return connectTimeoutMs; + } + + /** + * @return number of seconds between TCP keepalive packets being sent to the peer + */ + public int keepAliveIntervalSecs() { + return keepAliveIntervalSecs; + } + + /** + * @return number of seconds to wait for a keepalive response before considering the connection timed out + */ + public int keepAliveTimeoutSecs() { + return keepAliveTimeoutSecs; + } + + public static Builder builder() { + return new DefaultSocketOptionsConfigurationBuilder(); + } + + /** + * A builder for {@link SocketOptionsConfiguration}. + * + *

All implementations of this interface are mutable and not thread safe.

+ */ + public interface Builder { + + /** + * Sets the socket domain + * @param domain socket domain + * @return Builder + */ + Builder domain(SocketOptions.SocketDomain domain); + + /** + * Sets the socket type + * @param type socket type + * @return Builder + */ + Builder type(SocketOptions.SocketType type); + + /** + * Sets the number of milliseconds before a connection will be considered timed out + * @param connectTimeoutMs number of milliseconds before a connection will be considered timed out + * @return Builder + */ + Builder connectTimeoutMs(int connectTimeoutMs); + + /** + * Sets the number of seconds between TCP keepalive packets being sent to the peer + * @param keepAliveIntervalSecs number of seconds between TCP keepalive packets being sent to the peer + * @return Builder + */ + Builder keepAliveIntervalSecs(int keepAliveIntervalSecs); + + /** + * Sets the number of seconds to wait for a keepalive response before considering the connection timed out + * @param keepAliveTimeoutSecs number of seconds to wait for a keepalive response before considering the connection timed out + * @return Builder + */ + Builder keepAliveTimeoutSecs(int keepAliveTimeoutSecs); + + SocketOptionsConfiguration build(); + } + + /** + * An SDK-internal implementation of {@link Builder}. + */ + private static final class DefaultSocketOptionsConfigurationBuilder implements Builder { + + private SocketOptions.SocketDomain domain = SocketOptions.SocketDomain.IPv6; + private SocketOptions.SocketType type = SocketOptions.SocketType.STREAM; + private int connectTimeoutMs = 3000; + private int keepAliveIntervalSecs = 0; + private int keepAliveTimeoutSecs = 0; + + private DefaultSocketOptionsConfigurationBuilder() { + } + + /** + * Sets the socket domain + * Default: {@link SocketOptions.SocketDomain#IPv6} + * @param domain socket domain + * @return Builder + */ + @Override + public Builder domain(SocketOptions.SocketDomain domain) { + this.domain = domain; + return this; + } + + /** + * Sets the socket type + * Default: {@link SocketOptions.SocketType#STREAM} + * @param type socket type + * @return Builder + */ + @Override + public Builder type(SocketOptions.SocketType type) { + this.type = type; + return this; + } + + /** + * Sets the number of milliseconds before a connection will be considered timed out + * Default: 3000ms + * @param connectTimeoutMs number of milliseconds before a connection will be considered timed out + * @return Builder + */ + @Override + public Builder connectTimeoutMs(int connectTimeoutMs) { + this.connectTimeoutMs = connectTimeoutMs; + return this; + } + + /** + * Sets the number of seconds between TCP keepalive packets being sent to the peer + * Default: 0 disables keepalive + * @param keepAliveIntervalSecs number of seconds between TCP keepalive packets being sent to the peer + * @return Builder + */ + @Override + public Builder keepAliveIntervalSecs(int keepAliveIntervalSecs) { + this.keepAliveIntervalSecs = keepAliveIntervalSecs; + return this; + } + + /** + * Sets the number of seconds to wait for a keepalive response before considering the connection timed out + * Default: 0 disables keepalive + * @param keepAliveTimeoutSecs number of seconds to wait for a keepalive response before considering the connection timed out + * @return Builder + */ + @Override + public Builder keepAliveTimeoutSecs(int keepAliveTimeoutSecs) { + this.keepAliveTimeoutSecs = keepAliveTimeoutSecs; + return this; + } + + @Override + public SocketOptionsConfiguration build() { + return new SocketOptionsConfiguration(this); + } + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/SocketOptionsConfigurationTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/SocketOptionsConfigurationTest.java new file mode 100644 index 000000000000..fb9b85765c9a --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/SocketOptionsConfigurationTest.java @@ -0,0 +1,101 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.crt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.crt.io.SocketOptions; + +public class SocketOptionsConfigurationTest { + + @Test + public void builder_defaultPropertiesSet() { + SocketOptionsConfiguration socketOptionsConfiguration = + SocketOptionsConfiguration.builder() + .build(); + + assertThat(socketOptionsConfiguration.domain()).isEqualTo(SocketOptions.SocketDomain.IPv6); + assertThat(socketOptionsConfiguration.type()).isEqualTo(SocketOptions.SocketType.STREAM); + assertThat(socketOptionsConfiguration.connectTimeoutMs()).isEqualTo(3000); + assertThat(socketOptionsConfiguration.keepAliveIntervalSecs()).isEqualTo(0); + assertThat(socketOptionsConfiguration.keepAliveTimeoutSecs()).isEqualTo(0); + } + + @Test + public void builder_allPropertiesSet() { + SocketOptionsConfiguration socketOptionsConfiguration = + SocketOptionsConfiguration.builder() + .domain(SocketOptions.SocketDomain.IPv4) + .type(SocketOptions.SocketType.DGRAM) + .connectTimeoutMs(1000) + .keepAliveIntervalSecs(300) + .keepAliveTimeoutSecs(1) + .build(); + + assertThat(socketOptionsConfiguration.domain()).isEqualTo(SocketOptions.SocketDomain.IPv4); + assertThat(socketOptionsConfiguration.type()).isEqualTo(SocketOptions.SocketType.DGRAM); + assertThat(socketOptionsConfiguration.connectTimeoutMs()).isEqualTo(1000); + assertThat(socketOptionsConfiguration.keepAliveIntervalSecs()).isEqualTo(300); + assertThat(socketOptionsConfiguration.keepAliveTimeoutSecs()).isEqualTo(1); + } + + @Test + public void builder_nullSocketDomain_shouldThrowException() { + assertThatThrownBy(() -> + SocketOptionsConfiguration.builder() + .domain(null) + .build()) + .hasMessageContaining("domain"); + } + + @Test + public void builder_nullSocketType_shouldThrowException() { + assertThatThrownBy(() -> + SocketOptionsConfiguration.builder() + .type(null) + .build()) + .hasMessageContaining("type"); + } + + @Test + public void builder_nonPositiveSocketTimeout_shouldThrowException() { + assertThatThrownBy(() -> + SocketOptionsConfiguration.builder() + .connectTimeoutMs(0) + .build()) + .hasMessageContaining("connectTimeoutMs"); + } + + @Test + public void builder_negativeKeepAliveInterval_shouldThrowException() { + assertThatThrownBy(() -> + SocketOptionsConfiguration.builder() + .keepAliveIntervalSecs(-1) + .build()) + .hasMessageContaining("keepAliveIntervalSecs"); + } + + @Test + public void builder_negativeKeepAliveTimeout_shouldThrowException() { + assertThatThrownBy(() -> + SocketOptionsConfiguration.builder() + .keepAliveTimeoutSecs(-1) + .build()) + .hasMessageContaining("keepAliveTimeoutSecs"); + } +}