From f2897be1c59305eed6b5191f443ff98bcbb5fc49 Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Wed, 26 Aug 2026 08:45:19 -0700 Subject: [PATCH 1/6] feat: add default read timeout rollout gate (#7319) Mirror the AWS_NEW_RETRIES_2026 gate: add the AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026 system setting, its resolver, the DEFAULT_ENABLE_READ_TIMEOUT_2026 client option, and the codegen customization field that bakes it into mergeInternalDefaults. Nothing consumes the gate yet; the aws-core and CRT client wiring follow separately. --- .../customization/CustomizationConfig.java | 13 ++ .../poet/builder/BaseClientBuilderClass.java | 16 ++- ...lient-builder-internal-defaults-class.java | 1 + .../c2j/internalconfig/customization.config | 3 +- .../amazon/awssdk/core/SdkSystemSetting.java | 12 +- .../core/client/config/SdkClientOption.java | 7 + .../EnableDefaultReadTimeout2026Resolver.java | 57 +++++++++ ...bleDefaultReadTimeout2026ResolverTest.java | 121 ++++++++++++++++++ 8 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026Resolver.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026ResolverTest.java diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java index ab3c3f78833d..875e272066ce 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java @@ -230,6 +230,11 @@ public class CustomizationConfig { */ private Boolean defaultNewRetries2026; + /** + * Whether the client will apply a default read/write timeout by default. + */ + private Boolean defaultEnableReadTimeout2026; + /** * Whether to generate an abstract decorator class that delegates to the async service client */ @@ -737,6 +742,14 @@ public void setDefaultNewRetries2026(Boolean defaultNewRetries2026) { this.defaultNewRetries2026 = defaultNewRetries2026; } + public Boolean getDefaultEnableReadTimeout2026() { + return defaultEnableReadTimeout2026; + } + + public void setDefaultEnableReadTimeout2026(Boolean defaultEnableReadTimeout2026) { + this.defaultEnableReadTimeout2026 = defaultEnableReadTimeout2026; + } + public ServiceConfig getServiceConfig() { return serviceConfig; } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java index f2ea2836be7a..8e583ae119c3 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java @@ -48,6 +48,7 @@ import software.amazon.awssdk.awscore.client.config.AwsClientOption; import software.amazon.awssdk.awscore.endpoint.AwsClientEndpointProvider; import software.amazon.awssdk.codegen.internal.Utils; +import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; import software.amazon.awssdk.codegen.model.intermediate.OperationModel; import software.amazon.awssdk.codegen.model.rules.endpoints.BuiltInParameter; @@ -330,9 +331,10 @@ private Optional mergeInternalDefaultsMethod() { String userAgent = model.getCustomizationConfig().getUserAgent(); RetryMode defaultRetryMode = model.getCustomizationConfig().getDefaultRetryMode(); Boolean defaultNewRetries2026 = model.getCustomizationConfig().getDefaultNewRetries2026(); + Boolean defaultEnableReadTimeout2026 = model.getCustomizationConfig().getDefaultEnableReadTimeout2026(); // If none of the options are customized, then we do not need to bother overriding the method - if (userAgent == null && defaultRetryMode == null && defaultNewRetries2026 == null) { + if (!hasInternalDefaults()) { return Optional.empty(); } @@ -354,10 +356,22 @@ private Optional mergeInternalDefaultsMethod() { builder.addCode("c.option($T.DEFAULT_NEW_RETRIES_2026, $L);\n", SdkClientOption.class, defaultNewRetries2026); } + if (defaultEnableReadTimeout2026 != null) { + builder.addCode("c.option($T.DEFAULT_ENABLE_READ_TIMEOUT_2026, $L);\n", + SdkClientOption.class, defaultEnableReadTimeout2026); + } builder.addCode("});\n"); return Optional.of(builder.build()); } + private boolean hasInternalDefaults() { + CustomizationConfig customizationConfig = model.getCustomizationConfig(); + return customizationConfig.getUserAgent() != null + || customizationConfig.getDefaultRetryMode() != null + || customizationConfig.getDefaultNewRetries2026() != null + || customizationConfig.getDefaultEnableReadTimeout2026() != null; + } + private MethodSpec finalizeServiceConfigurationMethod() { String requestHandlerDirectory = Utils.packageToDirectory(model.getMetadata().getFullClientPackageName()); String requestHandlerPath = String.format("%s/execution.interceptors", requestHandlerDirectory); diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java index 9f269bf6e33f..c8d612714b5e 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java @@ -77,6 +77,7 @@ protected final SdkClientConfiguration mergeInternalDefaults(SdkClientConfigurat c.option(SdkClientOption.INTERNAL_USER_AGENT, "md/foobar"); c.option(SdkClientOption.DEFAULT_RETRY_MODE, RetryMode.STANDARD); c.option(SdkClientOption.DEFAULT_NEW_RETRIES_2026, true); + c.option(SdkClientOption.DEFAULT_ENABLE_READ_TIMEOUT_2026, true); }); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config index 594a4aceb54d..c816c420aa1d 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config @@ -4,5 +4,6 @@ }, "userAgent": "md/foobar", "defaultRetryMode": "STANDARD", - "defaultNewRetries2026": "true" + "defaultNewRetries2026": "true", + "defaultEnableReadTimeout2026": "true" } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java index e941ed2fd905..7f51b3417526 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java @@ -276,7 +276,17 @@ public enum SdkSystemSetting implements SystemSetting { * defaults including STANDARD as the default retry mode, reduced base backoff delays, differentiated token bucket * costs, and other v2.1 retry specification changes. When {@code false} (the default), the SDK uses v2.0 retry behavior. */ - AWS_NEW_RETRIES_2026("aws.newRetries2026", null); + AWS_NEW_RETRIES_2026("aws.newRetries2026", null), + + /** + * Configure whether a default read/write inactivity timeout is applied to HTTP clients that do not enforce one of their + * own (currently the AWS CRT-based clients). When {@code true}, such clients shut down a connection that transfers no + * bytes for the resolved timeout window. When {@code false} (the default), no default read/write timeout is applied. + * + *

This setting is not intended to be used by end users. It gates an interim rollout and is subject to removal in a + * future release. + */ + AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026("aws.enableDefaultReadTimeout2026", null); private final String systemProperty; private final String defaultValue; diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java index 20ea0d01c3b9..1ab50ae46caa 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java @@ -311,6 +311,13 @@ public final class SdkClientOption extends ClientOption { */ public static final SdkClientOption DEFAULT_NEW_RETRIES_2026 = new SdkClientOption<>(Boolean.class); + /** + * Option to specify the default for the {@code AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} feature gate for the SDK client. + * This option is not intended to be set by end users. It gates an interim rollout and is subject to removal in a future + * release. + */ + public static final SdkClientOption DEFAULT_ENABLE_READ_TIMEOUT_2026 = new SdkClientOption<>(Boolean.class); + /** * Whether retries 2.1 behavior is enabled. */ diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026Resolver.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026Resolver.java new file mode 100644 index 000000000000..200124395958 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026Resolver.java @@ -0,0 +1,57 @@ +/* + * 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.core.http; + +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.core.SdkSystemSetting; + +/** + * Resolver for the {@link SdkSystemSetting#AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} that supports setting a fallback value if not + * defined in the environment or system properties. + */ +@SdkProtectedApi +public final class EnableDefaultReadTimeout2026Resolver { + private Boolean defaultEnableReadTimeout2026; + + /** + * The default value for {@code AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} if not configured via + * {@link SdkSystemSetting#AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026}. + * + * @return This resolver for method chaining. + */ + public EnableDefaultReadTimeout2026Resolver defaultEnableReadTimeout2026(Boolean defaultEnableReadTimeout2026) { + this.defaultEnableReadTimeout2026 = defaultEnableReadTimeout2026; + return this; + } + + /** + * Resolve whether a default read/write timeout is applied. + */ + public boolean resolve() { + Optional envConfig = SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.getBooleanValue(); + + if (envConfig.isPresent()) { + return envConfig.get(); + } + + if (defaultEnableReadTimeout2026 != null) { + return defaultEnableReadTimeout2026; + } + + return false; + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026ResolverTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026ResolverTest.java new file mode 100644 index 000000000000..6ef795f658ad --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026ResolverTest.java @@ -0,0 +1,121 @@ +/* + * 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.core.http; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; + +public class EnableDefaultReadTimeout2026ResolverTest { + private static String enableDefaultReadTimeout2026Save; + + @BeforeAll + static void setup() { + enableDefaultReadTimeout2026Save = System.getProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property()); + } + + @AfterAll + static void teardown() { + if (enableDefaultReadTimeout2026Save != null) { + System.setProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property(), + enableDefaultReadTimeout2026Save); + } else { + System.clearProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property()); + } + } + + @BeforeEach + void methodSetup() { + System.clearProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property()); + } + + @Test + void systemSetting_usesExpectedEnvironmentVariableAndSystemPropertyNames() { + assertThat(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.environmentVariable()) + .isEqualTo("AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026"); + assertThat(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property()) + .isEqualTo("aws.enableDefaultReadTimeout2026"); + } + + @ParameterizedTest + @MethodSource("params") + void resolve_behavesCorrectly(TestParams params) { + EnvironmentVariableHelper.run((env) -> { + if (params.systemProperty != null) { + System.setProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property(), params.systemProperty); + } + + if (params.envVar != null) { + env.set(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.environmentVariable(), params.envVar); + } + + EnableDefaultReadTimeout2026Resolver resolver = + new EnableDefaultReadTimeout2026Resolver().defaultEnableReadTimeout2026(params.defaultEnableReadTimeout2026); + + assertThat(resolver.resolve()).isEqualTo(params.expected); + }); + } + + private static Stream params() { + return Stream.of( + // default + new TestParams().expected(false), + + // precedence testing + new TestParams().systemProperty("true").defaultEnableReadTimeout2026(true).expected(true), + new TestParams().systemProperty("false").defaultEnableReadTimeout2026(true).expected(false), + new TestParams().envVar("true").defaultEnableReadTimeout2026(true).expected(true), + new TestParams().envVar("false").defaultEnableReadTimeout2026(true).expected(false), + new TestParams().defaultEnableReadTimeout2026(true).expected(true), + new TestParams().defaultEnableReadTimeout2026(false).expected(false) + ); + } + + private static class TestParams { + private String systemProperty; + private String envVar; + private Boolean defaultEnableReadTimeout2026; + private boolean expected; + + public TestParams systemProperty(String systemProperty) { + this.systemProperty = systemProperty; + return this; + } + + public TestParams envVar(String envVar) { + this.envVar = envVar; + return this; + } + + public TestParams defaultEnableReadTimeout2026(Boolean defaultEnableReadTimeout2026) { + this.defaultEnableReadTimeout2026 = defaultEnableReadTimeout2026; + return this; + } + + public TestParams expected(boolean expected) { + this.expected = expected; + return this; + } + } +} From 3ba7c4f20eed1973dc2ef7dda294621526b4556c Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:34:45 -0700 Subject: [PATCH 2/6] feat: apply default read/write timeout for CRT (#7326) The CRT-based HTTP clients now honor SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, mapping it onto an HttpMonitoringOptions minimum-throughput monitor. An explicit ConnectionHealthConfiguration takes precedence; a standalone client reads the opt-in environment variable directly. --- .../http/SdkHttpConfigurationOption.java | 10 ++ .../http/crt/AwsCrtAsyncHttpClient.java | 20 ++-- .../awssdk/http/crt/AwsCrtHttpClient.java | 20 ++-- .../awssdk/http/crt/AwsCrtHttpClientBase.java | 8 +- .../internal/AwsCrtConfigurationUtils.java | 52 +++++++++ .../AwsCrtDefaultReadTimeoutSetting.java | 45 ++++++++ ...AsyncHttpClientLongRunningRequestTest.java | 52 ++++++++- ...wsCrtHttpClientLongRunningRequestTest.java | 87 ++++++++++++++- .../AwsCrtConfigurationUtilsTest.java | 102 ++++++++++++++++++ 9 files changed, 366 insertions(+), 30 deletions(-) create mode 100644 http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtDefaultReadTimeoutSetting.java diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java index e9efa8b1814b..69487fb28129 100644 --- a/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java +++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java @@ -41,6 +41,16 @@ public final class SdkHttpConfigurationOption extends AttributeMap.Key { public static final SdkHttpConfigurationOption WRITE_TIMEOUT = new SdkHttpConfigurationOption<>("WriteTimeout", Duration.class); + /** + * SDK-internal. A fallback read/write timeout that is honored only by HTTP client implementations which do not + * enforce a read/write timeout of their own (currently the AWS CRT-based clients); implementations that already + * apply one, such as the Apache and Netty clients, ignore it. Unlike {@link #READ_TIMEOUT}/{@link #WRITE_TIMEOUT}, + * which carry a caller-configured socket timeout, this value is resolved by the SDK and is not intended to be set by + * end users. {@link Duration#ZERO} means no timeout is applied (the service is fully exempt, or the rollout gate is off). + */ + public static final SdkHttpConfigurationOption SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT = + new SdkHttpConfigurationOption<>("SdkInternalFallbackReadWriteTimeout", Duration.class); + /** * Timeout for establishing a connection to a remote service. */ 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 1f2a35765893..c2b8cc7e0897 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 @@ -165,17 +165,19 @@ public interface Builder extends SdkAsyncHttpClient.Builder proxyConfigurationBuilderConsumer); /** - * Configure the health checks for all connections established by this client. + * Configure the health checks for all connections established by this client. This is the CRT client's knob for a + * read/write inactivity timeout: a connection whose throughput stays below + * {@link ConnectionHealthConfiguration#minimumThroughputInBps()} for + * {@link ConnectionHealthConfiguration#minimumThroughputTimeout()} is considered unhealthy and shut down, failing the + * in-flight request with a retryable {@code IOException}. * - *

- * You can set a throughput threshold for a connection to be considered healthy. - * If a connection falls below this threshold ({@link ConnectionHealthConfiguration#minimumThroughputInBps() - * }) for the configurable amount - * of time ({@link ConnectionHealthConfiguration#minimumThroughputTimeout()}), - * then the connection is considered unhealthy and will be shut down. + *

To set a read/write inactivity timeout, use {@code minimumThroughputInBps(1L)} with + * {@code minimumThroughputTimeout(yourDuration)}: any byte moved in either direction resets the window, so the connection + * is shut down only after {@code yourDuration} elapses with no bytes transferred. The timeout has whole-second + * granularity and must be at least two seconds. * - *

- * Disabled by default. + *

Absent an explicit configuration here, a default read/write inactivity timeout may apply; a configuration set here + * always takes precedence over that default. * * @param healthChecksConfiguration The health checks config to use * @return The builder of the method chaining. diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java index 6eeab8a20242..4e21ecaa0909 100644 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java @@ -221,17 +221,19 @@ public interface Builder extends SdkHttpClient.Builder AwsCrtHttpClient.Builder proxyConfiguration(Consumer proxyConfigurationBuilderConsumer); /** - * Configure the health checks for all connections established by this client. + * Configure the health checks for all connections established by this client. This is the CRT client's knob for a + * read/write inactivity timeout: a connection whose throughput stays below + * {@link ConnectionHealthConfiguration#minimumThroughputInBps()} for + * {@link ConnectionHealthConfiguration#minimumThroughputTimeout()} is considered unhealthy and shut down, failing the + * in-flight request with a retryable {@code IOException}. * - *

- * You can set a throughput threshold for a connection to be considered healthy. - * If a connection falls below this threshold ({@link ConnectionHealthConfiguration#minimumThroughputInBps() - * }) for the configurable amount - * of time ({@link ConnectionHealthConfiguration#minimumThroughputTimeout()}), - * then the connection is considered unhealthy and will be shut down. + *

To set a read/write inactivity timeout, use {@code minimumThroughputInBps(1L)} with + * {@code minimumThroughputTimeout(yourDuration)}: any byte moved in either direction resets the window, so the connection + * is shut down only after {@code yourDuration} elapses with no bytes transferred. The timeout has whole-second + * granularity and must be at least two seconds. * - *

- * Disabled by default. + *

Absent an explicit configuration here, a default read/write inactivity timeout may apply; a configuration set here + * always takes precedence over that default. * * @param healthChecksConfiguration The health checks config to use * @return The builder of the method chaining. diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java index 4d8dc88ba6f6..703a196dd102 100644 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java @@ -15,13 +15,13 @@ package software.amazon.awssdk.http.crt; -import static software.amazon.awssdk.crtcore.CrtConfigurationUtils.resolveHttpMonitoringOptions; import static software.amazon.awssdk.crtcore.CrtConfigurationUtils.resolveProxy; import static software.amazon.awssdk.http.SdkHttpConfigurationOption.PROTOCOL; import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildSocketOptions; import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildTlsConnectionOptions; import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveCipherPreference; import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveMinTlsVersion; +import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveMonitoringOptions; import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; import java.net.URI; @@ -136,9 +136,9 @@ abstract class AwsCrtHttpClientBase implements SdkAutoCloseable { this.readBufferSize = builder.getReadBufferSizeInBytes() == null ? DEFAULT_STREAM_WINDOW_SIZE : builder.getReadBufferSizeInBytes(); this.maxStreamsPerEndpoint = config.get(SdkHttpConfigurationOption.MAX_CONNECTIONS); - this.monitoringOptions = - resolveHttpMonitoringOptions(builder.getConnectionHealthConfiguration()) - .orElse(null); + this.monitoringOptions = resolveMonitoringOptions( + builder.getConnectionHealthConfiguration(), + config.get(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT)); this.maxConnectionIdleInMilliseconds = config.get(SdkHttpConfigurationOption.CONNECTION_MAX_IDLE_TIMEOUT).toMillis(); this.connectionAcquisitionTimeout = config.get(SdkHttpConfigurationOption.CONNECTION_ACQUIRE_TIMEOUT).toMillis(); this.proxyOptions = resolveProxy(builder.getProxyConfiguration(), tlsContext).orElse(null); diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java index c3de2a1aee2f..cc5f06d60f85 100644 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java @@ -18,11 +18,14 @@ import java.time.Duration; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.crt.http.HttpMonitoringOptions; import software.amazon.awssdk.crt.io.SocketOptions; import software.amazon.awssdk.crt.io.TlsCipherPreference; import software.amazon.awssdk.crt.io.TlsConnectionOptions; import software.amazon.awssdk.crt.io.TlsContext; import software.amazon.awssdk.crt.io.TlsContextOptions; +import software.amazon.awssdk.crtcore.CrtConfigurationUtils; +import software.amazon.awssdk.http.crt.ConnectionHealthConfiguration; import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration; import software.amazon.awssdk.http.crt.TlsVersion; import software.amazon.awssdk.utils.Logger; @@ -32,9 +35,58 @@ public final class AwsCrtConfigurationUtils { private static final Logger log = Logger.loggerFor(AwsCrtConfigurationUtils.class); + // CRT rejects a throughput-failure interval below two seconds (HttpMonitoringOptions). + private static final int MIN_MONITORING_FAILURE_INTERVAL_SECONDS = 2; + + // The default read/write inactivity timeout applied on the standalone path (a CRT client not built by a service client) + // when the rollout gate is on. On the service-client path this default is supplied above the transport, in aws-core. + private static final Duration STANDALONE_DEFAULT_READ_WRITE_TIMEOUT = Duration.ofMinutes(5); + private AwsCrtConfigurationUtils() { } + /** + * Resolves the throughput monitor that enforces a connection's read/write inactivity timeout, highest precedence first: + *

    + *
  1. an explicit {@link ConnectionHealthConfiguration} always wins;
  2. + *
  3. on the service-client path, the {@code fallbackTimeout}, which aws-core has already resolved against the rollout + * gate, so a present value is post-gate: {@link Duration#ZERO} applies nothing, a positive value is mapped;
  4. + *
  5. on the standalone path ({@code fallbackTimeout} null, no aws-core resolution ran), the rollout gate is read + * directly, applying the default timeout when it is on and nothing otherwise.
  6. + *
+ */ + public static HttpMonitoringOptions resolveMonitoringOptions(ConnectionHealthConfiguration healthConfiguration, + Duration fallbackTimeout) { + if (healthConfiguration != null) { + return CrtConfigurationUtils.resolveHttpMonitoringOptions(healthConfiguration).orElse(null); + } + + if (fallbackTimeout != null) { + return fallbackTimeout.isZero() ? null : mapReadWriteTimeout(fallbackTimeout); + } + + if (AwsCrtDefaultReadTimeoutSetting.ENABLE_DEFAULT_READ_TIMEOUT_2026.getBooleanValue().orElse(false)) { + return mapReadWriteTimeout(STANDALONE_DEFAULT_READ_WRITE_TIMEOUT); + } + return null; + } + + /** + * Maps a read/write inactivity timeout onto the CRT throughput monitor that enforces it: a minimum throughput of one byte + * per second measured over a failure interval of {@code readWriteTimeout}. Because the threshold is a single byte, any byte + * moved while a stream is pending resets the interval, so the connection is shut down only after {@code readWriteTimeout} of + * continuous zero-byte progress. The interval has whole-second granularity, and CRT rejects an interval below two seconds, so + * a shorter timeout is raised to that floor. + */ + public static HttpMonitoringOptions mapReadWriteTimeout(Duration readWriteTimeout) { + HttpMonitoringOptions httpMonitoringOptions = new HttpMonitoringOptions(); + httpMonitoringOptions.setMinThroughputBytesPerSecond(1); + int seconds = Math.max(MIN_MONITORING_FAILURE_INTERVAL_SECONDS, + NumericUtils.saturatedCast(readWriteTimeout.getSeconds())); + httpMonitoringOptions.setAllowableThroughputFailureIntervalSeconds(seconds); + return httpMonitoringOptions; + } + public static SocketOptions buildSocketOptions(TcpKeepAliveConfiguration tcpKeepAliveConfiguration, Duration connectionTimeout) { SocketOptions clientSocketOptions = new SocketOptions(); diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtDefaultReadTimeoutSetting.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtDefaultReadTimeoutSetting.java new file mode 100644 index 000000000000..3e032347e1ff --- /dev/null +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtDefaultReadTimeoutSetting.java @@ -0,0 +1,45 @@ +/* + * 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.internal; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.utils.SystemSetting; + +/** + * The rollout gate the standalone (non-service-client) CRT HTTP Client reads to decide whether to apply the default read/write + * inactivity timeout. It mirrors {@code SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} in sdk-core, using the same + * environment variable and system property; the CRT client reads them directly because it cannot depend on sdk-core. This is + * needed to ensure a timeout is configured when a standalone HTTP client is created. + */ +@SdkInternalApi +public enum AwsCrtDefaultReadTimeoutSetting implements SystemSetting { + ENABLE_DEFAULT_READ_TIMEOUT_2026; + + @Override + public String property() { + return "aws.enableDefaultReadTimeout2026"; + } + + @Override + public String environmentVariable() { + return "AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026"; + } + + @Override + public String defaultValue() { + return null; + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientLongRunningRequestTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientLongRunningRequestTest.java index 17c8f1f37c8a..5a0a4b9d7ec1 100644 --- a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientLongRunningRequestTest.java +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientLongRunningRequestTest.java @@ -15,9 +15,21 @@ package software.amazon.awssdk.http.crt; +import static software.amazon.awssdk.http.LongRunningRequestTestSupport.CONFIGURED_TIMEOUT; +import static software.amazon.awssdk.http.LongRunningRequestTestSupport.assertFailsWithIoExceptionWithinTimeBound; +import static software.amazon.awssdk.http.LongRunningRequestTestSupport.stubLongPolling; +import static software.amazon.awssdk.http.LongRunningRequestTestSupport.stubStreamingWithPauses; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import software.amazon.awssdk.crt.Log; +import software.amazon.awssdk.http.HttpTestUtils; import software.amazon.awssdk.http.SdkAsyncHttpClientLongRunningRequestTestSuite; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.utils.AttributeMap; @@ -34,15 +46,47 @@ protected SdkAsyncHttpClient createSdkAsyncHttpClient(AttributeMap config) { return AwsCrtAsyncHttpClient.builder().buildWithDefaults(config); } - // Empty test; the CRT async client does not currently enforce READ_TIMEOUT. Delete this - // override when connection health monitoring is re-added. + // The CRT client enforces a read/write inactivity timeout through SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT rather than + // READ_TIMEOUT (which it ignores), so these two suite cases are re-implemented to configure that option. A stalled or + // paused response then trips the CRT throughput monitor (AWS_ERROR_HTTP_CHANNEL_THROUGHPUT_FAILURE, a retryable + // IOException) and fails the request within the bound instead of hanging. + @Test @Override public void executeWhenReadTimeoutAndServerDelaysResponseFailsWithinTimeoutBound() { + stubLongPolling(mockServer); + SdkAsyncHttpClient client = createSdkAsyncHttpClient(fallbackTimeoutConfig()); + try { + assertFailsWithIoExceptionWithinTimeBound(sendRequest(client), CONFIGURED_TIMEOUT); + } finally { + client.close(); + } } - // Empty test; the CRT async client does not currently enforce READ_TIMEOUT. Delete this - // override when connection health monitoring is re-added. + @Test @Override public void executeWhenReadTimeoutAndStreamingResponsePausesFailsWithinTimeoutBound() { + stubStreamingWithPauses(mockServer); + SdkAsyncHttpClient client = createSdkAsyncHttpClient(fallbackTimeoutConfig()); + try { + assertFailsWithIoExceptionWithinTimeBound(sendRequest(client), CONFIGURED_TIMEOUT); + } finally { + client.close(); + } + } + + private static AttributeMap fallbackTimeoutConfig() { + return AttributeMap.builder() + .put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, CONFIGURED_TIMEOUT) + .build(); + } + + private CompletableFuture sendRequest(SdkAsyncHttpClient client) { + URI uri = URI.create("http://localhost:" + mockServer.getPort()); + SdkHttpFullRequest request = SdkHttpFullRequest.builder() + .uri(uri) + .method(SdkHttpMethod.GET) + .putHeader("Host", uri.getHost()) + .build(); + return HttpTestUtils.sendRequest(client, request); } } diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientLongRunningRequestTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientLongRunningRequestTest.java index e51ca3aa7e16..3b6049403abc 100644 --- a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientLongRunningRequestTest.java +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientLongRunningRequestTest.java @@ -15,10 +15,27 @@ package software.amazon.awssdk.http.crt; +import static software.amazon.awssdk.http.LongRunningRequestTestSupport.CONFIGURED_TIMEOUT; +import static software.amazon.awssdk.http.LongRunningRequestTestSupport.assertFailsWithIoExceptionWithinTimeBound; +import static software.amazon.awssdk.http.LongRunningRequestTestSupport.stubLongPolling; +import static software.amazon.awssdk.http.LongRunningRequestTestSupport.stubStreamingWithPauses; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import software.amazon.awssdk.crt.Log; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpClientLongRunningRequestTestSuite; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; import software.amazon.awssdk.utils.AttributeMap; public class AwsCrtHttpClientLongRunningRequestTest extends SdkHttpClientLongRunningRequestTestSuite { @@ -34,15 +51,77 @@ protected SdkHttpClient createSdkHttpClient(AttributeMap config) { return AwsCrtHttpClient.builder().buildWithDefaults(config); } - // Empty test; the CRT sync client does not currently enforce READ_TIMEOUT. Delete this - // override when connection health monitoring is re-added. + // The CRT client enforces a read/write inactivity timeout through SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT rather than + // READ_TIMEOUT (which it ignores), so these two suite cases are re-implemented to configure that option. A stalled or + // paused response then trips the CRT throughput monitor (AWS_ERROR_HTTP_CHANNEL_THROUGHPUT_FAILURE, a retryable + // IOException) and fails the request within the bound instead of hanging. + @Test @Override public void executeWhenReadTimeoutAndServerDelaysResponseFailsWithinTimeoutBound() { + stubLongPolling(mockServer); + SdkHttpClient client = createSdkHttpClient(fallbackTimeoutConfig()); + try { + assertFailsWithIoExceptionWithinTimeBound(executeAsync(client), CONFIGURED_TIMEOUT); + } finally { + client.close(); + } } - // Empty test; the CRT sync client does not currently enforce READ_TIMEOUT. Delete this - // override when connection health monitoring is re-added. + @Test @Override public void executeWhenReadTimeoutAndStreamingResponsePausesFailsWithinTimeoutBound() { + stubStreamingWithPauses(mockServer); + SdkHttpClient client = createSdkHttpClient(fallbackTimeoutConfig()); + try { + assertFailsWithIoExceptionWithinTimeBound(executeAsync(client), CONFIGURED_TIMEOUT); + } finally { + client.close(); + } + } + + private static AttributeMap fallbackTimeoutConfig() { + return AttributeMap.builder() + .put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, CONFIGURED_TIMEOUT) + .build(); + } + + private CompletableFuture executeAsync(SdkHttpClient client) { + return CompletableFuture.supplyAsync(() -> { + executeRequest(client); + return null; + }); + } + + private void executeRequest(SdkHttpClient client) { + URI uri = URI.create("http://localhost:" + mockServer.getPort()); + SdkHttpFullRequest request = SdkHttpFullRequest.builder() + .uri(uri) + .method(SdkHttpMethod.POST) + .putHeader("Host", uri.getHost()) + .putHeader("Content-Length", "4") + .contentStreamProvider(() -> new ByteArrayInputStream( + "Body".getBytes(StandardCharsets.UTF_8))) + .build(); + try { + HttpExecuteResponse response = client.prepareRequest(HttpExecuteRequest.builder() + .request(request) + .contentStreamProvider( + request.contentStreamProvider() + .orElse(null)) + .build()) + .call(); + response.responseBody().ifPresent(body -> { + try { + while (body.read() != -1) { + // drain body so mid-body timeouts surface + } + body.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } } diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java index c2dcfa282a07..ea0d4e960de8 100644 --- a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java @@ -21,6 +21,8 @@ import java.time.Duration; import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -30,11 +32,21 @@ import software.amazon.awssdk.crt.io.TlsCipherPreference; import software.amazon.awssdk.crt.io.TlsContextOptions; import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.crt.ConnectionHealthConfiguration; import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration; import software.amazon.awssdk.http.crt.TlsVersion; import software.amazon.awssdk.utils.AttributeMap; class AwsCrtConfigurationUtilsTest { + + private static final String GATE_PROPERTY = "aws.enableDefaultReadTimeout2026"; + + @BeforeEach + @AfterEach + void clearGateProperty() { + System.clearProperty(GATE_PROPERTY); + } + @ParameterizedTest @MethodSource("cipherPreferences") void resolveCipherPreference_shouldResolveCorrectly(Boolean postQuantumTlsEnabled, @@ -120,6 +132,96 @@ void resolveMinTlsVersion_tls13_returnsTLSv1_3() { .isEqualTo(TlsContextOptions.TlsVersions.TLSv1_3); } + @ParameterizedTest(name = "{0} inactivity timeout -> {1}s failure interval") + @MethodSource("readWriteTimeoutMappings") + void mapReadWriteTimeout_mapsToOneBytePerSecondMonitor(Duration readWriteTimeout, int expectedIntervalSeconds) { + HttpMonitoringOptions options = AwsCrtConfigurationUtils.mapReadWriteTimeout(readWriteTimeout); + assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(1L); + assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(expectedIntervalSeconds); + } + + private static Stream readWriteTimeoutMappings() { + return Stream.of( + Arguments.of(Duration.ofMinutes(5), 300), + Arguments.of(Duration.ofMinutes(15), 900), + Arguments.of(Duration.ofSeconds(2), 2), + Arguments.of(Duration.ofSeconds(1), 2), + Arguments.of(Duration.ZERO, 2), + Arguments.of(Duration.ofMillis(2999), 2), + Arguments.of(Duration.ofMillis(3999), 3) + ); + } + + @Test + void resolveMonitoringOptions_explicitConnectionHealthConfiguration_winsOverFallback() { + HttpMonitoringOptions options = AwsCrtConfigurationUtils.resolveMonitoringOptions(healthConfiguration(), + Duration.ofMinutes(15)); + assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(500L); + assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(10); + } + + @Test + void resolveMonitoringOptions_serviceFallbackZero_appliesNothing() { + assertThat(AwsCrtConfigurationUtils.resolveMonitoringOptions(null, Duration.ZERO)).isNull(); + } + + @Test + void resolveMonitoringOptions_serviceFallbackZeroWithGateOn_appliesNothing() { + System.setProperty(GATE_PROPERTY, "true"); + assertThat(AwsCrtConfigurationUtils.resolveMonitoringOptions(null, Duration.ZERO)).isNull(); + } + + @Test + void resolveMonitoringOptions_serviceFallbackPositive_mapped() { + HttpMonitoringOptions options = AwsCrtConfigurationUtils.resolveMonitoringOptions(null, Duration.ofMinutes(15)); + assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(1L); + assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(900); + } + + @Test + void resolveMonitoringOptions_serviceFallbackPositive_ignoresGate() { + System.setProperty(GATE_PROPERTY, "false"); + HttpMonitoringOptions options = AwsCrtConfigurationUtils.resolveMonitoringOptions(null, Duration.ofMinutes(15)); + assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(1L); + assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(900); + } + + @Test + void resolveMonitoringOptions_standaloneGateOn_appliesDefault() { + System.setProperty(GATE_PROPERTY, "true"); + HttpMonitoringOptions options = AwsCrtConfigurationUtils.resolveMonitoringOptions(null, null); + assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(1L); + assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(300); + } + + @Test + void resolveMonitoringOptions_standaloneGateOff_appliesNothing() { + assertThat(AwsCrtConfigurationUtils.resolveMonitoringOptions(null, null)).isNull(); + } + + @Test + void resolveMonitoringOptions_standaloneGateOnWithExplicitConfig_usesExplicitConfig() { + System.setProperty(GATE_PROPERTY, "true"); + HttpMonitoringOptions options = AwsCrtConfigurationUtils.resolveMonitoringOptions(healthConfiguration(), null); + assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(500L); + assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(10); + } + + @Test + void standaloneGateSetting_matchesRolloutGateNames() { + assertThat(AwsCrtDefaultReadTimeoutSetting.ENABLE_DEFAULT_READ_TIMEOUT_2026.environmentVariable()) + .isEqualTo("AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026"); + assertThat(AwsCrtDefaultReadTimeoutSetting.ENABLE_DEFAULT_READ_TIMEOUT_2026.property()) + .isEqualTo("aws.enableDefaultReadTimeout2026"); + } + + private static ConnectionHealthConfiguration healthConfiguration() { + return ConnectionHealthConfiguration.builder() + .minimumThroughputInBps(500L) + .minimumThroughputTimeout(Duration.ofSeconds(10)) + .build(); + } + private static Stream defaultConnectionHealthConfigurationCases() { return Stream.of( From 415445cca5ff2b88d22ba7ed307c959407e74bc7 Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:11:05 -0700 Subject: [PATCH 3/6] feat: bake per-service read timeout exemptions (#7329) Codegen bakes each listed service's default read/write timeout tier into its generated serviceHttpConfig from a checked-in exemption artifact; aws-core applies the rollout gate to the baked value. --- .../DefaultCustomizationProcessor.java | 3 +- ...ultReadWriteTimeoutExemptionProcessor.java | 117 ++++++++ .../codegen/model/intermediate/Metadata.java | 20 ++ .../poet/builder/BaseClientBuilderClass.java | 42 ++- ...default-read-write-timeout-exemptions.json | 85 ++++++ ...eadWriteTimeoutExemptionProcessorTest.java | 147 ++++++++++ ...lientBuilderClassReadWriteTimeoutTest.java | 67 +++++ .../builder/BaseClientBuilderClassTest.java | 9 + ...-timeout-service-client-builder-class.java | 250 ++++++++++++++++++ .../builder/AwsDefaultClientBuilder.java | 48 +++- ...aultClientBuilderReadWriteTimeoutTest.java | 205 ++++++++++++++ 11 files changed, 979 insertions(+), 14 deletions(-) create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessor.java create mode 100644 codegen/src/main/resources/software/amazon/awssdk/codegen/default-read-write-timeout-exemptions.json create mode 100644 codegen/src/test/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessorTest.java create mode 100644 codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassReadWriteTimeoutTest.java create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-read-write-timeout-service-client-builder-class.java create mode 100644 core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilderReadWriteTimeoutTest.java diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultCustomizationProcessor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultCustomizationProcessor.java index 1511bf6be282..d0fbd360dbf3 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultCustomizationProcessor.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultCustomizationProcessor.java @@ -43,7 +43,8 @@ public static CodegenCustomizationProcessor getProcessorFor( new S3ControlRemoveAccountIdHostPrefixProcessor(), new ExplicitStringPayloadQueryProtocolProcessor(), new LowercaseShapeValidatorProcessor(), - new LongPollingOperationProcessor() + new LongPollingOperationProcessor(), + new DefaultReadWriteTimeoutExemptionProcessor() ); } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessor.java new file mode 100644 index 000000000000..87766c3cfb85 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessor.java @@ -0,0 +1,117 @@ +/* + * 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.codegen.customization.processors; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.SdkTestInternalApi; +import software.amazon.awssdk.codegen.customization.CodegenCustomizationProcessor; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.service.ServiceModel; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; +import software.amazon.awssdk.utils.Validate; + +/** + * Bakes the per-service default read/write inactivity timeout tier into the generated service HTTP config. The tiers come from a + * checked-in copy of the shared exemption artifact ({@code default-read-write-timeout-exemptions.json}), keyed by the service's + * sdkId ({@link software.amazon.awssdk.codegen.model.intermediate.Metadata#getServiceId()}). + * + *

An artifact value of {@code -1} marks a fully-exempt service (no default timeout applies); a positive value is the applied + * timeout in milliseconds. A service absent from the artifact has nothing baked, and {@code aws-core} supplies the flat default + * when the rollout gate is on. The rollout gate itself is applied later, in {@code aws-core}; this processor only bakes the + * per-service tier, which is the same regardless of whether the gate is on. + */ +public class DefaultReadWriteTimeoutExemptionProcessor implements CodegenCustomizationProcessor { + + private static final String EXEMPTIONS_RESOURCE = "software/amazon/awssdk/codegen/default-read-write-timeout-exemptions.json"; + + private static final Map SERVICE_ID_TO_TIMEOUT_MILLIS = loadExemptions(); + + private final Map serviceIdToTimeoutMillis; + + public DefaultReadWriteTimeoutExemptionProcessor() { + this(SERVICE_ID_TO_TIMEOUT_MILLIS); + } + + @SdkTestInternalApi + DefaultReadWriteTimeoutExemptionProcessor(Map serviceIdToTimeoutMillis) { + this.serviceIdToTimeoutMillis = serviceIdToTimeoutMillis; + } + + @Override + public void preprocess(ServiceModel serviceModel) { + // no-op + } + + @Override + public void postprocess(IntermediateModel intermediateModel) { + String serviceId = intermediateModel.getMetadata().getServiceId(); + Long timeoutMillis = serviceIdToTimeoutMillis.get(serviceId); + if (timeoutMillis != null) { + intermediateModel.getMetadata().setDefaultReadWriteTimeoutMillis(timeoutMillis); + } + } + + /** + * Fails if any artifact key does not match one of {@code knownServiceIds}. Matching is exact (case-sensitive), so a stale + * key (no such service) or a mis-cased key both surface here: either would otherwise silently leave the intended service + * unlisted and wrongly apply the flat default instead of its exempt/partial tier. + * + *

Codegen processes one service per run, so this whole-artifact cross-check cannot run inside {@link #postprocess} (a + * single run never sees every serviceId). It is invoked at build time by the coverage test against the full set of service + * sdkIds. + */ + void validateArtifactKeys(Set knownServiceIds) { + List unknownKeys = serviceIdToTimeoutMillis.keySet().stream() + .filter(key -> !knownServiceIds.contains(key)) + .sorted() + .collect(Collectors.toList()); + if (!unknownKeys.isEmpty()) { + throw new IllegalStateException( + "Read/write timeout exemption artifact " + EXEMPTIONS_RESOURCE + " contains key(s) matching no service sdkId " + + "(a stale or mis-cased key silently leaves that service unlisted): " + unknownKeys); + } + } + + private static Map loadExemptions() { + Map exemptions = new HashMap<>(); + try (InputStream stream = DefaultReadWriteTimeoutExemptionProcessor.class.getClassLoader() + .getResourceAsStream(EXEMPTIONS_RESOURCE)) { + Validate.notNull(stream, "Failed to load read/write timeout exemption artifact: %s", EXEMPTIONS_RESOURCE); + JsonNode root = JsonNodeParser.create().parse(stream); + root.asObject().forEach((serviceId, value) -> exemptions.put(serviceId, parseTimeoutMillis(serviceId, value))); + } catch (IOException e) { + throw new RuntimeException("Failed to read read/write timeout exemption artifact: " + EXEMPTIONS_RESOURCE, e); + } + return Collections.unmodifiableMap(exemptions); + } + + private static long parseTimeoutMillis(String serviceId, JsonNode value) { + try { + return Long.parseLong(value.asNumber()); + } catch (RuntimeException e) { + throw new IllegalArgumentException( + "Invalid numeric value for key '" + serviceId + "' in " + EXEMPTIONS_RESOURCE + ": " + value, e); + } + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/Metadata.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/Metadata.java index 07b5e319d60d..5916fb1ee4de 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/Metadata.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/Metadata.java @@ -115,6 +115,8 @@ public class Metadata { private String serviceId; + private Long defaultReadWriteTimeoutMillis; + private List auth; public List getAuth() { @@ -710,6 +712,24 @@ public Metadata withServiceId(String serviceId) { return this; } + /** + * The default read/write inactivity timeout baked for this service by the exemption processor, in milliseconds, or + * {@code null} when the service is not listed in the exemption artifact. A value of {@code -1} marks a fully-exempt service + * (no default timeout); a positive value is the applied timeout in milliseconds. + */ + public Long getDefaultReadWriteTimeoutMillis() { + return defaultReadWriteTimeoutMillis; + } + + public void setDefaultReadWriteTimeoutMillis(Long defaultReadWriteTimeoutMillis) { + this.defaultReadWriteTimeoutMillis = defaultReadWriteTimeoutMillis; + } + + public Metadata withDefaultReadWriteTimeoutMillis(Long defaultReadWriteTimeoutMillis) { + setDefaultReadWriteTimeoutMillis(defaultReadWriteTimeoutMillis); + return this; + } + public String getWaitersPackageName() { return waitersPackageName; } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java index 8e583ae119c3..77f4eb8485ee 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java @@ -31,6 +31,7 @@ import com.squareup.javapoet.TypeVariableName; import com.squareup.javapoet.WildcardTypeName; import java.net.URI; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -842,24 +843,27 @@ private void addServiceHttpConfigIfNeeded(TypeSpec.Builder builder, Intermediate String serviceDefaultFqcn = model.getCustomizationConfig().getServiceSpecificHttpConfig(); boolean supportsH2 = model.getMetadata().supportsH2(); boolean usePriorKnowledgeForH2 = model.getCustomizationConfig().isUsePriorKnowledgeForH2(); + Long readWriteTimeoutMillis = model.getMetadata().getDefaultReadWriteTimeoutMillis(); - if (serviceDefaultFqcn != null || supportsH2) { - builder.addMethod(serviceSpecificHttpConfigMethod(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2)); + if (serviceDefaultFqcn != null || supportsH2 || readWriteTimeoutMillis != null) { + builder.addMethod(serviceSpecificHttpConfigMethod(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2, + readWriteTimeoutMillis)); } } private MethodSpec serviceSpecificHttpConfigMethod(String serviceDefaultFqcn, boolean supportsH2, - boolean usePriorKnowledgeForH2) { + boolean usePriorKnowledgeForH2, Long readWriteTimeoutMillis) { return MethodSpec.methodBuilder("serviceHttpConfig") .addAnnotation(Override.class) .addModifiers(PROTECTED, FINAL) .returns(AttributeMap.class) - .addCode(serviceSpecificHttpConfigMethodBody(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2)) + .addCode(serviceSpecificHttpConfigMethodBody(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2, + readWriteTimeoutMillis)) .build(); } private CodeBlock serviceSpecificHttpConfigMethodBody(String serviceDefaultFqcn, boolean supportsH2, - boolean usePriorKnowledgeForH2) { + boolean usePriorKnowledgeForH2, Long readWriteTimeoutMillis) { CodeBlock.Builder builder = CodeBlock.builder(); if (serviceDefaultFqcn != null) { @@ -870,14 +874,28 @@ private CodeBlock serviceSpecificHttpConfigMethodBody(String serviceDefaultFqcn, builder.addStatement("$1T result = $1T.empty()", AttributeMap.class); } - if (supportsH2) { - builder.add("return result.merge(AttributeMap.builder()" - + ".put($T.PROTOCOL, $T.HTTP2)", - SdkHttpConfigurationOption.class, Protocol.class); + if (supportsH2 || readWriteTimeoutMillis != null) { + builder.add("return result.merge(AttributeMap.builder()"); - if (!usePriorKnowledgeForH2) { - builder.add(".put($T.PROTOCOL_NEGOTIATION, $T.ALPN)", - SdkHttpConfigurationOption.class, ProtocolNegotiation.class); + if (supportsH2) { + builder.add(".put($T.PROTOCOL, $T.HTTP2)", SdkHttpConfigurationOption.class, Protocol.class); + + if (!usePriorKnowledgeForH2) { + builder.add(".put($T.PROTOCOL_NEGOTIATION, $T.ALPN)", + SdkHttpConfigurationOption.class, ProtocolNegotiation.class); + } + } + + if (readWriteTimeoutMillis != null) { + // A negative artifact value marks a fully-exempt service: bake Duration.ZERO, which means apply no + // read/write timeout. A positive value is the timeout in milliseconds. + if (readWriteTimeoutMillis < 0) { + builder.add(".put($T.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, $T.ZERO)", + SdkHttpConfigurationOption.class, Duration.class); + } else { + builder.add(".put($T.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, $T.ofMillis($L))", + SdkHttpConfigurationOption.class, Duration.class, readWriteTimeoutMillis + "L"); + } } builder.addStatement(".build())"); diff --git a/codegen/src/main/resources/software/amazon/awssdk/codegen/default-read-write-timeout-exemptions.json b/codegen/src/main/resources/software/amazon/awssdk/codegen/default-read-write-timeout-exemptions.json new file mode 100644 index 000000000000..89bbc5e49239 --- /dev/null +++ b/codegen/src/main/resources/software/amazon/awssdk/codegen/default-read-write-timeout-exemptions.json @@ -0,0 +1,85 @@ +{ + "Bedrock Runtime": -1, + "CloudSearch Domain": -1, + "codeartifact": -1, + "ConnectHealth": -1, + "EBS": -1, + "Glacier": -1, + "Lambda": -1, + "Lex Runtime Service": -1, + "Lex Runtime V2": -1, + "MediaStore Data": -1, + "Omics": -1, + "Polly": -1, + "QBusiness": -1, + "S3": -1, + "SageMaker Runtime HTTP2": -1, + "Transcribe Streaming": -1, + "b2bi": 900000, + "Bedrock Agent Runtime": 900000, + "Bedrock AgentCore": 900000, + "Bedrock Data Automation Runtime": 900000, + "Data Pipeline": 900000, + "DataExchange": 900000, + "ECS": 900000, + "Glue": 900000, + "Kinesis": 900000, + "Kinesis Analytics V2": 900000, + "Kinesis Video Archived Media": 900000, + "Kinesis Video Media": 900000, + "Kinesis Video Signaling": 900000, + "Kinesis Video WebRTC Storage": 900000, + "Neptune Graph": 900000, + "neptunedata": 900000, + "Nova Act": 900000, + "QApps": 900000, + "QConnect": 900000, + "QuickSight": 900000, + "SageMaker Runtime": 900000, + "SagemakerJobRuntime": 900000, + "SFN": 900000, + "SQS": 900000, + "SWF": 900000, + "Timestream Query": 900000, + "Wisdom": 900000, + "API Gateway": 900000, + "ApiGatewayV2": 900000, + "AppIntegrations": 900000, + "AppStream": 900000, + "Athena": 900000, + "Auto Scaling": 900000, + "Batch": 900000, + "Bedrock": 900000, + "Bedrock Agent": 900000, + "Bedrock AgentCore Control": 900000, + "CloudFormation": 900000, + "CloudWatch": 900000, + "CodeBuild": 900000, + "CodeCatalyst": 900000, + "CodeDeploy": 900000, + "Config Service": 900000, + "Connect": 900000, + "DataBrew": 900000, + "DataZone": 900000, + "Device Farm": 900000, + "EC2": 900000, + "Elastic Load Balancing v2": 900000, + "EMR Serverless": 900000, + "GameLift": 900000, + "GameLiftStreams": 900000, + "IoT": 900000, + "IoT Data Plane": 900000, + "IoT Jobs Data Plane": 900000, + "IoTSecureTunneling": 900000, + "Lex Model Building Service": 900000, + "Lex Models V2": 900000, + "mgn": 900000, + "RDS": 900000, + "RDS Data": 900000, + "RTBFabric": 900000, + "SageMaker": 900000, + "SSM": 900000, + "Storage Gateway": 900000, + "WorkSpaces": 900000, + "WorkSpaces Web": 900000 +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessorTest.java new file mode 100644 index 000000000000..eb42cc3ca5e8 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessorTest.java @@ -0,0 +1,147 @@ +/* + * 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.codegen.customization.processors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.intermediate.Metadata; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; + +class DefaultReadWriteTimeoutExemptionProcessorTest { + + private static final String EXEMPTIONS_RESOURCE = "software/amazon/awssdk/codegen/default-read-write-timeout-exemptions.json"; + private static final Pattern SERVICE_ID = Pattern.compile("\"serviceId\"\\s*:\\s*\"([^\"]+)\""); + + @ParameterizedTest + @MethodSource("exemptionEntries") + void postprocess_serviceInArtifact_bakesExpectedTier(String serviceId, long expectedMillis) { + IntermediateModel model = modelWithServiceId(serviceId); + + new DefaultReadWriteTimeoutExemptionProcessor().postprocess(model); + + assertThat(model.getMetadata().getDefaultReadWriteTimeoutMillis()).isEqualTo(expectedMillis); + } + + @ParameterizedTest + @ValueSource(strings = {"sqs", "s3", "lambda", "kinesis", "CODEARTIFACT", "MGN", "Sqs"}) + void postprocess_misCasedServiceId_bakesNothing(String misCasedServiceId) { + IntermediateModel model = modelWithServiceId(misCasedServiceId); + + new DefaultReadWriteTimeoutExemptionProcessor().postprocess(model); + + assertThat(model.getMetadata().getDefaultReadWriteTimeoutMillis()).isNull(); + } + + @Test + void postprocess_serviceNotInArtifact_bakesNothing() { + IntermediateModel model = modelWithServiceId("Not A Real Service"); + + new DefaultReadWriteTimeoutExemptionProcessor().postprocess(model); + + assertThat(model.getMetadata().getDefaultReadWriteTimeoutMillis()).isNull(); + } + + @Test + void validateArtifactKeys_everyKeyMatchesARealServiceId() throws IOException { + new DefaultReadWriteTimeoutExemptionProcessor().validateArtifactKeys(realServiceIds()); + } + + @Test + void validateArtifactKeys_keyMatchingNoServiceId_throws() { + DefaultReadWriteTimeoutExemptionProcessor processor = + new DefaultReadWriteTimeoutExemptionProcessor(Collections.singletonMap("sqs", 900000L)); + + assertThatThrownBy(() -> processor.validateArtifactKeys(Collections.singleton("SQS"))) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("sqs"); + } + + private static IntermediateModel modelWithServiceId(String serviceId) { + IntermediateModel model = new IntermediateModel(); + model.setMetadata(new Metadata().withServiceId(serviceId)); + return model; + } + + private static Stream exemptionEntries() throws IOException { + try (InputStream stream = DefaultReadWriteTimeoutExemptionProcessorTest.class.getClassLoader() + .getResourceAsStream(EXEMPTIONS_RESOURCE)) { + Map artifact = JsonNodeParser.create().parse(stream).asObject(); + return artifact.entrySet().stream() + .map(e -> Arguments.of(e.getKey(), Long.parseLong(e.getValue().asNumber()))); + } + } + + private static Set realServiceIds() throws IOException { + Path servicesDir = locateServicesDir(); + Set serviceIds = new HashSet<>(); + try (DirectoryStream modules = Files.newDirectoryStream(servicesDir)) { + for (Path module : modules) { + Path model = module.resolve("src/main/resources/codegen-resources/service-2.json"); + if (Files.isRegularFile(model)) { + extractServiceId(model).ifPresent(serviceIds::add); + } + } + } + assertThat(serviceIds).as("expected to harvest serviceIds from the service models").isNotEmpty(); + return serviceIds; + } + + private static Optional extractServiceId(Path serviceModel) throws IOException { + try (BufferedReader reader = Files.newBufferedReader(serviceModel, StandardCharsets.UTF_8)) { + String line; + while ((line = reader.readLine()) != null) { + Matcher matcher = SERVICE_ID.matcher(line); + if (matcher.find()) { + return Optional.of(matcher.group(1)); + } + } + } + return Optional.empty(); + } + + private static Path locateServicesDir() { + for (Path candidate : new Path[] {Paths.get("..", "services"), Paths.get("services"), Paths.get("..", "..", "services")}) { + if (Files.isDirectory(candidate)) { + return candidate; + } + } + throw new IllegalStateException("Could not locate the services/ directory from " + Paths.get("").toAbsolutePath()); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassReadWriteTimeoutTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassReadWriteTimeoutTest.java new file mode 100644 index 000000000000..9dabf03da41e --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassReadWriteTimeoutTest.java @@ -0,0 +1,67 @@ +/* + * 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.codegen.poet.builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.codegen.poet.PoetUtils.buildJavaFile; + +import java.io.IOException; +import java.io.UncheckedIOException; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.poet.ClientTestModels; + +/** + * Emission cases for the codegen-baked read/write timeout tier. + */ +class BaseClientBuilderClassReadWriteTimeoutTest { + + @Test + void serviceHttpConfig_fullyExemptTier_bakesDurationZero() { + IntermediateModel model = ClientTestModels.queryServiceModels(); + model.getMetadata().setDefaultReadWriteTimeoutMillis(-1L); + + assertThat(generate(model)) + .contains(".put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, Duration.ZERO)"); + } + + @Test + void serviceHttpConfig_partialTier_bakesDurationMillis() { + IntermediateModel model = ClientTestModels.queryServiceModels(); + model.getMetadata().setDefaultReadWriteTimeoutMillis(900000L); + + assertThat(generate(model)) + .contains(".put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, Duration.ofMillis(900000L))"); + } + + @Test + void serviceHttpConfig_noTierBaked_omitsFallbackTimeout() { + IntermediateModel model = ClientTestModels.queryServiceModels(); + model.getMetadata().setDefaultReadWriteTimeoutMillis(null); + + assertThat(generate(model)).doesNotContain("SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT"); + } + + private static String generate(IntermediateModel model) { + StringBuilder output = new StringBuilder(); + try { + buildJavaFile(new BaseClientBuilderClass(model)).writeTo(output); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return output.toString(); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java index 3a0f184b1462..a48a74c9163c 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java @@ -107,6 +107,15 @@ void baseClientBuilderClass_noRegionEndpointRules() { "test-no-region-client-builder-class.java"); } + @Test + void baseClientBuilderClassWithReadWriteTimeout() { + IntermediateModel model = serviceWithH2(); + // Simulate the exemption processor baking a 15-minute partial tier; verify it composes with the existing H2 + // serviceHttpConfig content. + model.getMetadata().setDefaultReadWriteTimeoutMillis(900000L); + validateBaseClientBuilderClassGeneration(model, "test-read-write-timeout-service-client-builder-class.java"); + } + private void validateBaseClientBuilderClassGeneration(IntermediateModel model, String expectedClassName) { validateGeneration(BaseClientBuilderClass::new, model, expectedClassName); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-read-write-timeout-service-client-builder-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-read-write-timeout-service-client-builder-class.java new file mode 100644 index 000000000000..765fa5789e03 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-read-write-timeout-service-client-builder-class.java @@ -0,0 +1,250 @@ +package software.amazon.awssdk.services.h2; + +import java.net.URI; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.awscore.auth.AuthSchemePreferenceResolver; +import software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder; +import software.amazon.awssdk.awscore.client.config.AwsClientOption; +import software.amazon.awssdk.awscore.endpoint.AwsClientEndpointProvider; +import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute; +import software.amazon.awssdk.awscore.endpoints.authscheme.EndpointAuthScheme; +import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4AuthScheme; +import software.amazon.awssdk.awscore.retry.AwsRetryStrategy; +import software.amazon.awssdk.core.ClientEndpointProvider; +import software.amazon.awssdk.core.SdkPlugin; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.interceptor.ClasspathInterceptorChainFactory; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.http.Protocol; +import software.amazon.awssdk.http.ProtocolNegotiation; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.auth.aws.scheme.AwsV4AuthScheme; +import software.amazon.awssdk.http.auth.scheme.NoAuthAuthScheme; +import software.amazon.awssdk.http.auth.spi.scheme.AuthScheme; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.identity.spi.IdentityProviders; +import software.amazon.awssdk.protocols.json.internal.unmarshall.SdkClientJsonProtocolAdvancedOption; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.retries.api.RetryStrategy; +import software.amazon.awssdk.services.h2.auth.scheme.H2AuthSchemeProvider; +import software.amazon.awssdk.services.h2.endpoints.H2EndpointParams; +import software.amazon.awssdk.services.h2.endpoints.H2EndpointProvider; +import software.amazon.awssdk.services.h2.internal.H2ServiceClientConfigurationBuilder; +import software.amazon.awssdk.utils.AttributeMap; +import software.amazon.awssdk.utils.CollectionUtils; +import software.amazon.awssdk.utils.CompletableFutureUtils; + +/** + * Internal base class for {@link DefaultH2ClientBuilder} and {@link DefaultH2AsyncClientBuilder}. + */ +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +abstract class DefaultH2BaseClientBuilder, C> extends AwsDefaultClientBuilder { + private final Map> additionalAuthSchemes = new HashMap<>(); + + @Override + protected final String serviceEndpointPrefix() { + return "h2-service"; + } + + @Override + protected final String serviceName() { + return "H2"; + } + + @Override + protected final SdkClientConfiguration mergeServiceDefaults(SdkClientConfiguration config) { + return config.merge(c -> { + c.option(SdkClientOption.ENDPOINT_PROVIDER, defaultEndpointProvider()) + .option(SdkClientOption.AUTH_SCHEME_PROVIDER, defaultAuthSchemeProvider(config)) + .option(SdkClientOption.AUTH_SCHEMES, authSchemes()) + .option(SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED, false); + }); + } + + @Override + protected final SdkClientConfiguration finalizeServiceConfiguration(SdkClientConfiguration config) { + List endpointInterceptors = new ArrayList<>(); + ClasspathInterceptorChainFactory interceptorFactory = new ClasspathInterceptorChainFactory(); + List interceptors = interceptorFactory + .getInterceptors("software/amazon/awssdk/services/h2/execution.interceptors"); + List additionalInterceptors = new ArrayList<>(); + interceptors = CollectionUtils.mergeLists(endpointInterceptors, interceptors); + interceptors = CollectionUtils.mergeLists(interceptors, additionalInterceptors); + interceptors = CollectionUtils.mergeLists(interceptors, config.option(SdkClientOption.EXECUTION_INTERCEPTORS)); + SdkClientConfiguration.Builder builder = config.toBuilder(); + builder.lazyOption(SdkClientOption.IDENTITY_PROVIDERS, c -> { + IdentityProviders.Builder result = IdentityProviders.builder(); + IdentityProvider credentialsIdentityProvider = c.get(AwsClientOption.CREDENTIALS_IDENTITY_PROVIDER); + if (credentialsIdentityProvider != null) { + result.putIdentityProvider(credentialsIdentityProvider); + } + return result.build(); + }); + builder.option(SdkClientOption.EXECUTION_INTERCEPTORS, interceptors); + builder.lazyOptionIfAbsent( + SdkClientOption.CLIENT_ENDPOINT_PROVIDER, + c -> { + Optional overrideEndpoint = AwsClientEndpointProvider.builder() + .serviceEndpointOverrideEnvironmentVariable("AWS_ENDPOINT_URL_H2_SERVICE") + .serviceEndpointOverrideSystemProperty("aws.endpointUrlH2").serviceProfileProperty("h2_service") + .profileFile(c.get(SdkClientOption.PROFILE_FILE_SUPPLIER)) + .profileName(c.get(SdkClientOption.PROFILE_NAME)).resolveFromOverrides(); + if (overrideEndpoint.isPresent()) { + return ClientEndpointProvider.create(overrideEndpoint.get(), true); + } + URI clientEndpointUri = null; + Region region = c.get(AwsClientOption.AWS_REGION); + try { + H2EndpointParams endpointParams = H2EndpointParams.builder().region(region).build(); + Endpoint endpoint = CompletableFutureUtils.joinLikeSync(defaultEndpointProvider().resolveEndpoint( + endpointParams)); + clientEndpointUri = endpoint.url(); + } catch (Exception e) { + // Endpoint resolution failed. This is expected for services with required parameters + // beyond region, dualstack, and FIPS. Use a placeholder that will be replaced at request time. + return ClientEndpointProvider.create(URI.create("https://localhost"), false); + } + if (clientEndpointUri.getHost() == null) { + throw SdkClientException.create("Configured region (" + region + ") resulted in an invalid URI: " + + clientEndpointUri + ". This is usually caused by an invalid region configuration."); + } + return ClientEndpointProvider.create(clientEndpointUri, false); + }); + builder.lazyOptionIfAbsent( + AwsClientOption.SIGNING_REGION, + c -> { + Region region = c.get(AwsClientOption.AWS_REGION); + try { + H2EndpointParams endpointParams = H2EndpointParams.builder().region(region).build(); + Endpoint endpoint = CompletableFutureUtils.joinLikeSync(defaultEndpointProvider().resolveEndpoint( + endpointParams)); + List authSchemes = endpoint.attribute(AwsEndpointAttribute.AUTH_SCHEMES); + if (authSchemes != null && !authSchemes.isEmpty()) { + EndpointAuthScheme firstScheme = authSchemes.get(0); + if (firstScheme instanceof SigV4AuthScheme) { + String signingRegion = ((SigV4AuthScheme) firstScheme).signingRegion(); + if (signingRegion != null) { + return Region.of(signingRegion); + } + } + } + } catch (Exception e) { + // Endpoint resolution failed. Fall back to using the client region as signing region. + } + return region; + }); + builder.option(SdkClientJsonProtocolAdvancedOption.ENABLE_FAST_UNMARSHALLER, true); + return builder.build(); + } + + @Override + protected final String signingName() { + return "h2-service"; + } + + private H2EndpointProvider defaultEndpointProvider() { + return H2EndpointProvider.defaultProvider(); + } + + public B authSchemeProvider(H2AuthSchemeProvider authSchemeProvider) { + clientConfiguration.option(SdkClientOption.AUTH_SCHEME_PROVIDER, authSchemeProvider); + return thisBuilder(); + } + + private H2AuthSchemeProvider defaultAuthSchemeProvider(SdkClientConfiguration config) { + AuthSchemePreferenceResolver authSchemePreferenceProvider = AuthSchemePreferenceResolver.builder() + .profileFile(config.option(SdkClientOption.PROFILE_FILE_SUPPLIER)) + .profileName(config.option(SdkClientOption.PROFILE_NAME)).build(); + List preferences = authSchemePreferenceProvider.resolveAuthSchemePreference(); + if (!preferences.isEmpty()) { + return H2AuthSchemeProvider.defaultProvider(preferences); + } + return H2AuthSchemeProvider.defaultProvider(); + } + + @Override + public B putAuthScheme(AuthScheme authScheme) { + additionalAuthSchemes.put(authScheme.schemeId(), authScheme); + return thisBuilder(); + } + + private Map> authSchemes() { + Map> schemes = new HashMap<>(2 + this.additionalAuthSchemes.size()); + AwsV4AuthScheme awsV4AuthScheme = AwsV4AuthScheme.create(); + schemes.put(awsV4AuthScheme.schemeId(), awsV4AuthScheme); + NoAuthAuthScheme noAuthAuthScheme = NoAuthAuthScheme.create(); + schemes.put(noAuthAuthScheme.schemeId(), noAuthAuthScheme); + schemes.putAll(this.additionalAuthSchemes); + return schemes; + } + + @Override + protected final AttributeMap serviceHttpConfig() { + AttributeMap result = AttributeMap.empty(); + return result.merge(AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2) + .put(SdkHttpConfigurationOption.PROTOCOL_NEGOTIATION, ProtocolNegotiation.ALPN) + .put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, Duration.ofMillis(900000L)).build()); + } + + @Override + protected SdkClientConfiguration invokePlugins(SdkClientConfiguration config) { + List internalPlugins = internalPlugins(config); + List externalPlugins = plugins(); + if (internalPlugins.isEmpty() && externalPlugins.isEmpty()) { + return config; + } + List plugins = CollectionUtils.mergeLists(internalPlugins, externalPlugins); + SdkClientConfiguration.Builder configuration = config.toBuilder(); + H2ServiceClientConfigurationBuilder serviceConfigBuilder = new H2ServiceClientConfigurationBuilder(configuration); + for (SdkPlugin plugin : plugins) { + plugin.configureClient(serviceConfigBuilder); + } + updateRetryStrategyClientConfiguration(configuration); + return configuration.build(); + } + + private void updateRetryStrategyClientConfiguration(SdkClientConfiguration.Builder configuration) { + ClientOverrideConfiguration.Builder builder = configuration.asOverrideConfigurationBuilder(); + RetryMode retryMode = builder.retryMode(); + if (retryMode != null) { + configuration.option(SdkClientOption.RETRY_STRATEGY, AwsRetryStrategy.forRetryMode(retryMode)); + } else { + Consumer> configurator = builder.retryStrategyConfigurator(); + if (configurator != null) { + RetryStrategy.Builder defaultBuilder = AwsRetryStrategy.defaultRetryStrategy().toBuilder(); + configurator.accept(defaultBuilder); + configuration.option(SdkClientOption.RETRY_STRATEGY, defaultBuilder.build()); + } else { + RetryStrategy retryStrategy = builder.retryStrategy(); + if (retryStrategy != null) { + configuration.option(SdkClientOption.RETRY_STRATEGY, retryStrategy); + } + } + } + configuration.option(SdkClientOption.CONFIGURED_RETRY_MODE, null); + configuration.option(SdkClientOption.CONFIGURED_RETRY_STRATEGY, null); + configuration.option(SdkClientOption.CONFIGURED_RETRY_CONFIGURATOR, null); + } + + private List internalPlugins(SdkClientConfiguration config) { + return Collections.emptyList(); + } + + protected static void validateClientOptions(SdkClientConfiguration c) { + } +} diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java index 92036575eeb1..2f4a77e7e9a0 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java @@ -21,6 +21,7 @@ import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_STRATEGY; import java.net.URI; +import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -50,6 +51,7 @@ import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.core.http.EnableDefaultReadTimeout2026Resolver; import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.core.internal.SdkInternalTestAdvancedClientOption; import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy; @@ -57,6 +59,7 @@ import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; import software.amazon.awssdk.identity.spi.IdentityProvider; @@ -99,6 +102,12 @@ public abstract class AwsDefaultClientBuilder */ private AttributeMap resolveHttpClientConfig(LazyValueSource config) { - AttributeMap attributeMap = serviceHttpConfig(); + AttributeMap attributeMap = applyDefaultReadWriteTimeout(config, serviceHttpConfig()); return mergeSmartHttpDefaults(config, attributeMap); } + /** + * Applies the {@code AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} rollout gate to the codegen-baked + * {@link SdkHttpConfigurationOption#SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT} contributed by {@link #serviceHttpConfig()}. + * The gate resolves from the {@code AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} environment variable/system property, else the + * codegen-baked {@link SdkClientOption#DEFAULT_ENABLE_READ_TIMEOUT_2026} default, else off. + * + *

When the gate is on, an unlisted service (nothing baked) gets the flat 5-minute default and a baked tier is kept as-is + * ({@link Duration#ZERO} for fully-exempt, 15 minutes for partial). When the gate is off, a baked positive tier (partial) + * is forced to {@link Duration#ZERO} so it cannot apply; otherwise the option is left untouched. Leaving it absent for an + * unlisted, gated-off service is equivalent to {@link Duration#ZERO}: the gate being off means the environment variable is + * not truthy, so the HTTP client's option-absent path applies nothing either way. + */ + private AttributeMap applyDefaultReadWriteTimeout(LazyValueSource config, AttributeMap serviceHttpConfig) { + boolean gateEnabled = new EnableDefaultReadTimeout2026Resolver() + .defaultEnableReadTimeout2026(config.get(SdkClientOption.DEFAULT_ENABLE_READ_TIMEOUT_2026)) + .resolve(); + + Duration bakedTier = serviceHttpConfig.get(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT); + + if (gateEnabled) { + if (bakedTier != null) { + return serviceHttpConfig; + } + return serviceHttpConfig.toBuilder() + .put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, + DEFAULT_READ_WRITE_TIMEOUT) + .build(); + } + + if (bakedTier != null && !bakedTier.isZero()) { + return serviceHttpConfig.toBuilder() + .put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, Duration.ZERO) + .build(); + } + return serviceHttpConfig; + } + /** * Optionally overridden by child classes to define service-specific HTTP configuration defaults. */ diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilderReadWriteTimeoutTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilderReadWriteTimeoutTest.java new file mode 100644 index 000000000000..9bdec6b844a0 --- /dev/null +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilderReadWriteTimeoutTest.java @@ -0,0 +1,205 @@ +/* + * 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.awscore.client.builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static software.amazon.awssdk.awscore.client.config.AwsAdvancedClientOption.ENABLE_DEFAULT_REGION_DETECTION; + +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.awscore.client.config.AwsClientOption; +import software.amazon.awssdk.awscore.internal.defaultsmode.AutoDefaultsModeDiscovery; +import software.amazon.awssdk.core.ClientEndpointProvider; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.utils.AttributeMap; + +/** + * Verifies the {@code AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} rollout gate applied in + * {@link AwsDefaultClientBuilder#resolveHttpClientConfig} to the codegen-baked + * {@link SdkHttpConfigurationOption#SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT}. + */ +@ExtendWith(MockitoExtension.class) +class AwsDefaultClientBuilderReadWriteTimeoutTest { + + private static final String GATE_PROPERTY = SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property(); + private static final Duration PARTIAL_TIER = Duration.ofMinutes(15); + private static final Duration FLAT_DEFAULT = Duration.ofMinutes(5); + + @Mock(lenient = true) + private SdkHttpClient.Builder defaultHttpClientBuilder; + + @Mock(lenient = true) + private SdkAsyncHttpClient.Builder defaultAsyncHttpClientFactory; + + @Mock(lenient = true) + private AutoDefaultsModeDiscovery autoModeDiscovery; + + private String savedProperty; + + @BeforeEach + void setup() { + savedProperty = System.getProperty(GATE_PROPERTY); + System.clearProperty(GATE_PROPERTY); + } + + @AfterEach + void teardown() { + if (savedProperty != null) { + System.setProperty(GATE_PROPERTY, savedProperty); + } else { + System.clearProperty(GATE_PROPERTY); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("gateScenarios") + void resolvesReadWriteTimeout(String scenario, String gateProperty, boolean codegenGateDefault, + Duration bakedTier, Duration expected) { + if (gateProperty != null) { + System.setProperty(GATE_PROPERTY, gateProperty); + } + AttributeMap serviceHttpConfig = bakedTier == null ? AttributeMap.empty() : bakedServiceHttpConfig(bakedTier); + + AttributeMap resolved = resolvedServiceDefaults(serviceHttpConfig, codegenGateDefault); + + assertThat(resolved.get(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT)).isEqualTo(expected); + } + + @Test + void unlistedService_gateOff_leavesOptionAbsent() { + AttributeMap resolved = resolvedServiceDefaults(AttributeMap.empty(), false); + + assertThat(resolved.containsKey(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT)).isFalse(); + } + + private static Stream gateScenarios() { + return Stream.of( + Arguments.of("gate off, partial tier baked -> overridden to ZERO", null, false, PARTIAL_TIER, Duration.ZERO), + Arguments.of("gate on, unlisted service -> flat 5-minute default", "true", false, null, FLAT_DEFAULT), + Arguments.of("gate on, fully-exempt tier baked -> ZERO", "true", false, Duration.ZERO, Duration.ZERO), + Arguments.of("gate on, partial tier baked -> 15 minutes", "true", false, PARTIAL_TIER, PARTIAL_TIER), + Arguments.of("gate on via codegen default, unlisted -> flat 5-minute default", null, true, null, FLAT_DEFAULT), + Arguments.of("gate property false overrides codegen default -> ZERO", "false", true, PARTIAL_TIER, Duration.ZERO) + ); + } + + private static AttributeMap bakedServiceHttpConfig(Duration tier) { + return AttributeMap.builder() + .put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, tier) + .build(); + } + + private AttributeMap resolvedServiceDefaults(AttributeMap bakedServiceHttpConfig, boolean codegenGateDefault) { + AtomicReference captured = new AtomicReference<>(); + ClientOverrideConfiguration overrideConfig = + ClientOverrideConfiguration.builder() + .putAdvancedOption(ENABLE_DEFAULT_REGION_DETECTION, false) + .build(); + + new TestClientBuilder(bakedServiceHttpConfig, codegenGateDefault) + .credentialsProvider(AnonymousCredentialsProvider.create()) + .overrideConfiguration(overrideConfig) + .region(Region.US_WEST_1) + .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> { + captured.set(serviceDefaults); + return mock(SdkHttpClient.class); + }) + .build(); + + return captured.get(); + } + + private static class TestClient { + } + + private class TestClientBuilder extends AwsDefaultClientBuilder + implements AwsClientBuilder { + + private final AttributeMap bakedServiceHttpConfig; + private final boolean codegenGateDefault; + + TestClientBuilder(AttributeMap bakedServiceHttpConfig, boolean codegenGateDefault) { + super(defaultHttpClientBuilder, defaultAsyncHttpClientFactory, autoModeDiscovery); + this.bakedServiceHttpConfig = bakedServiceHttpConfig; + this.codegenGateDefault = codegenGateDefault; + } + + @Override + protected TestClient buildClient() { + syncClientConfiguration(); + return new TestClient(); + } + + @Override + protected SdkClientConfiguration mergeInternalDefaults(SdkClientConfiguration config) { + if (!codegenGateDefault) { + return config; + } + return config.merge(c -> c.option(SdkClientOption.DEFAULT_ENABLE_READ_TIMEOUT_2026, true)); + } + + @Override + protected SdkClientConfiguration finalizeServiceConfiguration(SdkClientConfiguration config) { + return config.toBuilder() + .lazyOptionIfAbsent(SdkClientOption.CLIENT_ENDPOINT_PROVIDER, c -> { + URI endpoint = URI.create("https://" + serviceEndpointPrefix() + "." + + c.get(AwsClientOption.AWS_REGION) + ".amazonaws.com"); + return ClientEndpointProvider.create(endpoint, false); + }) + .build(); + } + + @Override + protected AttributeMap serviceHttpConfig() { + return bakedServiceHttpConfig; + } + + @Override + protected String serviceEndpointPrefix() { + return "test"; + } + + @Override + protected String signingName() { + return "test"; + } + + @Override + protected String serviceName() { + return "test"; + } + } +} From eb81725273f870075b3f6a61542def4c39bfd042 Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:21:46 -0700 Subject: [PATCH 4/6] [Default Read/Write Timeout 4/N] Scope the default read/write timeout to SDK-managed clients (#7334) * fix: scope CRT default timeout to managed clients A directly-supplied CRT client is caller-owned (buildWithDefaults is not called on it) and is often shared across services with different exemption tiers, so applying a flat default could break an exempt service. The client no longer reads the opt-in setting itself; the default now applies only to SDK-managed clients, via the option resolved in aws-core. * docs: clarify CRT default timeout scope in Javadoc Document that the CRT builder's connectionHealthConfiguration applies an automatic default only for SDK-managed clients; a directly-supplied or standalone client gets no default and honors only an explicit configuration. --- .../http/crt/AwsCrtAsyncHttpClient.java | 6 +- .../awssdk/http/crt/AwsCrtHttpClient.java | 6 +- .../internal/AwsCrtConfigurationUtils.java | 21 ++----- .../AwsCrtDefaultReadTimeoutSetting.java | 45 --------------- .../AwsCrtConfigurationUtilsTest.java | 55 +++---------------- 5 files changed, 21 insertions(+), 112 deletions(-) delete mode 100644 http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtDefaultReadTimeoutSetting.java 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 c2b8cc7e0897..79a150eabab8 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 @@ -176,8 +176,10 @@ public interface Builder extends SdkAsyncHttpClient.BuilderAbsent an explicit configuration here, a default read/write inactivity timeout may apply; a configuration set here - * always takes precedence over that default. + *

When this client is created and managed by an AWS SDK service client, a default read/write inactivity timeout + * may be applied automatically, resolved per service by the SDK. When this client is built directly and supplied to a + * service client, or used standalone, no automatic default is applied; monitoring is enabled only by an explicit + * configuration set here, which always takes precedence over any SDK-applied default. * * @param healthChecksConfiguration The health checks config to use * @return The builder of the method chaining. diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java index 4e21ecaa0909..9fe64121b2ca 100644 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java @@ -232,8 +232,10 @@ public interface Builder extends SdkHttpClient.Builder * is shut down only after {@code yourDuration} elapses with no bytes transferred. The timeout has whole-second * granularity and must be at least two seconds. * - *

Absent an explicit configuration here, a default read/write inactivity timeout may apply; a configuration set here - * always takes precedence over that default. + *

When this client is created and managed by an AWS SDK service client, a default read/write inactivity timeout + * may be applied automatically, resolved per service by the SDK. When this client is built directly and supplied to a + * service client, or used standalone, no automatic default is applied; monitoring is enabled only by an explicit + * configuration set here, which always takes precedence over any SDK-applied default. * * @param healthChecksConfiguration The health checks config to use * @return The builder of the method chaining. diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java index cc5f06d60f85..ce03372ae257 100644 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java +++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java @@ -38,10 +38,6 @@ public final class AwsCrtConfigurationUtils { // CRT rejects a throughput-failure interval below two seconds (HttpMonitoringOptions). private static final int MIN_MONITORING_FAILURE_INTERVAL_SECONDS = 2; - // The default read/write inactivity timeout applied on the standalone path (a CRT client not built by a service client) - // when the rollout gate is on. On the service-client path this default is supplied above the transport, in aws-core. - private static final Duration STANDALONE_DEFAULT_READ_WRITE_TIMEOUT = Duration.ofMinutes(5); - private AwsCrtConfigurationUtils() { } @@ -49,11 +45,10 @@ private AwsCrtConfigurationUtils() { * Resolves the throughput monitor that enforces a connection's read/write inactivity timeout, highest precedence first: *

    *
  1. an explicit {@link ConnectionHealthConfiguration} always wins;
  2. - *
  3. on the service-client path, the {@code fallbackTimeout}, which aws-core has already resolved against the rollout - * gate, so a present value is post-gate: {@link Duration#ZERO} applies nothing, a positive value is mapped;
  4. - *
  5. on the standalone path ({@code fallbackTimeout} null, no aws-core resolution ran), the rollout gate is read - * directly, applying the default timeout when it is on and nothing otherwise.
  6. + *
  7. otherwise the SDK-resolved {@code fallbackTimeout}: {@link Duration#ZERO} applies nothing, a positive value is + * mapped.
  8. *
+ * The fallback is only ever supplied to an SDK-managed client. */ public static HttpMonitoringOptions resolveMonitoringOptions(ConnectionHealthConfiguration healthConfiguration, Duration fallbackTimeout) { @@ -61,14 +56,10 @@ public static HttpMonitoringOptions resolveMonitoringOptions(ConnectionHealthCon return CrtConfigurationUtils.resolveHttpMonitoringOptions(healthConfiguration).orElse(null); } - if (fallbackTimeout != null) { - return fallbackTimeout.isZero() ? null : mapReadWriteTimeout(fallbackTimeout); - } - - if (AwsCrtDefaultReadTimeoutSetting.ENABLE_DEFAULT_READ_TIMEOUT_2026.getBooleanValue().orElse(false)) { - return mapReadWriteTimeout(STANDALONE_DEFAULT_READ_WRITE_TIMEOUT); + if (fallbackTimeout == null || fallbackTimeout.isZero()) { + return null; } - return null; + return mapReadWriteTimeout(fallbackTimeout); } /** diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtDefaultReadTimeoutSetting.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtDefaultReadTimeoutSetting.java deleted file mode 100644 index 3e032347e1ff..000000000000 --- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtDefaultReadTimeoutSetting.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.internal; - -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.utils.SystemSetting; - -/** - * The rollout gate the standalone (non-service-client) CRT HTTP Client reads to decide whether to apply the default read/write - * inactivity timeout. It mirrors {@code SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} in sdk-core, using the same - * environment variable and system property; the CRT client reads them directly because it cannot depend on sdk-core. This is - * needed to ensure a timeout is configured when a standalone HTTP client is created. - */ -@SdkInternalApi -public enum AwsCrtDefaultReadTimeoutSetting implements SystemSetting { - ENABLE_DEFAULT_READ_TIMEOUT_2026; - - @Override - public String property() { - return "aws.enableDefaultReadTimeout2026"; - } - - @Override - public String environmentVariable() { - return "AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026"; - } - - @Override - public String defaultValue() { - return null; - } -} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java index ea0d4e960de8..1ca5ad6d1173 100644 --- a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java @@ -21,8 +21,6 @@ import java.time.Duration; import java.util.stream.Stream; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -39,14 +37,6 @@ class AwsCrtConfigurationUtilsTest { - private static final String GATE_PROPERTY = "aws.enableDefaultReadTimeout2026"; - - @BeforeEach - @AfterEach - void clearGateProperty() { - System.clearProperty(GATE_PROPERTY); - } - @ParameterizedTest @MethodSource("cipherPreferences") void resolveCipherPreference_shouldResolveCorrectly(Boolean postQuantumTlsEnabled, @@ -161,60 +151,29 @@ void resolveMonitoringOptions_explicitConnectionHealthConfiguration_winsOverFall } @Test - void resolveMonitoringOptions_serviceFallbackZero_appliesNothing() { - assertThat(AwsCrtConfigurationUtils.resolveMonitoringOptions(null, Duration.ZERO)).isNull(); + void resolveMonitoringOptions_explicitConnectionHealthConfigurationWithoutFallback_usesExplicitConfig() { + HttpMonitoringOptions options = AwsCrtConfigurationUtils.resolveMonitoringOptions(healthConfiguration(), null); + assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(500L); + assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(10); } @Test - void resolveMonitoringOptions_serviceFallbackZeroWithGateOn_appliesNothing() { - System.setProperty(GATE_PROPERTY, "true"); + void resolveMonitoringOptions_fallbackZero_appliesNothing() { assertThat(AwsCrtConfigurationUtils.resolveMonitoringOptions(null, Duration.ZERO)).isNull(); } @Test - void resolveMonitoringOptions_serviceFallbackPositive_mapped() { - HttpMonitoringOptions options = AwsCrtConfigurationUtils.resolveMonitoringOptions(null, Duration.ofMinutes(15)); - assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(1L); - assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(900); - } - - @Test - void resolveMonitoringOptions_serviceFallbackPositive_ignoresGate() { - System.setProperty(GATE_PROPERTY, "false"); + void resolveMonitoringOptions_fallbackPositive_mapped() { HttpMonitoringOptions options = AwsCrtConfigurationUtils.resolveMonitoringOptions(null, Duration.ofMinutes(15)); assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(1L); assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(900); } @Test - void resolveMonitoringOptions_standaloneGateOn_appliesDefault() { - System.setProperty(GATE_PROPERTY, "true"); - HttpMonitoringOptions options = AwsCrtConfigurationUtils.resolveMonitoringOptions(null, null); - assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(1L); - assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(300); - } - - @Test - void resolveMonitoringOptions_standaloneGateOff_appliesNothing() { + void resolveMonitoringOptions_fallbackAbsent_appliesNothing() { assertThat(AwsCrtConfigurationUtils.resolveMonitoringOptions(null, null)).isNull(); } - @Test - void resolveMonitoringOptions_standaloneGateOnWithExplicitConfig_usesExplicitConfig() { - System.setProperty(GATE_PROPERTY, "true"); - HttpMonitoringOptions options = AwsCrtConfigurationUtils.resolveMonitoringOptions(healthConfiguration(), null); - assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(500L); - assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(10); - } - - @Test - void standaloneGateSetting_matchesRolloutGateNames() { - assertThat(AwsCrtDefaultReadTimeoutSetting.ENABLE_DEFAULT_READ_TIMEOUT_2026.environmentVariable()) - .isEqualTo("AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026"); - assertThat(AwsCrtDefaultReadTimeoutSetting.ENABLE_DEFAULT_READ_TIMEOUT_2026.property()) - .isEqualTo("aws.enableDefaultReadTimeout2026"); - } - private static ConnectionHealthConfiguration healthConfiguration() { return ConnectionHealthConfiguration.builder() .minimumThroughputInBps(500L) From 37aba736f95df6f5b3c0f231b0ef58a570d3c82c Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:01:08 -0700 Subject: [PATCH 5/6] refactor: rename READ timeout gate to SOCKET (#7337) Rename the interim rollout-gate symbols (system setting, env var, property, resolver, client option, codegen customization) from the READ name to the cross-SDK-settled SOCKET name. Behavior-preserving. SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT is intentionally not renamed; it names the read+write mechanism value, not the gate. --- .../customization/CustomizationConfig.java | 10 ++-- .../poet/builder/BaseClientBuilderClass.java | 10 ++-- ...lient-builder-internal-defaults-class.java | 2 +- .../c2j/internalconfig/customization.config | 2 +- .../builder/AwsDefaultClientBuilder.java | 12 ++--- ...aultClientBuilderReadWriteTimeoutTest.java | 6 +-- .../amazon/awssdk/core/SdkSystemSetting.java | 2 +- .../core/client/config/SdkClientOption.java | 4 +- ...ableDefaultSocketTimeout2026Resolver.java} | 20 ++++---- ...DefaultSocketTimeout2026ResolverTest.java} | 50 +++++++++---------- 10 files changed, 59 insertions(+), 59 deletions(-) rename core/sdk-core/src/main/java/software/amazon/awssdk/core/http/{EnableDefaultReadTimeout2026Resolver.java => EnableDefaultSocketTimeout2026Resolver.java} (61%) rename core/sdk-core/src/test/java/software/amazon/awssdk/core/http/{EnableDefaultReadTimeout2026ResolverTest.java => EnableDefaultSocketTimeout2026ResolverTest.java} (60%) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java index 875e272066ce..8d3837433731 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java @@ -233,7 +233,7 @@ public class CustomizationConfig { /** * Whether the client will apply a default read/write timeout by default. */ - private Boolean defaultEnableReadTimeout2026; + private Boolean defaultEnableSocketTimeout2026; /** * Whether to generate an abstract decorator class that delegates to the async service client @@ -742,12 +742,12 @@ public void setDefaultNewRetries2026(Boolean defaultNewRetries2026) { this.defaultNewRetries2026 = defaultNewRetries2026; } - public Boolean getDefaultEnableReadTimeout2026() { - return defaultEnableReadTimeout2026; + public Boolean getDefaultEnableSocketTimeout2026() { + return defaultEnableSocketTimeout2026; } - public void setDefaultEnableReadTimeout2026(Boolean defaultEnableReadTimeout2026) { - this.defaultEnableReadTimeout2026 = defaultEnableReadTimeout2026; + public void setDefaultEnableSocketTimeout2026(Boolean defaultEnableSocketTimeout2026) { + this.defaultEnableSocketTimeout2026 = defaultEnableSocketTimeout2026; } public ServiceConfig getServiceConfig() { diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java index 77f4eb8485ee..47dca6ae9c20 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java @@ -332,7 +332,7 @@ private Optional mergeInternalDefaultsMethod() { String userAgent = model.getCustomizationConfig().getUserAgent(); RetryMode defaultRetryMode = model.getCustomizationConfig().getDefaultRetryMode(); Boolean defaultNewRetries2026 = model.getCustomizationConfig().getDefaultNewRetries2026(); - Boolean defaultEnableReadTimeout2026 = model.getCustomizationConfig().getDefaultEnableReadTimeout2026(); + Boolean defaultEnableSocketTimeout2026 = model.getCustomizationConfig().getDefaultEnableSocketTimeout2026(); // If none of the options are customized, then we do not need to bother overriding the method if (!hasInternalDefaults()) { @@ -357,9 +357,9 @@ private Optional mergeInternalDefaultsMethod() { builder.addCode("c.option($T.DEFAULT_NEW_RETRIES_2026, $L);\n", SdkClientOption.class, defaultNewRetries2026); } - if (defaultEnableReadTimeout2026 != null) { - builder.addCode("c.option($T.DEFAULT_ENABLE_READ_TIMEOUT_2026, $L);\n", - SdkClientOption.class, defaultEnableReadTimeout2026); + if (defaultEnableSocketTimeout2026 != null) { + builder.addCode("c.option($T.DEFAULT_ENABLE_SOCKET_TIMEOUT_2026, $L);\n", + SdkClientOption.class, defaultEnableSocketTimeout2026); } builder.addCode("});\n"); return Optional.of(builder.build()); @@ -370,7 +370,7 @@ private boolean hasInternalDefaults() { return customizationConfig.getUserAgent() != null || customizationConfig.getDefaultRetryMode() != null || customizationConfig.getDefaultNewRetries2026() != null - || customizationConfig.getDefaultEnableReadTimeout2026() != null; + || customizationConfig.getDefaultEnableSocketTimeout2026() != null; } private MethodSpec finalizeServiceConfigurationMethod() { diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java index c8d612714b5e..d4a7d48d2b92 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java @@ -77,7 +77,7 @@ protected final SdkClientConfiguration mergeInternalDefaults(SdkClientConfigurat c.option(SdkClientOption.INTERNAL_USER_AGENT, "md/foobar"); c.option(SdkClientOption.DEFAULT_RETRY_MODE, RetryMode.STANDARD); c.option(SdkClientOption.DEFAULT_NEW_RETRIES_2026, true); - c.option(SdkClientOption.DEFAULT_ENABLE_READ_TIMEOUT_2026, true); + c.option(SdkClientOption.DEFAULT_ENABLE_SOCKET_TIMEOUT_2026, true); }); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config index c816c420aa1d..f37cc8509cb1 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config @@ -5,5 +5,5 @@ "userAgent": "md/foobar", "defaultRetryMode": "STANDARD", "defaultNewRetries2026": "true", - "defaultEnableReadTimeout2026": "true" + "defaultEnableSocketTimeout2026": "true" } diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java index 2f4a77e7e9a0..325585dc6d7b 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java @@ -51,7 +51,7 @@ import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; -import software.amazon.awssdk.core.http.EnableDefaultReadTimeout2026Resolver; +import software.amazon.awssdk.core.http.EnableDefaultSocketTimeout2026Resolver; import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.core.internal.SdkInternalTestAdvancedClientOption; import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy; @@ -263,10 +263,10 @@ private AttributeMap resolveHttpClientConfig(LazyValueSource config) { } /** - * Applies the {@code AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} rollout gate to the codegen-baked + * Applies the {@code AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026} rollout gate to the codegen-baked * {@link SdkHttpConfigurationOption#SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT} contributed by {@link #serviceHttpConfig()}. - * The gate resolves from the {@code AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} environment variable/system property, else the - * codegen-baked {@link SdkClientOption#DEFAULT_ENABLE_READ_TIMEOUT_2026} default, else off. + * The gate resolves from the {@code AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026} environment variable/system property, else the + * codegen-baked {@link SdkClientOption#DEFAULT_ENABLE_SOCKET_TIMEOUT_2026} default, else off. * *

When the gate is on, an unlisted service (nothing baked) gets the flat 5-minute default and a baked tier is kept as-is * ({@link Duration#ZERO} for fully-exempt, 15 minutes for partial). When the gate is off, a baked positive tier (partial) @@ -275,8 +275,8 @@ private AttributeMap resolveHttpClientConfig(LazyValueSource config) { * not truthy, so the HTTP client's option-absent path applies nothing either way. */ private AttributeMap applyDefaultReadWriteTimeout(LazyValueSource config, AttributeMap serviceHttpConfig) { - boolean gateEnabled = new EnableDefaultReadTimeout2026Resolver() - .defaultEnableReadTimeout2026(config.get(SdkClientOption.DEFAULT_ENABLE_READ_TIMEOUT_2026)) + boolean gateEnabled = new EnableDefaultSocketTimeout2026Resolver() + .defaultEnableSocketTimeout2026(config.get(SdkClientOption.DEFAULT_ENABLE_SOCKET_TIMEOUT_2026)) .resolve(); Duration bakedTier = serviceHttpConfig.get(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT); diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilderReadWriteTimeoutTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilderReadWriteTimeoutTest.java index 9bdec6b844a0..7184bc194fa9 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilderReadWriteTimeoutTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilderReadWriteTimeoutTest.java @@ -47,14 +47,14 @@ import software.amazon.awssdk.utils.AttributeMap; /** - * Verifies the {@code AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} rollout gate applied in + * Verifies the {@code AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026} rollout gate applied in * {@link AwsDefaultClientBuilder#resolveHttpClientConfig} to the codegen-baked * {@link SdkHttpConfigurationOption#SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT}. */ @ExtendWith(MockitoExtension.class) class AwsDefaultClientBuilderReadWriteTimeoutTest { - private static final String GATE_PROPERTY = SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property(); + private static final String GATE_PROPERTY = SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property(); private static final Duration PARTIAL_TIER = Duration.ofMinutes(15); private static final Duration FLAT_DEFAULT = Duration.ofMinutes(5); @@ -168,7 +168,7 @@ protected SdkClientConfiguration mergeInternalDefaults(SdkClientConfiguration co if (!codegenGateDefault) { return config; } - return config.merge(c -> c.option(SdkClientOption.DEFAULT_ENABLE_READ_TIMEOUT_2026, true)); + return config.merge(c -> c.option(SdkClientOption.DEFAULT_ENABLE_SOCKET_TIMEOUT_2026, true)); } @Override diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java index 7f51b3417526..0da073035e64 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java @@ -286,7 +286,7 @@ public enum SdkSystemSetting implements SystemSetting { *

This setting is not intended to be used by end users. It gates an interim rollout and is subject to removal in a * future release. */ - AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026("aws.enableDefaultReadTimeout2026", null); + AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026("aws.enableDefaultSocketTimeout2026", null); private final String systemProperty; private final String defaultValue; diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java index 1ab50ae46caa..199232a088b3 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java @@ -312,11 +312,11 @@ public final class SdkClientOption extends ClientOption { public static final SdkClientOption DEFAULT_NEW_RETRIES_2026 = new SdkClientOption<>(Boolean.class); /** - * Option to specify the default for the {@code AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} feature gate for the SDK client. + * Option to specify the default for the {@code AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026} feature gate for the SDK client. * This option is not intended to be set by end users. It gates an interim rollout and is subject to removal in a future * release. */ - public static final SdkClientOption DEFAULT_ENABLE_READ_TIMEOUT_2026 = new SdkClientOption<>(Boolean.class); + public static final SdkClientOption DEFAULT_ENABLE_SOCKET_TIMEOUT_2026 = new SdkClientOption<>(Boolean.class); /** * Whether retries 2.1 behavior is enabled. diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026Resolver.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/EnableDefaultSocketTimeout2026Resolver.java similarity index 61% rename from core/sdk-core/src/main/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026Resolver.java rename to core/sdk-core/src/main/java/software/amazon/awssdk/core/http/EnableDefaultSocketTimeout2026Resolver.java index 200124395958..4a37c9a80ea7 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026Resolver.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/EnableDefaultSocketTimeout2026Resolver.java @@ -20,21 +20,21 @@ import software.amazon.awssdk.core.SdkSystemSetting; /** - * Resolver for the {@link SdkSystemSetting#AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} that supports setting a fallback value if not + * Resolver for the {@link SdkSystemSetting#AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026} that supports setting a fallback value if not * defined in the environment or system properties. */ @SdkProtectedApi -public final class EnableDefaultReadTimeout2026Resolver { - private Boolean defaultEnableReadTimeout2026; +public final class EnableDefaultSocketTimeout2026Resolver { + private Boolean defaultEnableSocketTimeout2026; /** - * The default value for {@code AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026} if not configured via - * {@link SdkSystemSetting#AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026}. + * The default value for {@code AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026} if not configured via + * {@link SdkSystemSetting#AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026}. * * @return This resolver for method chaining. */ - public EnableDefaultReadTimeout2026Resolver defaultEnableReadTimeout2026(Boolean defaultEnableReadTimeout2026) { - this.defaultEnableReadTimeout2026 = defaultEnableReadTimeout2026; + public EnableDefaultSocketTimeout2026Resolver defaultEnableSocketTimeout2026(Boolean defaultEnableSocketTimeout2026) { + this.defaultEnableSocketTimeout2026 = defaultEnableSocketTimeout2026; return this; } @@ -42,14 +42,14 @@ public EnableDefaultReadTimeout2026Resolver defaultEnableReadTimeout2026(Boolean * Resolve whether a default read/write timeout is applied. */ public boolean resolve() { - Optional envConfig = SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.getBooleanValue(); + Optional envConfig = SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.getBooleanValue(); if (envConfig.isPresent()) { return envConfig.get(); } - if (defaultEnableReadTimeout2026 != null) { - return defaultEnableReadTimeout2026; + if (defaultEnableSocketTimeout2026 != null) { + return defaultEnableSocketTimeout2026; } return false; diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026ResolverTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/EnableDefaultSocketTimeout2026ResolverTest.java similarity index 60% rename from core/sdk-core/src/test/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026ResolverTest.java rename to core/sdk-core/src/test/java/software/amazon/awssdk/core/http/EnableDefaultSocketTimeout2026ResolverTest.java index 6ef795f658ad..cc1d2757d754 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/EnableDefaultReadTimeout2026ResolverTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/EnableDefaultSocketTimeout2026ResolverTest.java @@ -27,35 +27,35 @@ import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.testutils.EnvironmentVariableHelper; -public class EnableDefaultReadTimeout2026ResolverTest { - private static String enableDefaultReadTimeout2026Save; +public class EnableDefaultSocketTimeout2026ResolverTest { + private static String enableDefaultSocketTimeout2026Save; @BeforeAll static void setup() { - enableDefaultReadTimeout2026Save = System.getProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property()); + enableDefaultSocketTimeout2026Save = System.getProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property()); } @AfterAll static void teardown() { - if (enableDefaultReadTimeout2026Save != null) { - System.setProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property(), - enableDefaultReadTimeout2026Save); + if (enableDefaultSocketTimeout2026Save != null) { + System.setProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property(), + enableDefaultSocketTimeout2026Save); } else { - System.clearProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property()); + System.clearProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property()); } } @BeforeEach void methodSetup() { - System.clearProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property()); + System.clearProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property()); } @Test void systemSetting_usesExpectedEnvironmentVariableAndSystemPropertyNames() { - assertThat(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.environmentVariable()) - .isEqualTo("AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026"); - assertThat(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property()) - .isEqualTo("aws.enableDefaultReadTimeout2026"); + assertThat(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.environmentVariable()) + .isEqualTo("AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026"); + assertThat(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property()) + .isEqualTo("aws.enableDefaultSocketTimeout2026"); } @ParameterizedTest @@ -63,15 +63,15 @@ void systemSetting_usesExpectedEnvironmentVariableAndSystemPropertyNames() { void resolve_behavesCorrectly(TestParams params) { EnvironmentVariableHelper.run((env) -> { if (params.systemProperty != null) { - System.setProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.property(), params.systemProperty); + System.setProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property(), params.systemProperty); } if (params.envVar != null) { - env.set(SdkSystemSetting.AWS_ENABLE_DEFAULT_READ_TIMEOUT_2026.environmentVariable(), params.envVar); + env.set(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.environmentVariable(), params.envVar); } - EnableDefaultReadTimeout2026Resolver resolver = - new EnableDefaultReadTimeout2026Resolver().defaultEnableReadTimeout2026(params.defaultEnableReadTimeout2026); + EnableDefaultSocketTimeout2026Resolver resolver = + new EnableDefaultSocketTimeout2026Resolver().defaultEnableSocketTimeout2026(params.defaultEnableSocketTimeout2026); assertThat(resolver.resolve()).isEqualTo(params.expected); }); @@ -83,19 +83,19 @@ private static Stream params() { new TestParams().expected(false), // precedence testing - new TestParams().systemProperty("true").defaultEnableReadTimeout2026(true).expected(true), - new TestParams().systemProperty("false").defaultEnableReadTimeout2026(true).expected(false), - new TestParams().envVar("true").defaultEnableReadTimeout2026(true).expected(true), - new TestParams().envVar("false").defaultEnableReadTimeout2026(true).expected(false), - new TestParams().defaultEnableReadTimeout2026(true).expected(true), - new TestParams().defaultEnableReadTimeout2026(false).expected(false) + new TestParams().systemProperty("true").defaultEnableSocketTimeout2026(true).expected(true), + new TestParams().systemProperty("false").defaultEnableSocketTimeout2026(true).expected(false), + new TestParams().envVar("true").defaultEnableSocketTimeout2026(true).expected(true), + new TestParams().envVar("false").defaultEnableSocketTimeout2026(true).expected(false), + new TestParams().defaultEnableSocketTimeout2026(true).expected(true), + new TestParams().defaultEnableSocketTimeout2026(false).expected(false) ); } private static class TestParams { private String systemProperty; private String envVar; - private Boolean defaultEnableReadTimeout2026; + private Boolean defaultEnableSocketTimeout2026; private boolean expected; public TestParams systemProperty(String systemProperty) { @@ -108,8 +108,8 @@ public TestParams envVar(String envVar) { return this; } - public TestParams defaultEnableReadTimeout2026(Boolean defaultEnableReadTimeout2026) { - this.defaultEnableReadTimeout2026 = defaultEnableReadTimeout2026; + public TestParams defaultEnableSocketTimeout2026(Boolean defaultEnableSocketTimeout2026) { + this.defaultEnableSocketTimeout2026 = defaultEnableSocketTimeout2026; return this; } From 20616113dbb62a94f1d5d251e8ba66ed9cd369bb Mon Sep 17 00:00:00 2001 From: Zoe Wang <33073555+zoewangg@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:46:29 -0700 Subject: [PATCH 6/6] docs: clarify httpClientBuilder builds a client per service client A supplied httpClientBuilder is used to build a new HTTP client instance for each service client, each with that service's HTTP defaults, so sharing one builder across clients yields separate instances with their own connection pools. Document this and point to httpClient(...) for sharing a single instance. --- .../awssdk/core/client/builder/SdkAsyncClientBuilder.java | 7 +++++++ .../awssdk/core/client/builder/SdkSyncClientBuilder.java | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkAsyncClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkAsyncClientBuilder.java index cb02b440d49a..55fdb4c9d989 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkAsyncClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkAsyncClientBuilder.java @@ -68,6 +68,13 @@ default B asyncConfiguration(Consumer clientAs * customize the HTTP client as it benefits from service specific defaults. * *

+ * A new HTTP client instance is built from this builder for each service client it is supplied to, configured with that + * service's HTTP defaults. Supplying the same builder to multiple service clients therefore produces a separate, + * independently configured HTTP client instance (with its own connection pool) for each of them. To share a single HTTP + * client instance across service clients, pass a pre-built client to {@link #httpClient(SdkAsyncHttpClient)} instead. + *

+ * + *

* Clients created by the builder are managed by the SDK and will be closed when the service client is closed. *

* diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkSyncClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkSyncClientBuilder.java index 5bf6f8a7b483..bec5fc0e5c9a 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkSyncClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkSyncClientBuilder.java @@ -50,6 +50,13 @@ public interface SdkSyncClientBuilder, C> { * customize the HTTP client as it benefits from service-specific default configuration. * *

+ * A new HTTP client instance is built from this builder for each service client it is supplied to, configured with that + * service's HTTP defaults. Supplying the same builder to multiple service clients therefore produces a separate, + * independently configured HTTP client instance (with its own connection pool) for each of them. To share a single HTTP + * client instance across service clients, pass a pre-built client to {@link #httpClient(SdkHttpClient)} instead. + *

+ * + *

* Clients created by the builder are managed by the SDK and will be closed when the service client is closed. *

*/