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