Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public static CodegenCustomizationProcessor getProcessorFor(
new S3ControlRemoveAccountIdHostPrefixProcessor(),
new ExplicitStringPayloadQueryProtocolProcessor(),
new LowercaseShapeValidatorProcessor(),
new LongPollingOperationProcessor()
new LongPollingOperationProcessor(),
new DefaultReadWriteTimeoutExemptionProcessor()
);
}
}
Original file line number Diff line number Diff line change
@@ -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()}).
*
* <p>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<String, Long> SERVICE_ID_TO_TIMEOUT_MILLIS = loadExemptions();

private final Map<String, Long> serviceIdToTimeoutMillis;

public DefaultReadWriteTimeoutExemptionProcessor() {
this(SERVICE_ID_TO_TIMEOUT_MILLIS);
}

@SdkTestInternalApi
DefaultReadWriteTimeoutExemptionProcessor(Map<String, Long> 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.
*
* <p>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<String> knownServiceIds) {
List<String> 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<String, Long> loadExemptions() {
Map<String, Long> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ public class Metadata {

private String serviceId;

private Long defaultReadWriteTimeoutMillis;

private List<AuthType> auth;

public List<AuthType> getAuth() {
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -48,6 +49,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;
Expand Down Expand Up @@ -330,9 +332,10 @@ private Optional<MethodSpec> mergeInternalDefaultsMethod() {
String userAgent = model.getCustomizationConfig().getUserAgent();
RetryMode defaultRetryMode = model.getCustomizationConfig().getDefaultRetryMode();
Boolean defaultNewRetries2026 = model.getCustomizationConfig().getDefaultNewRetries2026();
Boolean defaultEnableSocketTimeout2026 = model.getCustomizationConfig().getDefaultEnableSocketTimeout2026();

// 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();
}

Expand All @@ -354,10 +357,22 @@ private Optional<MethodSpec> mergeInternalDefaultsMethod() {
builder.addCode("c.option($T.DEFAULT_NEW_RETRIES_2026, $L);\n",
SdkClientOption.class, defaultNewRetries2026);
}
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());
}

private boolean hasInternalDefaults() {
CustomizationConfig customizationConfig = model.getCustomizationConfig();
return customizationConfig.getUserAgent() != null
|| customizationConfig.getDefaultRetryMode() != null
|| customizationConfig.getDefaultNewRetries2026() != null
|| customizationConfig.getDefaultEnableSocketTimeout2026() != null;
}

private MethodSpec finalizeServiceConfigurationMethod() {
String requestHandlerDirectory = Utils.packageToDirectory(model.getMetadata().getFullClientPackageName());
String requestHandlerPath = String.format("%s/execution.interceptors", requestHandlerDirectory);
Expand Down Expand Up @@ -828,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) {
Expand All @@ -856,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 (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 (!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())");
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading