From e93aa3132c2ab6a15ebf8b04acf1467607512750 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Wed, 22 Jul 2026 21:38:16 +0000 Subject: [PATCH 1/3] feat(codegen): fill URI/endpoint-bound members for the CRaC warm-up call and warm sync/async clients independently --- .../poet/crac/WarmUpOperationSelector.java | 47 +++++- .../codegen/poet/crac/WarmUpProviderSpec.java | 67 +++++--- .../crac/WarmUpOperationSelectorTest.java | 146 +++++++++++++++++- .../poet/crac/WarmUpProviderSpecTest.java | 18 +++ .../poet/crac/warmup-provider-async-only.java | 2 + .../crac/warmup-provider-bearer-auth.java | 63 ++++++++ .../warmup-provider-endpoint-discovery.java | 62 ++++++++ .../poet/crac/warmup-provider-query.java | 3 + .../poet/crac/warmup-provider-rest-json.java | 3 + .../poet/crac/warmup-provider-s3control.java | 60 +++++++ .../poet/crac/warmup-provider-xml.java | 3 + .../awssdk/core/crac/SdkWarmUpProvider.java | 8 +- .../internal/crac/TargetedWarmUpInvoker.java | 6 +- .../core/internal/crac/WarmUpDiscovery.java | 28 +++- .../core/crac/SdkWarmUpProviderTest.java | 111 +++++++++++++ 15 files changed, 591 insertions(+), 36 deletions(-) create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-bearer-auth.java create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-endpoint-discovery.java create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-s3control.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpProviderTest.java diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java index a84a872a7a4c..afb4e5b442fe 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java @@ -20,6 +20,7 @@ import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; import software.amazon.awssdk.codegen.model.intermediate.MemberModel; import software.amazon.awssdk.codegen.model.intermediate.OperationModel; @@ -29,7 +30,8 @@ /** * Selects the operation used for the CRaC warm-up call: filters out streaming/event-stream and deprecated - * operations, then ranks the rest (see {@link #warmUpPreference}). + * operations, plus operations that cannot be dummy-filled (see {@link #membersRequiringDummyValue}), then ranks the + * rest (see {@link #warmUpPreference}). */ public final class WarmUpOperationSelector { @@ -69,6 +71,17 @@ public static Optional selectWarmUpOperation(IntermediateModel m .min(preference); } + /** + * Required members the warm-up call must populate: those bound to the URI path or an endpoint context param, + * which reject null. Other members stay null. + */ + static List membersRequiringDummyValue(OperationModel operation) { + return inputMembers(operation).stream() + .filter(MemberModel::isRequired) + .filter(WarmUpOperationSelector::isUriOrEndpointBound) + .collect(Collectors.toList()); + } + /** * Preference order: returns output (so the unmarshaller is primed too), is authenticated (so signing is primed * too; {@code noAuth} operations skip signing entirely), verified simple method, accepts an empty request, @@ -94,7 +107,37 @@ private static Comparator byVerifiedSimpleFirst(List ver private static boolean passesHardGates(OperationModel operation) { return !isStreamingOrEventStream(operation) - && !operation.isDeprecated(); + && !operation.isDeprecated() + && allDummyMembersAreFillable(operation); + } + + /** + * A dummy member is fillable if it is a string. The warm-up call emits {@code "warmup"} for a plain member and an + * ARN-shaped value for an ARN member; see {@link #isArnMember}. + */ + private static boolean allDummyMembersAreFillable(OperationModel operation) { + return membersRequiringDummyValue(operation).stream() + .allMatch(WarmUpOperationSelector::isDummyFillable); + } + + private static boolean isDummyFillable(MemberModel member) { + return "String".equals(member.getVariable().getSimpleType()); + } + + /** + * An ARN endpoint context param needs an ARN-shaped dummy, since the endpoint rules parse it as an ARN. Identified + * by the conventional capitalized {@code Arn}/{@code ARN} name suffix (e.g. {@code resourceArn}), so a name that + * merely contains those letters (e.g. {@code learn}) does not match. + */ + static boolean isArnMember(MemberModel member) { + String name = member.getName(); + return member.getContextParam() != null + && (name.endsWith("Arn") || name.endsWith("ARN")); + } + + private static boolean isUriOrEndpointBound(MemberModel member) { + return (member.getHttp() != null && member.getHttp().isUri()) + || member.getContextParam() != null; } private static boolean isStreamingOrEventStream(OperationModel operation) { diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpProviderSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpProviderSpec.java index b121f391333b..b3ed6de59a16 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpProviderSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpProviderSpec.java @@ -26,11 +26,13 @@ import javax.lang.model.element.Modifier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.intermediate.MemberModel; import software.amazon.awssdk.codegen.model.intermediate.OperationModel; import software.amazon.awssdk.codegen.model.intermediate.Protocol; import software.amazon.awssdk.codegen.poet.ClassSpec; import software.amazon.awssdk.codegen.poet.PoetExtension; import software.amazon.awssdk.codegen.poet.PoetUtils; +import software.amazon.awssdk.codegen.utils.AuthUtils; import software.amazon.awssdk.core.ClientType; import software.amazon.awssdk.core.crac.SdkWarmUpProvider; @@ -54,8 +56,13 @@ public class WarmUpProviderSpec implements ClassSpec { private static final int SUCCESS_STATUS_CODE = 200; private static final String DUMMY_ACCESS_KEY_ID = "akid"; private static final String DUMMY_SECRET_ACCESS_KEY = "skid"; + private static final String DUMMY_TOKEN = "warmup-dummy-token"; private static final String LOCAL_ENDPOINT = "http://localhost"; + private static final String DUMMY_MEMBER_VALUE = "warmup"; + + private static final String DUMMY_ARN_VALUE = "arn:aws:service:us-east-1:123456789012:warmup"; + private static final ClassName CANNED_RESPONSE_HTTP_CLIENT = ClassName.get("software.amazon.awssdk.core.crac.http", "CannedResponseHttpClient"); private static final ClassName CANNED_RESPONSE_ASYNC_HTTP_CLIENT = @@ -68,6 +75,8 @@ public class WarmUpProviderSpec implements ClassSpec { ClassName.get("software.amazon.awssdk.auth.credentials", "StaticCredentialsProvider"); private static final ClassName AWS_BASIC_CREDENTIALS = ClassName.get("software.amazon.awssdk.auth.credentials", "AwsBasicCredentials"); + private static final ClassName STATIC_TOKEN_PROVIDER = + ClassName.get("software.amazon.awssdk.auth.token.credentials", "StaticTokenProvider"); private static final ClassName REGION = ClassName.get("software.amazon.awssdk.regions", "Region"); @@ -162,21 +171,7 @@ private CodeBlock clientBlock(ClassName clientType, ClassName cannedHttpClientTy .addStatement("$T $N = $T.builder().responseBody($L).statusCode($L).build()", httpClientType, httpClientVar, cannedHttpClientType, CANNED_RESPONSE_FIELD, SUCCESS_STATUS_CODE) - .beginControlFlow("try ($1T $2N = $1T.builder()\n" - + ".httpClient($3N)\n" - + ".credentialsProvider($4T.create($5T.create($6S, $7S)))\n" - + ".region($8T.US_EAST_1)\n" - + ".endpointOverride($9T.create($10S))\n" - + ".build())", - clientType, - clientVar, - httpClientVar, - STATIC_CREDENTIALS_PROVIDER, - AWS_BASIC_CREDENTIALS, - DUMMY_ACCESS_KEY_ID, DUMMY_SECRET_ACCESS_KEY, - REGION, - URI.class, - LOCAL_ENDPOINT); + .beginControlFlow("try ($L)", clientBuilder(clientType, clientVar, httpClientVar)); warmUpOperation.ifPresent(op -> block.add(warmUpOperationCall(op, clientVar, async))); @@ -185,8 +180,38 @@ private CodeBlock clientBlock(ClassName clientType, ClassName cannedHttpClientTy } /** - * Verified simple methods generate a no-arg overload on both clients, so the call uses it. Other operations pass - * an empty request. + * Builds the warm-up client: canned HTTP client, dummy credentials, local endpoint, plus any service-specific + * options from {@link #addServiceSpecificOptions}. + */ + private CodeBlock clientBuilder(ClassName clientType, String clientVar, String httpClientVar) { + CodeBlock.Builder builder = CodeBlock.builder() + .add("$1T $2N = $1T.builder()\n", clientType, clientVar) + .add(".httpClient($N)\n", httpClientVar) + .add(".credentialsProvider($T.create($T.create($S, $S)))\n", + STATIC_CREDENTIALS_PROVIDER, AWS_BASIC_CREDENTIALS, DUMMY_ACCESS_KEY_ID, DUMMY_SECRET_ACCESS_KEY); + addServiceSpecificOptions(builder); + return builder.add(".region($T.US_EAST_1)\n", REGION) + .add(".endpointOverride($T.create($S))\n", URI.class, LOCAL_ENDPOINT) + .add(".build()") + .build(); + } + + /** + * Options only some services need: a dummy token for bearer-auth services, and disabling endpoint discovery for + * services that have it (avoids a WARN about the endpoint override disabling it). + */ + private void addServiceSpecificOptions(CodeBlock.Builder builder) { + if (AuthUtils.usesBearerAuth(model)) { + builder.add(".tokenProvider($T.create(() -> $S))\n", STATIC_TOKEN_PROVIDER, DUMMY_TOKEN); + } + if (model.getEndpointOperation().isPresent()) { + builder.add(".endpointDiscoveryEnabled(false)\n"); + } + } + + /** + * Verified simple methods use the generated no-arg overload. Other operations pass an empty request, except for + * members from {@link WarmUpOperationSelector#membersRequiringDummyValue} which get a dummy value. */ private CodeBlock warmUpOperationCall(OperationModel operation, String clientVar, boolean async) { String join = async ? ".join()" : ""; @@ -196,8 +221,14 @@ private CodeBlock warmUpOperationCall(OperationModel operation, String clientVar .build(); } ClassName requestType = poetExtensions.getModelClass(operation.getInputShape().getShapeName()); + CodeBlock.Builder request = CodeBlock.builder().add("$T.builder()", requestType); + for (MemberModel member : WarmUpOperationSelector.membersRequiringDummyValue(operation)) { + String value = WarmUpOperationSelector.isArnMember(member) ? DUMMY_ARN_VALUE : DUMMY_MEMBER_VALUE; + request.add(".$N($S)", member.getFluentSetterMethodName(), value); + } + request.add(".build()"); return CodeBlock.builder() - .addStatement("$N.$N($T.builder().build())" + join, clientVar, operation.getMethodName(), requestType) + .addStatement("$N.$N($L)" + join, clientVar, operation.getMethodName(), request.build()) .build(); } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.java index 73c9eff943fb..fdf5b1ceb479 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.java @@ -25,14 +25,20 @@ import java.util.Map; import java.util.Optional; 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 software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; import software.amazon.awssdk.codegen.model.intermediate.MemberModel; import software.amazon.awssdk.codegen.model.intermediate.Metadata; import software.amazon.awssdk.codegen.model.intermediate.OperationModel; +import software.amazon.awssdk.codegen.model.intermediate.ParameterHttpMapping; import software.amazon.awssdk.codegen.model.intermediate.ShapeModel; +import software.amazon.awssdk.codegen.model.intermediate.VariableModel; +import software.amazon.awssdk.codegen.model.service.ContextParam; +import software.amazon.awssdk.codegen.model.service.Location; public class WarmUpOperationSelectorTest { @@ -167,10 +173,105 @@ private static Stream selectionScenarios() { .operation(op("DescribeLimits").withOutput()) .operation(op("GetItem").withOutput().withRequiredMembers(2)) .operation(op("PutItem").withRequiredMembers(2)) - .expect("ListTables") + .expect("ListTables"), + + // Hard gate: a required URI/endpoint-bound member is dummy-fillable only when it is a string. + scenario("requiredStringUriMember_isStillEligible") + .operation(op("GetThing").withOutput().withRequiredUriMember("ThingName", "String")) + .expect("GetThing"), + scenario("requiredNonStringUriMemberOnly_isNotSelected") + .operation(op("GetThing").withOutput().withRequiredUriMember("ThingVersion", "Integer")) + .expectNothing(), + scenario("requiredNonStringUriMember_fallsBackToNextBestOperation") + .operation(op("GetThing").withOutput().withRequiredUriMember("ThingVersion", "Integer")) + .operation(op("PutThing").withOutput().withRequiredMembers(2)) + .expect("PutThing"), + // An ARN context param is a string, so it is fillable with an ARN-shaped dummy value. + scenario("requiredArnContextParamMember_isStillEligible") + .operation(op("GetResource").withOutput().withRequiredContextParamMember("ResourceArn", "String")) + .expect("GetResource"), + scenario("requiredNonArnContextParamMember_isStillEligible") + .operation(op("ListGrants").withOutput().withRequiredContextParamMember("AccountId", "String")) + .expect("ListGrants") + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("dummyValueScenarios") + public void membersRequiringDummyValue_returnsOnlyUriAndEndpointBoundRequiredMembers(DummyValueScenario scenario) { + List memberNames = WarmUpOperationSelector.membersRequiringDummyValue(scenario.operation).stream() + .map(MemberModel::getName) + .collect(java.util.stream.Collectors.toList()); + assertThat(memberNames).containsExactlyElementsOf(scenario.expectedMemberNames); + } + + private static Stream dummyValueScenarios() { + return Stream.of( + new DummyValueScenario("requiredUriMember_needsDummy", + op("GetThing").withRequiredUriMember("ThingName", "String").build(), + Collections.singletonList("ThingName")), + new DummyValueScenario("requiredEndpointContextParamMember_needsDummy", + op("ListGrants").withRequiredContextParamMember("AccountId", "String").build(), + Collections.singletonList("AccountId")), + new DummyValueScenario("requiredBodyMember_staysNull", + op("ListThings").withRequiredMembers(2).build(), + Collections.emptyList()), + new DummyValueScenario("optionalUriMember_staysNull", + op("GetThing").withOptionalUriMember("ThingName", "String").build(), + Collections.emptyList()), + new DummyValueScenario("noInputShape_needsNothing", + op("ListThings").build(), + Collections.emptyList()) + ); + } + + @ParameterizedTest(name = "isArnMember({0}) == {1}") + @MethodSource("arnMemberScenarios") + public void isArnMember_matchesOnlyConventionalArnSuffix(String memberName, boolean expected) { + MemberModel member = new MemberModel(); + member.setName(memberName); + member.setContextParam(new ContextParam()); + assertThat(WarmUpOperationSelector.isArnMember(member)).isEqualTo(expected); + } + + private static Stream arnMemberScenarios() { + return Stream.of( + // Conventional ARN suffix, capitalized. Both the "ARN" and "Arn" forms occur in real models. + Arguments.of("resourceARN", true), + Arguments.of("resourceArn", true), + Arguments.of("RoleArn", true), + // Lookalikes: contain the letters "arn" but are not ARN members. + Arguments.of("learn", false), + Arguments.of("warning", false), + Arguments.of("yarnConfig", false), + Arguments.of("AccountId", false) ); } + @Test + public void isArnMember_withoutContextParam_isFalse() { + MemberModel member = new MemberModel(); + member.setName("resourceArn"); + assertThat(WarmUpOperationSelector.isArnMember(member)).isFalse(); + } + + private static final class DummyValueScenario { + private final String name; + private final OperationModel operation; + private final List expectedMemberNames; + + private DummyValueScenario(String name, OperationModel operation, List expectedMemberNames) { + this.name = name; + this.operation = operation; + this.expectedMemberNames = expectedMemberNames; + } + + @Override + public String toString() { + return name; + } + } + private static Scenario scenario(String name) { return new Scenario(name); } @@ -254,6 +355,23 @@ private OperationBuilder withRequiredMembers(int requiredCount) { return this; } + private OperationBuilder withRequiredUriMember(String memberName, String simpleType) { + addMember(uriMember(memberName, simpleType, true)); + return this; + } + + private OperationBuilder withOptionalUriMember(String memberName, String simpleType) { + addMember(uriMember(memberName, simpleType, false)); + return this; + } + + private OperationBuilder withRequiredContextParamMember(String memberName, String simpleType) { + MemberModel member = member(memberName, simpleType, true); + member.setContextParam(new ContextParam()); + addMember(member); + return this; + } + private OperationBuilder withStreamingInput() { operation.setInputShape(streamingShape()); return this; @@ -288,6 +406,32 @@ private OperationModel build() { return operation; } + private void addMember(MemberModel member) { + ShapeModel shape = operation.getInputShape(); + if (shape == null) { + shape = new ShapeModel(); + shape.setMembers(new ArrayList<>()); + operation.setInputShape(shape); + } + shape.getMembers().add(member); + } + + private static MemberModel uriMember(String memberName, String simpleType, boolean required) { + MemberModel member = member(memberName, simpleType, required); + ParameterHttpMapping http = new ParameterHttpMapping(); + http.setLocation(Location.URI); + member.setHttp(http); + return member; + } + + private static MemberModel member(String memberName, String simpleType, boolean required) { + MemberModel member = new MemberModel(); + member.setName(memberName); + member.setRequired(required); + member.setVariable(new VariableModel(memberName, simpleType)); + return member; + } + private static ShapeModel streamingShape() { ShapeModel shape = new ShapeModel(); shape.setHasStreamingMember(true); diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpProviderSpecTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpProviderSpecTest.java index 99bb92b2345d..cfb05f1d42c6 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpProviderSpecTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpProviderSpecTest.java @@ -60,4 +60,22 @@ public void warmUpProvider_cborProtocol_usesEmptyCborMapCannedResponse() { ClassSpec spec = new WarmUpProviderSpec(ClientTestModels.cborServiceModels()); assertThat(spec, generatesTo("warmup-provider-cbor.java")); } + + @Test + public void warmUpProvider_bearerAuthService_setsDummyTokenProvider() { + ClassSpec spec = new WarmUpProviderSpec(ClientTestModels.bearerAuthServiceModels()); + assertThat(spec, generatesTo("warmup-provider-bearer-auth.java")); + } + + @Test + public void warmUpProvider_endpointDiscoveryService_disablesEndpointDiscovery() { + ClassSpec spec = new WarmUpProviderSpec(ClientTestModels.endpointDiscoveryModels()); + assertThat(spec, generatesTo("warmup-provider-endpoint-discovery.java")); + } + + @Test + public void warmUpProvider_requiredEndpointBoundMember_getsDummyValue() { + ClassSpec spec = new WarmUpProviderSpec(ClientTestModels.serviceS3Control()); + assertThat(spec, generatesTo("warmup-provider-s3control.java")); + } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-async-only.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-async-only.java index 9fc448858286..77acc146a2e0 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-async-only.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-async-only.java @@ -6,6 +6,7 @@ import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.auth.token.credentials.StaticTokenProvider; import software.amazon.awssdk.core.ClientType; import software.amazon.awssdk.core.crac.SdkWarmUpProvider; import software.amazon.awssdk.core.crac.http.CannedResponseAsyncHttpClient; @@ -36,6 +37,7 @@ public void warmUpClient(ClientType clientType) { .statusCode(200).build(); try (QueryAsyncClient asyncClient = QueryAsyncClient.builder().httpClient(asyncHttpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .tokenProvider(StaticTokenProvider.create(() -> "warmup-dummy-token")) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { asyncClient.getOperationWithChecksum(GetOperationWithChecksumRequest.builder().build()).join(); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-bearer-auth.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-bearer-auth.java new file mode 100644 index 000000000000..064427dff054 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-bearer-auth.java @@ -0,0 +1,63 @@ +package software.amazon.awssdk.services.json.internal.crac; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.auth.token.credentials.StaticTokenProvider; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.core.crac.SdkWarmUpProvider; +import software.amazon.awssdk.core.crac.http.CannedResponseAsyncHttpClient; +import software.amazon.awssdk.core.crac.http.CannedResponseHttpClient; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.json.JsonAsyncClient; +import software.amazon.awssdk.services.json.JsonClient; +import software.amazon.awssdk.services.json.model.APostOperationRequest; + +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +public final class JsonWarmUpProvider implements SdkWarmUpProvider { + private static final byte[] CANNED_RESPONSE = "{}".getBytes(StandardCharsets.UTF_8); + + @Override + public String syncClientClassName() { + return "software.amazon.awssdk.services.json.JsonClient"; + } + + @Override + public String asyncClientClassName() { + return "software.amazon.awssdk.services.json.JsonAsyncClient"; + } + + @Override + public void warmUpClient(ClientType clientType) { + if (clientType == ClientType.SYNC) { + SdkHttpClient httpClient = CannedResponseHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); + try (JsonClient client = JsonClient.builder() + .httpClient(httpClient) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .tokenProvider(StaticTokenProvider.create(() -> "warmup-dummy-token")) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost")) + .build()) { + client.aPostOperation(APostOperationRequest.builder().build()); + } + } + if (clientType == ClientType.ASYNC) { + SdkAsyncHttpClient asyncHttpClient = CannedResponseAsyncHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); + try (JsonAsyncClient asyncClient = JsonAsyncClient.builder() + .httpClient(asyncHttpClient) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .tokenProvider(StaticTokenProvider.create(() -> "warmup-dummy-token")) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost")) + .build()) { + asyncClient.aPostOperation(APostOperationRequest.builder().build()).join(); + } + } + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-endpoint-discovery.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-endpoint-discovery.java new file mode 100644 index 000000000000..5048ae8a3a71 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-endpoint-discovery.java @@ -0,0 +1,62 @@ +package software.amazon.awssdk.services.endpointdiscoverytest.internal.crac; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.core.crac.SdkWarmUpProvider; +import software.amazon.awssdk.core.crac.http.CannedResponseAsyncHttpClient; +import software.amazon.awssdk.core.crac.http.CannedResponseHttpClient; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.endpointdiscoverytest.EndpointDiscoveryTestAsyncClient; +import software.amazon.awssdk.services.endpointdiscoverytest.EndpointDiscoveryTestClient; +import software.amazon.awssdk.services.endpointdiscoverytest.model.DescribeEndpointsRequest; + +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +public final class EndpointDiscoveryTestWarmUpProvider implements SdkWarmUpProvider { + private static final byte[] CANNED_RESPONSE = "{}".getBytes(StandardCharsets.UTF_8); + + @Override + public String syncClientClassName() { + return "software.amazon.awssdk.services.endpointdiscoverytest.EndpointDiscoveryTestClient"; + } + + @Override + public String asyncClientClassName() { + return "software.amazon.awssdk.services.endpointdiscoverytest.EndpointDiscoveryTestAsyncClient"; + } + + @Override + public void warmUpClient(ClientType clientType) { + if (clientType == ClientType.SYNC) { + SdkHttpClient httpClient = CannedResponseHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); + try (EndpointDiscoveryTestClient client = EndpointDiscoveryTestClient.builder() + .httpClient(httpClient) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .endpointDiscoveryEnabled(false) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost")) + .build()) { + client.describeEndpoints(DescribeEndpointsRequest.builder().build()); + } + } + if (clientType == ClientType.ASYNC) { + SdkAsyncHttpClient asyncHttpClient = CannedResponseAsyncHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); + try (EndpointDiscoveryTestAsyncClient asyncClient = EndpointDiscoveryTestAsyncClient.builder() + .httpClient(asyncHttpClient) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .endpointDiscoveryEnabled(false) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost")) + .build()) { + asyncClient.describeEndpoints(DescribeEndpointsRequest.builder().build()).join(); + } + } + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-query.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-query.java index 2b247d6448d1..aa62f1a207b1 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-query.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-query.java @@ -6,6 +6,7 @@ import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.auth.token.credentials.StaticTokenProvider; import software.amazon.awssdk.core.ClientType; import software.amazon.awssdk.core.crac.SdkWarmUpProvider; import software.amazon.awssdk.core.crac.http.CannedResponseAsyncHttpClient; @@ -38,6 +39,7 @@ public void warmUpClient(ClientType clientType) { SdkHttpClient httpClient = CannedResponseHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); try (QueryClient client = QueryClient.builder().httpClient(httpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .tokenProvider(StaticTokenProvider.create(() -> "warmup-dummy-token")) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { client.getOperationWithChecksum(GetOperationWithChecksumRequest.builder().build()); } @@ -47,6 +49,7 @@ public void warmUpClient(ClientType clientType) { .statusCode(200).build(); try (QueryAsyncClient asyncClient = QueryAsyncClient.builder().httpClient(asyncHttpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .tokenProvider(StaticTokenProvider.create(() -> "warmup-dummy-token")) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { asyncClient.getOperationWithChecksum(GetOperationWithChecksumRequest.builder().build()).join(); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-rest-json.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-rest-json.java index dd23589f3db9..109d753bb95c 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-rest-json.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-rest-json.java @@ -6,6 +6,7 @@ import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.auth.token.credentials.StaticTokenProvider; import software.amazon.awssdk.core.ClientType; import software.amazon.awssdk.core.crac.SdkWarmUpProvider; import software.amazon.awssdk.core.crac.http.CannedResponseAsyncHttpClient; @@ -37,6 +38,7 @@ public void warmUpClient(ClientType clientType) { SdkHttpClient httpClient = CannedResponseHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); try (JsonClient client = JsonClient.builder().httpClient(httpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .tokenProvider(StaticTokenProvider.create(() -> "warmup-dummy-token")) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { client.paginatedOperationWithResultKey(); } @@ -46,6 +48,7 @@ public void warmUpClient(ClientType clientType) { .statusCode(200).build(); try (JsonAsyncClient asyncClient = JsonAsyncClient.builder().httpClient(asyncHttpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .tokenProvider(StaticTokenProvider.create(() -> "warmup-dummy-token")) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { asyncClient.paginatedOperationWithResultKey().join(); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-s3control.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-s3control.java new file mode 100644 index 000000000000..94b5e182492f --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-s3control.java @@ -0,0 +1,60 @@ +package software.amazon.awssdk.services.s3control.internal.crac; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.core.crac.SdkWarmUpProvider; +import software.amazon.awssdk.core.crac.http.CannedResponseAsyncHttpClient; +import software.amazon.awssdk.core.crac.http.CannedResponseHttpClient; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3control.S3ControlAsyncClient; +import software.amazon.awssdk.services.s3control.S3ControlClient; +import software.amazon.awssdk.services.s3control.model.ListAccessGrantsRequest; + +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +public final class S3ControlWarmUpProvider implements SdkWarmUpProvider { + private static final byte[] CANNED_RESPONSE = "".getBytes(StandardCharsets.UTF_8); + + @Override + public String syncClientClassName() { + return "software.amazon.awssdk.services.s3control.S3ControlClient"; + } + + @Override + public String asyncClientClassName() { + return "software.amazon.awssdk.services.s3control.S3ControlAsyncClient"; + } + + @Override + public void warmUpClient(ClientType clientType) { + if (clientType == ClientType.SYNC) { + SdkHttpClient httpClient = CannedResponseHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); + try (S3ControlClient client = S3ControlClient.builder() + .httpClient(httpClient) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost")) + .build()) { + client.listAccessGrants(ListAccessGrantsRequest.builder().accountId("warmup").build()); + } + } + if (clientType == ClientType.ASYNC) { + SdkAsyncHttpClient asyncHttpClient = CannedResponseAsyncHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); + try (S3ControlAsyncClient asyncClient = S3ControlAsyncClient.builder() + .httpClient(asyncHttpClient) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost")) + .build()) { + asyncClient.listAccessGrants(ListAccessGrantsRequest.builder().accountId("warmup").build()).join(); + } + } + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-xml.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-xml.java index 58c8e63b692e..d6b564b2b842 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-xml.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-xml.java @@ -6,6 +6,7 @@ import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.auth.token.credentials.StaticTokenProvider; import software.amazon.awssdk.core.ClientType; import software.amazon.awssdk.core.crac.SdkWarmUpProvider; import software.amazon.awssdk.core.crac.http.CannedResponseAsyncHttpClient; @@ -38,6 +39,7 @@ public void warmUpClient(ClientType clientType) { SdkHttpClient httpClient = CannedResponseHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); try (XmlClient client = XmlClient.builder().httpClient(httpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .tokenProvider(StaticTokenProvider.create(() -> "warmup-dummy-token")) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { client.getOperationWithChecksum(GetOperationWithChecksumRequest.builder().build()); } @@ -47,6 +49,7 @@ public void warmUpClient(ClientType clientType) { .statusCode(200).build(); try (XmlAsyncClient asyncClient = XmlAsyncClient.builder().httpClient(asyncHttpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .tokenProvider(StaticTokenProvider.create(() -> "warmup-dummy-token")) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { asyncClient.getOperationWithChecksum(GetOperationWithChecksumRequest.builder().build()).join(); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUpProvider.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUpProvider.java index 6bfc82040632..b90af5e4026d 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUpProvider.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUpProvider.java @@ -18,6 +18,7 @@ import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.core.internal.crac.WarmUpDiscovery; /** * Service Provider Interface for warming up an SDK service's request path before a Coordinated Restore at Checkpoint @@ -33,11 +34,12 @@ public interface SdkWarmUpProvider { /** - * Exercises the service's request path so the Just-In-Time compiled code is captured in a CRaC snapshot. + * Exercises the service's request path so the Just-In-Time compiled code is captured in a CRaC snapshot. The sync + * and async clients are warmed independently: a failure warming one is logged at warn and does not stop the other. */ default void warmUp() { - warmUpClient(ClientType.SYNC); - warmUpClient(ClientType.ASYNC); + WarmUpDiscovery.runSafely(syncClientClassName(), () -> warmUpClient(ClientType.SYNC)); + WarmUpDiscovery.runSafely(asyncClientClassName(), () -> warmUpClient(ClientType.ASYNC)); } /** diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java index d45cef6514b3..766d56aa4ef2 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java @@ -71,11 +71,9 @@ public TargetedWarmUpResult invoke(Collection requestedClassNames) { continue; } matched.add(clientType); - try { - provider.warmUpClient(clientType); - } catch (RuntimeException | LinkageError e) { + if (!WarmUpDiscovery.runSafely(provider.getClass().getName(), + () -> provider.warmUpClient(clientType))) { warmFailed = true; - log.warn(() -> "Warm-up failed for " + provider.getClass().getName() + " and was skipped.", e); } } if (matched.isEmpty()) { diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpDiscovery.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpDiscovery.java index f97a13dcd613..f4caeefcfeb4 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpDiscovery.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpDiscovery.java @@ -22,7 +22,8 @@ import software.amazon.awssdk.utils.Logger; /** - * Shared best-effort {@link java.util.ServiceLoader} iteration for the CRaC warm-up paths. + * Shared best-effort helpers for the CRaC warm-up paths: {@link java.util.ServiceLoader} iteration and running a + * single warm-up task without letting its failure stop the others. */ @SdkInternalApi public final class WarmUpDiscovery { @@ -50,17 +51,28 @@ public static void forEachDiscovered(Iterator iterator, Consumer actio discoveredAny = true; T discovered = element; - try { - action.accept(discovered); - } catch (RuntimeException | LinkageError e) { - // LinkageError because a discovered element can fail to link (missing deps/native lib, failed static init), - // which is an Error, not an Exception. Skip it to keep warm-up best-effort; fatal Errors still propagate. - log.warn(() -> "Warm-up failed for " + discovered.getClass().getName() + " and was skipped.", e); - } + runSafely(discovered.getClass().getName(), () -> action.accept(discovered)); } if (!discoveredAny) { log.debug(() -> "No warm-up tasks were discovered on the classpath."); } } + + /** + * Runs one warm-up {@code task}. Returns {@code true} if it completed. If it fails, logs at warn with + * {@code description} and returns {@code false}, so a sibling task still runs. + * + *

Also catches {@link LinkageError}: a task can fail to link (missing dependency or native library) and that + * is an {@link Error}, not an {@link Exception}. Other errors still propagate. + */ + public static boolean runSafely(String description, Runnable task) { + try { + task.run(); + return true; + } catch (RuntimeException | LinkageError e) { + log.warn(() -> "Warm-up failed for " + description + " and was skipped.", e); + return false; + } + } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpProviderTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpProviderTest.java new file mode 100644 index 000000000000..43c9e405fa32 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpProviderTest.java @@ -0,0 +1,111 @@ +/* + * 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.crac; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.util.ArrayList; +import java.util.List; +import org.apache.logging.log4j.Level; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.testutils.LogCaptor; + +/** + * Tests the default {@link SdkWarmUpProvider#warmUp()} method, which warms the sync and async clients independently so + * a failure warming one does not stop the other. + */ +class SdkWarmUpProviderTest { + + @Test + void warmUp_warmsSyncThenAsync() { + RecordingProvider provider = new RecordingProvider(); + + provider.warmUp(); + + assertThat(provider.warmedTypes).containsExactly(ClientType.SYNC, ClientType.ASYNC); + } + + @Test + void warmUp_whenSyncFails_stillWarmsAsyncAndDoesNotThrow() { + RecordingProvider provider = new RecordingProvider() { + @Override + public void warmUpClient(ClientType clientType) { + if (clientType == ClientType.SYNC) { + throw new RuntimeException("sync boom"); + } + super.warmUpClient(clientType); + } + }; + + assertThatCode(provider::warmUp).doesNotThrowAnyException(); + assertThat(provider.warmedTypes).containsExactly(ClientType.ASYNC); + } + + @Test + void warmUp_whenSyncFailsToLink_stillWarmsAsync() { + RecordingProvider provider = new RecordingProvider() { + @Override + public void warmUpClient(ClientType clientType) { + if (clientType == ClientType.SYNC) { + throw new NoClassDefFoundError("missing signer"); + } + super.warmUpClient(clientType); + } + }; + + assertThatCode(provider::warmUp).doesNotThrowAnyException(); + assertThat(provider.warmedTypes).containsExactly(ClientType.ASYNC); + } + + @Test + void warmUp_whenClientFails_logsAtWarn() { + RecordingProvider provider = new RecordingProvider() { + @Override + public void warmUpClient(ClientType clientType) { + throw new RuntimeException("boom"); + } + }; + + try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) { + provider.warmUp(); + + assertThat(logCaptor.loggedEvents()) + .anyMatch(event -> event.getLevel() == Level.WARN + && event.getMessage().getFormattedMessage().contains("Warm-up failed for")); + } + } + + private static class RecordingProvider implements SdkWarmUpProvider { + private final List warmedTypes = new ArrayList<>(); + + @Override + public String syncClientClassName() { + return "software.amazon.awssdk.services.example.ExampleClient"; + } + + @Override + public String asyncClientClassName() { + return "software.amazon.awssdk.services.example.ExampleAsyncClient"; + } + + @Override + public void warmUpClient(ClientType clientType) { + warmedTypes.add(clientType); + } + } +} From 1d02dfea9bd1a9df20066c97f5cbd2e24376b71d Mon Sep 17 00:00:00 2001 From: John Viegas Date: Thu, 23 Jul 2026 15:04:27 +0000 Subject: [PATCH 2/3] minor update --- .../codegen/poet/crac/WarmUpOperationSelector.java | 14 +++++--------- .../poet/crac/WarmUpOperationSelectorTest.java | 3 ++- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java index afb4e5b442fe..6810f11c4f54 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java @@ -72,8 +72,8 @@ public static Optional selectWarmUpOperation(IntermediateModel m } /** - * Required members the warm-up call must populate: those bound to the URI path or an endpoint context param, - * which reject null. Other members stay null. + * The required input members the warm-up call must give a dummy value. A member needs one when it is bound to the + * URI path (a null breaks marshalling) or is an endpoint context param (a null breaks endpoint resolution). */ static List membersRequiringDummyValue(OperationModel operation) { return inputMembers(operation).stream() @@ -112,16 +112,12 @@ private static boolean passesHardGates(OperationModel operation) { } /** - * A dummy member is fillable if it is a string. The warm-up call emits {@code "warmup"} for a plain member and an - * ARN-shaped value for an ARN member; see {@link #isArnMember}. + * A member is fillable only if it is a string, since the warm-up call emits string dummies: {@code "warmup"} for a + * plain member and an ARN-shaped value for an ARN member (see {@link #isArnMember}). */ private static boolean allDummyMembersAreFillable(OperationModel operation) { return membersRequiringDummyValue(operation).stream() - .allMatch(WarmUpOperationSelector::isDummyFillable); - } - - private static boolean isDummyFillable(MemberModel member) { - return "String".equals(member.getVariable().getSimpleType()); + .allMatch(member -> "String".equals(member.getVariable().getSimpleType())); } /** diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.java index fdf5b1ceb479..3c65cc7e078e 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -201,7 +202,7 @@ private static Stream selectionScenarios() { public void membersRequiringDummyValue_returnsOnlyUriAndEndpointBoundRequiredMembers(DummyValueScenario scenario) { List memberNames = WarmUpOperationSelector.membersRequiringDummyValue(scenario.operation).stream() .map(MemberModel::getName) - .collect(java.util.stream.Collectors.toList()); + .collect(Collectors.toList()); assertThat(memberNames).containsExactlyElementsOf(scenario.expectedMemberNames); } From 3fca192167f0d310c9bb6ca72801818a852a45d3 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Mon, 27 Jul 2026 20:44:26 +0000 Subject: [PATCH 3/3] Handle review comments --- .../poet/crac/WarmUpOperationSelector.java | 20 +++------- .../codegen/poet/crac/WarmUpProviderSpec.java | 5 +-- .../crac/WarmUpOperationSelectorTest.java | 38 +------------------ 3 files changed, 8 insertions(+), 55 deletions(-) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java index 6810f11c4f54..5dd3cb30d1be 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java @@ -72,8 +72,9 @@ public static Optional selectWarmUpOperation(IntermediateModel m } /** - * The required input members the warm-up call must give a dummy value. A member needs one when it is bound to the - * URI path (a null breaks marshalling) or is an endpoint context param (a null breaks endpoint resolution). + * Returns the required input members that need a dummy value in the warm-up call. A member needs one when it is + * bound to the URI path (a null breaks marshalling) or is an endpoint context param (a null breaks endpoint + * resolution). */ static List membersRequiringDummyValue(OperationModel operation) { return inputMembers(operation).stream() @@ -112,25 +113,14 @@ private static boolean passesHardGates(OperationModel operation) { } /** - * A member is fillable only if it is a string, since the warm-up call emits string dummies: {@code "warmup"} for a - * plain member and an ARN-shaped value for an ARN member (see {@link #isArnMember}). + * A member is fillable only if it is a string, since the warm-up call emits a string dummy value + * ({@code "warmup"}). */ private static boolean allDummyMembersAreFillable(OperationModel operation) { return membersRequiringDummyValue(operation).stream() .allMatch(member -> "String".equals(member.getVariable().getSimpleType())); } - /** - * An ARN endpoint context param needs an ARN-shaped dummy, since the endpoint rules parse it as an ARN. Identified - * by the conventional capitalized {@code Arn}/{@code ARN} name suffix (e.g. {@code resourceArn}), so a name that - * merely contains those letters (e.g. {@code learn}) does not match. - */ - static boolean isArnMember(MemberModel member) { - String name = member.getName(); - return member.getContextParam() != null - && (name.endsWith("Arn") || name.endsWith("ARN")); - } - private static boolean isUriOrEndpointBound(MemberModel member) { return (member.getHttp() != null && member.getHttp().isUri()) || member.getContextParam() != null; diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpProviderSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpProviderSpec.java index b3ed6de59a16..feff9da092e6 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpProviderSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpProviderSpec.java @@ -61,8 +61,6 @@ public class WarmUpProviderSpec implements ClassSpec { private static final String DUMMY_MEMBER_VALUE = "warmup"; - private static final String DUMMY_ARN_VALUE = "arn:aws:service:us-east-1:123456789012:warmup"; - private static final ClassName CANNED_RESPONSE_HTTP_CLIENT = ClassName.get("software.amazon.awssdk.core.crac.http", "CannedResponseHttpClient"); private static final ClassName CANNED_RESPONSE_ASYNC_HTTP_CLIENT = @@ -223,8 +221,7 @@ private CodeBlock warmUpOperationCall(OperationModel operation, String clientVar ClassName requestType = poetExtensions.getModelClass(operation.getInputShape().getShapeName()); CodeBlock.Builder request = CodeBlock.builder().add("$T.builder()", requestType); for (MemberModel member : WarmUpOperationSelector.membersRequiringDummyValue(operation)) { - String value = WarmUpOperationSelector.isArnMember(member) ? DUMMY_ARN_VALUE : DUMMY_MEMBER_VALUE; - request.add(".$N($S)", member.getFluentSetterMethodName(), value); + request.add(".$N($S)", member.getFluentSetterMethodName(), DUMMY_MEMBER_VALUE); } request.add(".build()"); return CodeBlock.builder() diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.java index 3c65cc7e078e..be11ae313779 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.java @@ -28,7 +28,6 @@ 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 software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; @@ -187,11 +186,8 @@ private static Stream selectionScenarios() { .operation(op("GetThing").withOutput().withRequiredUriMember("ThingVersion", "Integer")) .operation(op("PutThing").withOutput().withRequiredMembers(2)) .expect("PutThing"), - // An ARN context param is a string, so it is fillable with an ARN-shaped dummy value. - scenario("requiredArnContextParamMember_isStillEligible") - .operation(op("GetResource").withOutput().withRequiredContextParamMember("ResourceArn", "String")) - .expect("GetResource"), - scenario("requiredNonArnContextParamMember_isStillEligible") + // A required string context param is dummy-fillable, so the operation stays eligible. + scenario("requiredContextParamMember_isStillEligible") .operation(op("ListGrants").withOutput().withRequiredContextParamMember("AccountId", "String")) .expect("ListGrants") ); @@ -226,36 +222,6 @@ private static Stream dummyValueScenarios() { ); } - @ParameterizedTest(name = "isArnMember({0}) == {1}") - @MethodSource("arnMemberScenarios") - public void isArnMember_matchesOnlyConventionalArnSuffix(String memberName, boolean expected) { - MemberModel member = new MemberModel(); - member.setName(memberName); - member.setContextParam(new ContextParam()); - assertThat(WarmUpOperationSelector.isArnMember(member)).isEqualTo(expected); - } - - private static Stream arnMemberScenarios() { - return Stream.of( - // Conventional ARN suffix, capitalized. Both the "ARN" and "Arn" forms occur in real models. - Arguments.of("resourceARN", true), - Arguments.of("resourceArn", true), - Arguments.of("RoleArn", true), - // Lookalikes: contain the letters "arn" but are not ARN members. - Arguments.of("learn", false), - Arguments.of("warning", false), - Arguments.of("yarnConfig", false), - Arguments.of("AccountId", false) - ); - } - - @Test - public void isArnMember_withoutContextParam_isFalse() { - MemberModel member = new MemberModel(); - member.setName("resourceArn"); - assertThat(WarmUpOperationSelector.isArnMember(member)).isFalse(); - } - private static final class DummyValueScenario { private final String name; private final OperationModel operation;