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/5] 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/5] 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/5] 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/5] [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/5] 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; }