From 8a81efb1a4d162dfd0d0529700273751c2c30e44 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Sun, 12 Jul 2026 16:27:01 +0000 Subject: [PATCH 1/3] Update to give more preference to operation with outputs --- .../poet/crac/WarmUpOperationSelector.java | 124 ++++++++ .../crac/WarmUpOperationSelectorTest.java | 292 ++++++++++++++++++ 2 files changed, 416 insertions(+) create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java create mode 100644 codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.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 new file mode 100644 index 000000000000..5d946143f1d4 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelector.java @@ -0,0 +1,124 @@ +/* + * 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.crac; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +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.ShapeModel; +import software.amazon.awssdk.utils.CollectionUtils; +import software.amazon.awssdk.utils.NumericUtils; + +/** + * 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}). + */ +public final class WarmUpOperationSelector { + + private static final List PREFERRED_VERBS = Arrays.asList("List", "Describe", "Get"); + + private static final Comparator BY_EMPTY_REQUEST_FIRST = + Comparator.comparing(op -> !acceptsEmptyRequest(op)); + + private static final Comparator BY_FEWEST_REQUIRED_INPUTS = + Comparator.comparingInt(WarmUpOperationSelector::requiredInputMemberCount); + + private static final Comparator BY_HAS_OUTPUT_FIRST = + Comparator.comparing(op -> !hasOutput(op)); + + private static final Comparator BY_PREFERRED_VERB = + Comparator.comparingInt(WarmUpOperationSelector::verbRank); + + private static final Comparator BY_NAME_ALPHABETICAL = + Comparator.comparing(OperationModel::getOperationName); + + private WarmUpOperationSelector() { + } + + /** + * Selects the warm-up operation for the given service, or {@link Optional#empty()} if no operation is safe to + * call as a warm-up. + */ + public static Optional selectWarmUpOperation(IntermediateModel model) { + List verifiedSimpleMethods = model.getCustomizationConfig().getVerifiedSimpleMethods(); + Comparator preference = warmUpPreference(verifiedSimpleMethods); + + return model.getOperations().values().stream() + .filter(WarmUpOperationSelector::passesHardGates) + .min(preference); + } + + /** + * Preference order: returns output (so the unmarshaller is primed too), verified simple method, accepts an + * empty request, fewest required input members, read-only verb, then operation name as the deterministic + * tie-break. + */ + private static Comparator warmUpPreference(List verifiedSimpleMethods) { + Comparator byVerifiedSimpleFirst = + Comparator.comparing(op -> !verifiedSimpleMethods.contains(op.getMethodName())); + + return BY_HAS_OUTPUT_FIRST + .thenComparing(byVerifiedSimpleFirst) + .thenComparing(BY_EMPTY_REQUEST_FIRST) + .thenComparing(BY_FEWEST_REQUIRED_INPUTS) + .thenComparing(BY_PREFERRED_VERB) + .thenComparing(BY_NAME_ALPHABETICAL); + } + + private static boolean passesHardGates(OperationModel operation) { + return !isStreamingOrEventStream(operation) + && !operation.isDeprecated(); + } + + private static boolean isStreamingOrEventStream(OperationModel operation) { + return operation.isStreaming() || operation.hasEventStreamInput() || operation.hasEventStreamOutput(); + } + + private static boolean acceptsEmptyRequest(OperationModel operation) { + return requiredInputMemberCount(operation) == 0; + } + + private static boolean hasOutput(OperationModel operation) { + return operation.getOutputShape() != null; + } + + private static int requiredInputMemberCount(OperationModel operation) { + return NumericUtils.saturatedCast(inputMembers(operation).stream().filter(MemberModel::isRequired).count()); + } + + private static List inputMembers(OperationModel operation) { + ShapeModel inputShape = operation.getInputShape(); + if (inputShape == null || CollectionUtils.isNullOrEmpty(inputShape.getMembers())) { + return Collections.emptyList(); + } + return inputShape.getMembers(); + } + + private static int verbRank(OperationModel operation) { + String operationName = operation.getOperationName(); + for (int i = 0; i < PREFERRED_VERBS.size(); i++) { + if (operationName.startsWith(PREFERRED_VERBS.get(i))) { + return i; + } + } + return Integer.MAX_VALUE; + } +} 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 new file mode 100644 index 000000000000..e528a81d9154 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/crac/WarmUpOperationSelectorTest.java @@ -0,0 +1,292 @@ +/* + * 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.crac; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +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.ShapeModel; + +public class WarmUpOperationSelectorTest { + + @ParameterizedTest(name = "{0}") + @MethodSource("selectionScenarios") + public void selectsExpectedOperation(Scenario scenario) { + Optional selected = WarmUpOperationSelector.selectWarmUpOperation(scenario.model()); + + if (scenario.expected == null) { + assertThat(selected).isEmpty(); + } else { + assertThat(selected).map(OperationModel::getOperationName).contains(scenario.expected); + } + } + + private static Stream selectionScenarios() { + return Stream.of( + // Filter: streaming, event-stream and deprecated operations. + scenario("streamingInputOperationOnly_isNotSelected") + .operation(op("PutObject").withStreamingInput()) + .expectNothing(), + scenario("streamingOutputOperationOnly_isNotSelected") + .operation(op("GetObject").withStreamingOutput()) + .expectNothing(), + scenario("eventStreamInputOperationOnly_isNotSelected") + .operation(op("StartConversation").withEventStreamInput()) + .expectNothing(), + scenario("eventStreamOutputOperationOnly_isNotSelected") + .operation(op("SubscribeToShard").withEventStreamOutput()) + .expectNothing(), + scenario("deprecatedOperationOnly_isNotSelected") + .operation(op("OldListThings").deprecated()) + .expectNothing(), + scenario("serviceWithNoOperations_selectsNothing") + .expectNothing(), + + // Preference 1: returns output, so the unmarshaller is primed too. An operation with no output shape + // (e.g. S3's DeleteBucket) can win every other tier and still lose here. + scenario("hasOutput_beatsVoidOutput_whenOtherwiseEqual") + .operation(op("ListThings").withOutput()) + .operation(op("ListOthers")) + .expect("ListThings"), + scenario("hasOutput_outranksVerifiedSimpleMethod") + .verifiedSimpleMethods("listThings") + .operation(op("ListThings")) + .operation(op("ListOthers").withOutput()) + .expect("ListOthers"), + scenario("hasOutput_outranksEmptyRequest") + .operation(op("ListThings")) + .operation(op("ListOthers").withOutput().withRequiredMembers(1)) + .expect("ListOthers"), + scenario("hasOutput_outranksFewestRequiredMembers") + .operation(op("ListThings").withRequiredMembers(1)) + .operation(op("ListOthers").withOutput().withRequiredMembers(2)) + .expect("ListOthers"), + scenario("hasOutput_outranksPreferredVerb") + .operation(op("ListThings")) + .operation(op("DescribeThings").withOutput()) + .expect("DescribeThings"), + scenario("hasOutput_outranksEveryOtherPreferenceCombined") + .verifiedSimpleMethods("deleteThings") + .operation(op("DeleteThings")) + .operation(op("ListOthers").withOutput().withRequiredMembers(1)) + .expect("ListOthers"), + + // Preference 2: verified simple method. Both operations have output so tier 1 ties. + scenario("verifiedSimpleMethod_beatsNonVerified_whenOtherwiseEqual") + .verifiedSimpleMethods("listThings") + .operation(op("ListThings").withOutput()) + .operation(op("ListOthers").withOutput()) + .expect("ListThings"), + scenario("verifiedSimpleMethod_outranksEmptyRequest") + .verifiedSimpleMethods("listThings") + .operation(op("ListThings").withOutput().withRequiredMembers(1)) + .operation(op("ListOthers").withOutput()) + .expect("ListThings"), + + // Preference 3: accepts an empty request. Both operations have output and neither is verified simple. + scenario("emptyRequest_beatsRequiredMembers") + .operation(op("ListThings").withOutput()) + .operation(op("ListOthers").withOutput().withRequiredMembers(1)) + .expect("ListThings"), + + // Preference 4: fewest required input members. Both operations have output and require input, so + // tiers 1-3 tie. + scenario("fewerRequiredMembers_beatsMore_whenBothRequireInput") + .operation(op("ListThings").withOutput().withRequiredMembers(1)) + .operation(op("ListOthers").withOutput().withRequiredMembers(2)) + .expect("ListThings"), + + // Preference 5: read-only verb, List > Describe > Get. Fixtures are chosen so the alphabetical + // tie-break would pick the loser. + scenario("verb_listBeatsDescribe") + .operation(op("DescribeThings").withOutput()) + .operation(op("ListThings").withOutput()) + .expect("ListThings"), + scenario("verb_describeBeatsNonPreferred") + .operation(op("BatchThings").withOutput()) + .operation(op("DescribeThings").withOutput()) + .expect("DescribeThings"), + scenario("verb_getBeatsNonPreferred") + .operation(op("CountThings").withOutput()) + .operation(op("GetThings").withOutput()) + .expect("GetThings"), + scenario("verb_describeBeatsGet") + .operation(op("DescribeThings").withOutput()) + .operation(op("GetThings").withOutput()) + .expect("DescribeThings"), + + // Preference 6: alphabetical tie-break. + scenario("fullyTiedOperations_areBrokenAlphabetically") + .operation(op("BravoOperation").withOutput()) + .operation(op("AlphaOperation").withOutput()) + .expect("AlphaOperation"), + + // All four operations return output. + scenario("dynamoDbWorkedExample_selectsListTables") + .verifiedSimpleMethods("listTables", "describeLimits") + .operation(op("ListTables").withOutput()) + .operation(op("DescribeLimits").withOutput()) + .operation(op("GetItem").withOutput().withRequiredMembers(2)) + .operation(op("PutItem").withRequiredMembers(2)) + .expect("ListTables") + ); + } + + private static Scenario scenario(String name) { + return new Scenario(name); + } + + private static OperationBuilder op(String operationName) { + return new OperationBuilder(operationName); + } + + private static final class Scenario { + private final String name; + private final List operations = new ArrayList<>(); + private List verifiedSimpleMethods = Collections.emptyList(); + private String expected; + + private Scenario(String name) { + this.name = name; + } + + private Scenario verifiedSimpleMethods(String... methodNames) { + this.verifiedSimpleMethods = Arrays.asList(methodNames); + return this; + } + + private Scenario operation(OperationBuilder operation) { + this.operations.add(operation.build()); + return this; + } + + private Scenario expect(String operationName) { + this.expected = operationName; + return this; + } + + private Scenario expectNothing() { + this.expected = null; + return this; + } + + private IntermediateModel model() { + Map operationsByName = new HashMap<>(); + for (OperationModel operation : operations) { + operationsByName.put(operation.getOperationName(), operation); + } + + CustomizationConfig config = CustomizationConfig.create(); + config.setVerifiedSimpleMethods(verifiedSimpleMethods); + + Metadata metadata = new Metadata().withServiceName("TestService"); + return new IntermediateModel(metadata, operationsByName, Collections.emptyMap(), config); + } + + @Override + public String toString() { + return name; + } + } + + private static final class OperationBuilder { + private final OperationModel operation = new OperationModel(); + + private OperationBuilder(String operationName) { + operation.setOperationName(operationName); + } + + private OperationBuilder withOutput() { + operation.setOutputShape(new ShapeModel()); + return this; + } + + private OperationBuilder withRequiredMembers(int requiredCount) { + ShapeModel shape = new ShapeModel(); + List members = new ArrayList<>(); + for (int i = 0; i < requiredCount; i++) { + MemberModel member = new MemberModel(); + member.setName("Member" + i); + member.setRequired(true); + members.add(member); + } + shape.setMembers(members); + operation.setInputShape(shape); + return this; + } + + private OperationBuilder withStreamingInput() { + operation.setInputShape(streamingShape()); + return this; + } + + private OperationBuilder withStreamingOutput() { + operation.setOutputShape(streamingShape()); + return this; + } + + private OperationBuilder withEventStreamInput() { + operation.setInputShape(eventStreamShape()); + return this; + } + + private OperationBuilder withEventStreamOutput() { + operation.setOutputShape(eventStreamShape()); + return this; + } + + private OperationBuilder deprecated() { + operation.setDeprecated(true); + return this; + } + + private OperationModel build() { + return operation; + } + + private static ShapeModel streamingShape() { + ShapeModel shape = new ShapeModel(); + shape.setHasStreamingMember(true); + return shape; + } + + private static ShapeModel eventStreamShape() { + ShapeModel eventStreamShape = new ShapeModel(); + eventStreamShape.withIsEventStream(true); + + MemberModel member = new MemberModel(); + member.setShape(eventStreamShape); + + ShapeModel shape = new ShapeModel(); + shape.setMembers(Collections.singletonList(member)); + return shape; + } + } +} From 194c60e1ec4bba8502760ce80898558d43ea6b09 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Tue, 14 Jul 2026 22:34:39 +0000 Subject: [PATCH 2/3] Handle review comments --- .../poet/crac/WarmUpOperationSelector.java | 31 ++++++++++++------- .../crac/WarmUpOperationSelectorTest.java | 31 ++++++++++++++----- 2 files changed, 44 insertions(+), 18 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 5d946143f1d4..a84a872a7a4c 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 @@ -35,15 +35,18 @@ public final class WarmUpOperationSelector { private static final List PREFERRED_VERBS = Arrays.asList("List", "Describe", "Get"); + private static final Comparator BY_HAS_OUTPUT_FIRST = + Comparator.comparing(op -> hasOutput(op) ? 0 : 1); + + private static final Comparator BY_AUTHENTICATED_FIRST = + Comparator.comparing(op -> op.isAuthenticated() ? 0 : 1); + private static final Comparator BY_EMPTY_REQUEST_FIRST = - Comparator.comparing(op -> !acceptsEmptyRequest(op)); + Comparator.comparing(op -> acceptsEmptyRequest(op) ? 0 : 1); private static final Comparator BY_FEWEST_REQUIRED_INPUTS = Comparator.comparingInt(WarmUpOperationSelector::requiredInputMemberCount); - private static final Comparator BY_HAS_OUTPUT_FIRST = - Comparator.comparing(op -> !hasOutput(op)); - private static final Comparator BY_PREFERRED_VERB = Comparator.comparingInt(WarmUpOperationSelector::verbRank); @@ -67,22 +70,28 @@ public static Optional selectWarmUpOperation(IntermediateModel m } /** - * Preference order: returns output (so the unmarshaller is primed too), verified simple method, accepts an - * empty request, fewest required input members, read-only verb, then operation name as the deterministic - * tie-break. + * 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, + * fewest required input members, read-only verb, then operation name as the deterministic tie-break. */ private static Comparator warmUpPreference(List verifiedSimpleMethods) { - Comparator byVerifiedSimpleFirst = - Comparator.comparing(op -> !verifiedSimpleMethods.contains(op.getMethodName())); - return BY_HAS_OUTPUT_FIRST - .thenComparing(byVerifiedSimpleFirst) + .thenComparing(BY_AUTHENTICATED_FIRST) + .thenComparing(byVerifiedSimpleFirst(verifiedSimpleMethods)) .thenComparing(BY_EMPTY_REQUEST_FIRST) .thenComparing(BY_FEWEST_REQUIRED_INPUTS) .thenComparing(BY_PREFERRED_VERB) .thenComparing(BY_NAME_ALPHABETICAL); } + /** + * Unlike the other tiers, this one depends on the service's customization config, so it cannot be a static + * comparator constant. + */ + private static Comparator byVerifiedSimpleFirst(List verifiedSimpleMethods) { + return Comparator.comparing(op -> verifiedSimpleMethods.contains(op.getMethodName()) ? 0 : 1); + } + private static boolean passesHardGates(OperationModel operation) { return !isStreamingOrEventStream(operation) && !operation.isDeprecated(); 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 e528a81d9154..73c9eff943fb 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 @@ -98,7 +98,19 @@ private static Stream selectionScenarios() { .operation(op("ListOthers").withOutput().withRequiredMembers(1)) .expect("ListOthers"), - // Preference 2: verified simple method. Both operations have output so tier 1 ties. + // Preference 2: is authenticated, so signing is primed too. Both operations have output so tier 1 ties. + scenario("authenticated_beatsNoAuth_whenOtherwiseEqual") + .operation(op("ListThings").withOutput()) + .operation(op("ListOthers").withOutput().withNoAuth()) + .expect("ListThings"), + scenario("authenticated_outranksVerifiedSimpleMethod") + .verifiedSimpleMethods("listOthers") + .operation(op("ListThings").withOutput()) + .operation(op("ListOthers").withOutput().withNoAuth()) + .expect("ListThings"), + + // Preference 3: verified simple method. Both operations have output and are authenticated, so tiers + // 1-2 tie. scenario("verifiedSimpleMethod_beatsNonVerified_whenOtherwiseEqual") .verifiedSimpleMethods("listThings") .operation(op("ListThings").withOutput()) @@ -110,20 +122,21 @@ private static Stream selectionScenarios() { .operation(op("ListOthers").withOutput()) .expect("ListThings"), - // Preference 3: accepts an empty request. Both operations have output and neither is verified simple. + // Preference 4: accepts an empty request. Both operations have output and are authenticated, and + // neither is verified simple, so tiers 1-3 tie. scenario("emptyRequest_beatsRequiredMembers") .operation(op("ListThings").withOutput()) .operation(op("ListOthers").withOutput().withRequiredMembers(1)) .expect("ListThings"), - // Preference 4: fewest required input members. Both operations have output and require input, so - // tiers 1-3 tie. + // Preference 5: fewest required input members. Both operations have output and require input, so + // tiers 1-4 tie. scenario("fewerRequiredMembers_beatsMore_whenBothRequireInput") .operation(op("ListThings").withOutput().withRequiredMembers(1)) .operation(op("ListOthers").withOutput().withRequiredMembers(2)) .expect("ListThings"), - // Preference 5: read-only verb, List > Describe > Get. Fixtures are chosen so the alphabetical + // Preference 6: read-only verb, List > Describe > Get. Fixtures are chosen so the alphabetical // tie-break would pick the loser. scenario("verb_listBeatsDescribe") .operation(op("DescribeThings").withOutput()) @@ -142,13 +155,12 @@ private static Stream selectionScenarios() { .operation(op("GetThings").withOutput()) .expect("DescribeThings"), - // Preference 6: alphabetical tie-break. + // Preference 7: alphabetical tie-break. scenario("fullyTiedOperations_areBrokenAlphabetically") .operation(op("BravoOperation").withOutput()) .operation(op("AlphaOperation").withOutput()) .expect("AlphaOperation"), - // All four operations return output. scenario("dynamoDbWorkedExample_selectsListTables") .verifiedSimpleMethods("listTables", "describeLimits") .operation(op("ListTables").withOutput()) @@ -267,6 +279,11 @@ private OperationBuilder deprecated() { return this; } + private OperationBuilder withNoAuth() { + operation.setIsAuthenticated(false); + return this; + } + private OperationModel build() { return operation; } From 6a8597c146cb4e0c6dc4e268939e70aadb1eea81 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Fri, 17 Jul 2026 14:29:09 +0000 Subject: [PATCH 3/3] feat(codegen): invoke selected warm-up operation in generated SdkWarmUpProvider --- .../codegen/poet/crac/WarmUpProviderSpec.java | 81 ++++++++++++------- .../poet/crac/warmup-provider-async-only.java | 2 + .../poet/crac/warmup-provider-cbor.java | 2 + .../poet/crac/warmup-provider-query.java | 3 + .../poet/crac/warmup-provider-rest-json.java | 2 + .../poet/crac/warmup-provider-rpcv2.java | 3 + .../poet/crac/warmup-provider-xml.java | 3 + .../crac/OperationRecordingInterceptor.java | 46 +++++++++++ .../crac/WarmUpProviderBindingTest.java | 74 +++++++++++++++++ .../global/handlers/execution.interceptors | 1 + 10 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/crac/OperationRecordingInterceptor.java create mode 100644 test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/crac/WarmUpProviderBindingTest.java create mode 100644 test/codegen-generated-classes-test/src/test/resources/software/amazon/awssdk/global/handlers/execution.interceptors 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 57923e2aed3d..b121f391333b 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 @@ -22,9 +22,11 @@ import com.squareup.javapoet.TypeSpec; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.Optional; 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.OperationModel; import software.amazon.awssdk.codegen.model.intermediate.Protocol; import software.amazon.awssdk.codegen.poet.ClassSpec; import software.amazon.awssdk.codegen.poet.PoetExtension; @@ -38,10 +40,10 @@ *

The {@code warmUpClient(ClientType)} body instantiates and closes a synthetic sync client (when the service * generates one) and a synthetic async client, each guarded by the requested {@link ClientType} and wired to an * in-memory canned HTTP client, dummy credentials, a fixed region and a local {@code endpointOverride}. Building the - * clients JIT-compiles the client construction and configuration-resolution path before a CRaC checkpoint. The - * {@code syncClientClassName()}/{@code asyncClientClassName()} strings let {@code SdkWarmUp.prime(Class...)} match a - * requested client class to this provider without loading excluded client interfaces. The operation call that - * exercises the marshal/unmarshal pipeline is added in a later stage. + * clients JIT-compiles the client construction and configuration-resolution path before a CRaC checkpoint. Each client + * also invokes the operation chosen by {@link WarmUpOperationSelector}, if any, to prime the marshalling, signing and + * unmarshalling paths. The {@code syncClientClassName()}/{@code asyncClientClassName()} strings let + * {@code SdkWarmUp.prime(Class...)} match a requested client class to this provider. */ public class WarmUpProviderSpec implements ClassSpec { @@ -71,10 +73,12 @@ public class WarmUpProviderSpec implements ClassSpec { private final IntermediateModel model; private final PoetExtension poetExtensions; + private final Optional warmUpOperation; public WarmUpProviderSpec(IntermediateModel model) { this.model = model; this.poetExtensions = new PoetExtension(model); + this.warmUpOperation = WarmUpOperationSelector.selectWarmUpOperation(model); } @Override @@ -123,12 +127,12 @@ private MethodSpec warmUpClientMethod() { if (!model.getCustomizationConfig().isSkipSyncClientGeneration()) { body.beginControlFlow("if (clientType == $T.SYNC)", ClientType.class) .add(clientBlock(syncClientClass(), - CANNED_RESPONSE_HTTP_CLIENT, SDK_HTTP_CLIENT, "httpClient", "client")) + CANNED_RESPONSE_HTTP_CLIENT, SDK_HTTP_CLIENT, "httpClient", "client", false)) .endControlFlow(); } body.beginControlFlow("if (clientType == $T.ASYNC)", ClientType.class) .add(clientBlock(asyncClientClass(), - CANNED_RESPONSE_ASYNC_HTTP_CLIENT, SDK_ASYNC_HTTP_CLIENT, "asyncHttpClient", "asyncClient")) + CANNED_RESPONSE_ASYNC_HTTP_CLIENT, SDK_ASYNC_HTTP_CLIENT, "asyncHttpClient", "asyncClient", true)) .endControlFlow(); return MethodSpec.methodBuilder("warmUpClient") @@ -148,31 +152,52 @@ private ClassName asyncClientClass() { } /** - * Emits a canned HTTP client plus a try-with-resources that builds and closes {@code clientType}. The sync and async - * paths differ only in these types and variable names, so they share this emitter. + * Emits a canned HTTP client plus a try-with-resources that builds {@code clientType}, calls the selected + * warm-up operation (if any) and closes the client. The sync and async paths differ only in these types, variable + * names and joining the async call, so they share this emitter. */ private CodeBlock clientBlock(ClassName clientType, ClassName cannedHttpClientType, ClassName httpClientType, - String httpClientVar, String clientVar) { + String httpClientVar, String clientVar, boolean async) { + CodeBlock.Builder block = CodeBlock.builder() + .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); + + warmUpOperation.ifPresent(op -> block.add(warmUpOperationCall(op, clientVar, async))); + + return block.endControlFlow() + .build(); + } + + /** + * Verified simple methods generate a no-arg overload on both clients, so the call uses it. Other operations pass + * an empty request. + */ + private CodeBlock warmUpOperationCall(OperationModel operation, String clientVar, boolean async) { + String join = async ? ".join()" : ""; + if (operation.getInputShape().isSimpleMethod()) { + return CodeBlock.builder() + .addStatement("$N.$N()" + join, clientVar, operation.getMethodName()) + .build(); + } + ClassName requestType = poetExtensions.getModelClass(operation.getInputShape().getShapeName()); return CodeBlock.builder() - .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) - .endControlFlow() + .addStatement("$N.$N($T.builder().build())" + join, clientVar, operation.getMethodName(), requestType) .build(); } 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 69aa99b17e53..9fc448858286 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 @@ -12,6 +12,7 @@ import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.query.QueryAsyncClient; +import software.amazon.awssdk.services.query.model.GetOperationWithChecksumRequest; @Generated("software.amazon.awssdk:codegen") @SdkInternalApi @@ -36,6 +37,7 @@ public void warmUpClient(ClientType clientType) { try (QueryAsyncClient asyncClient = QueryAsyncClient.builder().httpClient(asyncHttpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) .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-cbor.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-cbor.java index fd3ec7738eb9..380546713be2 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-cbor.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-cbor.java @@ -37,6 +37,7 @@ public void warmUpClient(ClientType clientType) { try (JsonClient client = JsonClient.builder().httpClient(httpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { + client.paginatedOperationWithResultKey(); } } if (clientType == ClientType.ASYNC) { @@ -45,6 +46,7 @@ public void warmUpClient(ClientType clientType) { try (JsonAsyncClient asyncClient = JsonAsyncClient.builder().httpClient(asyncHttpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) .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-query.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-query.java index b754976dbcad..2b247d6448d1 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 @@ -15,6 +15,7 @@ import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.query.QueryAsyncClient; import software.amazon.awssdk.services.query.QueryClient; +import software.amazon.awssdk.services.query.model.GetOperationWithChecksumRequest; @Generated("software.amazon.awssdk:codegen") @SdkInternalApi @@ -38,6 +39,7 @@ public void warmUpClient(ClientType clientType) { try (QueryClient client = QueryClient.builder().httpClient(httpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { + client.getOperationWithChecksum(GetOperationWithChecksumRequest.builder().build()); } } if (clientType == ClientType.ASYNC) { @@ -46,6 +48,7 @@ public void warmUpClient(ClientType clientType) { try (QueryAsyncClient asyncClient = QueryAsyncClient.builder().httpClient(asyncHttpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) .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 30a9f3662aa9..dd23589f3db9 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 @@ -38,6 +38,7 @@ public void warmUpClient(ClientType clientType) { try (JsonClient client = JsonClient.builder().httpClient(httpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { + client.paginatedOperationWithResultKey(); } } if (clientType == ClientType.ASYNC) { @@ -46,6 +47,7 @@ public void warmUpClient(ClientType clientType) { try (JsonAsyncClient asyncClient = JsonAsyncClient.builder().httpClient(asyncHttpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) .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-rpcv2.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-rpcv2.java index 070b4d8e2a91..9cde46dc7759 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-rpcv2.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-rpcv2.java @@ -14,6 +14,7 @@ import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.smithyrpcv2protocol.SmithyRpcV2ProtocolAsyncClient; import software.amazon.awssdk.services.smithyrpcv2protocol.SmithyRpcV2ProtocolClient; +import software.amazon.awssdk.services.smithyrpcv2protocol.model.EmptyInputOutputRequest; @Generated("software.amazon.awssdk:codegen") @SdkInternalApi @@ -37,6 +38,7 @@ public void warmUpClient(ClientType clientType) { try (SmithyRpcV2ProtocolClient client = SmithyRpcV2ProtocolClient.builder().httpClient(httpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { + client.emptyInputOutput(EmptyInputOutputRequest.builder().build()); } } if (clientType == ClientType.ASYNC) { @@ -46,6 +48,7 @@ public void warmUpClient(ClientType clientType) { .httpClient(asyncHttpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { + asyncClient.emptyInputOutput(EmptyInputOutputRequest.builder().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 3b22c563bc98..58c8e63b692e 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 @@ -15,6 +15,7 @@ import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.xml.XmlAsyncClient; import software.amazon.awssdk.services.xml.XmlClient; +import software.amazon.awssdk.services.xml.model.GetOperationWithChecksumRequest; @Generated("software.amazon.awssdk:codegen") @SdkInternalApi @@ -38,6 +39,7 @@ public void warmUpClient(ClientType clientType) { try (XmlClient client = XmlClient.builder().httpClient(httpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { + client.getOperationWithChecksum(GetOperationWithChecksumRequest.builder().build()); } } if (clientType == ClientType.ASYNC) { @@ -46,6 +48,7 @@ public void warmUpClient(ClientType clientType) { try (XmlAsyncClient asyncClient = XmlAsyncClient.builder().httpClient(asyncHttpClient) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { + asyncClient.getOperationWithChecksum(GetOperationWithChecksumRequest.builder().build()).join(); } } } diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/crac/OperationRecordingInterceptor.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/crac/OperationRecordingInterceptor.java new file mode 100644 index 000000000000..30d3d6196e95 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/crac/OperationRecordingInterceptor.java @@ -0,0 +1,46 @@ +/* + * 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.crac; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; + +/** + * Records invoked operation names. Registered as a global interceptor via + * {@code software/amazon/awssdk/global/handlers/execution.interceptors} so tests can observe calls made by clients + * they cannot configure, such as the client a generated {@code SdkWarmUpProvider} builds internally. + */ +public class OperationRecordingInterceptor implements ExecutionInterceptor { + + private static final List OPERATION_NAMES = new CopyOnWriteArrayList<>(); + + public static List operationNames() { + return OPERATION_NAMES; + } + + public static void reset() { + OPERATION_NAMES.clear(); + } + + @Override + public void beforeExecution(Context.BeforeExecution context, ExecutionAttributes executionAttributes) { + OPERATION_NAMES.add(executionAttributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME)); + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/crac/WarmUpProviderBindingTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/crac/WarmUpProviderBindingTest.java new file mode 100644 index 000000000000..431b14035ff3 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/crac/WarmUpProviderBindingTest.java @@ -0,0 +1,74 @@ +/* + * 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.crac; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClient; +import software.amazon.awssdk.services.protocolrestjson.internal.crac.ProtocolRestJsonWarmUpProvider; + +/** + * Verifies the generated {@code SdkWarmUpProvider} invokes the operation selected by + * {@code WarmUpOperationSelector}. {@link OperationRecordingInterceptor} is registered as a global interceptor to + * observe calls made by the clients the provider builds internally. + */ +class WarmUpProviderBindingTest { + + /** + * The operation WarmUpOperationSelector picks for the ProtocolRestJson test model. + */ + private static final String EXPECTED_OPERATION = "AllTypes"; + + private final ProtocolRestJsonWarmUpProvider provider = new ProtocolRestJsonWarmUpProvider(); + + @BeforeEach + void resetRecorder() { + OperationRecordingInterceptor.reset(); + } + + @Test + void warmUpClient_sync_invokesSelectedOperation() { + assertThatCode(() -> provider.warmUpClient(ClientType.SYNC)).doesNotThrowAnyException(); + + assertThat(OperationRecordingInterceptor.operationNames()).containsExactly(EXPECTED_OPERATION); + } + + @Test + void warmUpClient_async_invokesSelectedOperation() { + assertThatCode(() -> provider.warmUpClient(ClientType.ASYNC)).doesNotThrowAnyException(); + + assertThat(OperationRecordingInterceptor.operationNames()).containsExactly(EXPECTED_OPERATION); + } + + @Test + void warmUp_invokesSelectedOperationOnBothClients() { + assertThatCode(provider::warmUp).doesNotThrowAnyException(); + + assertThat(OperationRecordingInterceptor.operationNames()) + .containsExactly(EXPECTED_OPERATION, EXPECTED_OPERATION); + } + + @Test + void clientClassNames_matchGeneratedClients() { + assertThat(provider.syncClientClassName()).isEqualTo(ProtocolRestJsonClient.class.getName()); + assertThat(provider.asyncClientClassName()).isEqualTo(ProtocolRestJsonAsyncClient.class.getName()); + } +} diff --git a/test/codegen-generated-classes-test/src/test/resources/software/amazon/awssdk/global/handlers/execution.interceptors b/test/codegen-generated-classes-test/src/test/resources/software/amazon/awssdk/global/handlers/execution.interceptors new file mode 100644 index 000000000000..896d3f2aecae --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/resources/software/amazon/awssdk/global/handlers/execution.interceptors @@ -0,0 +1 @@ +software.amazon.awssdk.crac.OperationRecordingInterceptor