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 482164c30cc0..3aedf560478d 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 @@ -16,26 +16,61 @@ package software.amazon.awssdk.codegen.poet.crac; import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.TypeSpec; +import java.net.URI; +import java.nio.charset.StandardCharsets; 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.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.core.crac.SdkWarmUpProvider; /** - * Generates an empty {@link SdkWarmUpProvider} implementation per service. The {@code warmUp()} body is intentionally a - * no-op in this stage; the synthetic priming call is added in a later stage. ServiceLoader requires a public no-arg - * constructor, which the default constructor satisfies. + * Generates an {@link SdkWarmUpProvider} implementation per service. + * + *

The {@code warmUp()} body instantiates and closes a synthetic sync client (when the service generates one) and a + * synthetic async client, each 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 operation call that exercises the marshal/unmarshal pipeline is added in a later stage. */ public class WarmUpProviderSpec implements ClassSpec { + private static final String CANNED_RESPONSE_FIELD = "CANNED_RESPONSE"; + + // Values emitted into the warm-up call. Dummy credentials and a local endpoint keep the call offline; a 200 status + // exercises the success path. + 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 LOCAL_ENDPOINT = "http://localhost"; + + private static final ClassName CANNED_RESPONSE_HTTP_CLIENT = + ClassName.get("software.amazon.awssdk.core.internal.crac", "CannedResponseHttpClient"); + private static final ClassName CANNED_RESPONSE_ASYNC_HTTP_CLIENT = + ClassName.get("software.amazon.awssdk.core.internal.crac", "CannedResponseAsyncHttpClient"); + private static final ClassName SDK_HTTP_CLIENT = + ClassName.get("software.amazon.awssdk.http", "SdkHttpClient"); + private static final ClassName SDK_ASYNC_HTTP_CLIENT = + ClassName.get("software.amazon.awssdk.http.async", "SdkAsyncHttpClient"); + private static final ClassName STATIC_CREDENTIALS_PROVIDER = + 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 REGION = + ClassName.get("software.amazon.awssdk.regions", "Region"); + private final IntermediateModel model; + private final PoetExtension poetExtensions; public WarmUpProviderSpec(IntermediateModel model) { this.model = model; + this.poetExtensions = new PoetExtension(model); } @Override @@ -50,14 +85,86 @@ public TypeSpec poetSpec() { .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addAnnotation(SdkInternalApi.class) .addSuperinterface(SdkWarmUpProvider.class) + .addField(cannedResponseField()) .addMethod(warmUpMethod()) .build(); } private MethodSpec warmUpMethod() { + CodeBlock.Builder body = CodeBlock.builder(); + if (!model.getCustomizationConfig().isSkipSyncClientGeneration()) { + body.add(clientBlock(poetExtensions.getClientClass(model.getMetadata().getSyncInterface()), + CANNED_RESPONSE_HTTP_CLIENT, SDK_HTTP_CLIENT, "httpClient", "client")); + } + body.add(clientBlock(poetExtensions.getClientClass(model.getMetadata().getAsyncInterface()), + CANNED_RESPONSE_ASYNC_HTTP_CLIENT, SDK_ASYNC_HTTP_CLIENT, "asyncHttpClient", "asyncClient")); + return MethodSpec.methodBuilder("warmUp") .addAnnotation(Override.class) .addModifiers(Modifier.PUBLIC) + .addCode(body.build()) .build(); } + + /** + * 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. + */ + private CodeBlock clientBlock(ClassName clientType, ClassName cannedHttpClientType, ClassName httpClientType, + String httpClientVar, String clientVar) { + 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() + .build(); + } + + private FieldSpec cannedResponseField() { + return FieldSpec.builder(byte[].class, CANNED_RESPONSE_FIELD, + Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer(cannedResponseInitializer(model.getMetadata().getProtocol())) + .build(); + } + + /** + * A minimal, valid 200 response body per protocol. An empty body unmarshals for every protocol. + */ + private CodeBlock cannedResponseInitializer(Protocol protocol) { + switch (protocol) { + case REST_JSON: + case AWS_JSON: + return textInitializer("{}"); + case QUERY: + case EC2: + case REST_XML: + return textInitializer(""); + case CBOR: + case SMITHY_RPC_V2_CBOR: + // Both are binary CBOR protocols, so a text "{}" is not a valid body. 0xA0 is the single-byte CBOR + // encoding of an empty map, the equivalent of "{}" for JSON. + return CodeBlock.of("new byte[] {(byte) 0xA0}"); + default: + throw new IllegalArgumentException("Unsupported protocol for CRaC warm-up canned response: " + protocol); + } + } + + private CodeBlock textInitializer(String body) { + return CodeBlock.of("$S.getBytes($T.UTF_8)", body, StandardCharsets.class); + } } 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 2a69773e2a34..180dbc55a459 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 @@ -25,8 +25,39 @@ public class WarmUpProviderSpecTest { @Test - public void warmUpProvider() { + public void warmUpProvider_queryProtocol_generatesSyncAndAsyncClients() { ClassSpec spec = new WarmUpProviderSpec(ClientTestModels.queryServiceModels()); assertThat(spec, generatesTo("warmup-provider.java")); } + + @Test + public void warmUpProvider_whenSyncClientSkipped_generatesAsyncClientOnly() { + ClassSpec spec = new WarmUpProviderSpec( + ClientTestModels.queryServiceModelWithSpecialCustomization("customization-skip-sync.config")); + assertThat(spec, generatesTo("warmup-provider-async-only.java")); + } + + @Test + public void warmUpProvider_restJsonProtocol_usesJsonCannedResponse() { + ClassSpec spec = new WarmUpProviderSpec(ClientTestModels.restJsonServiceModels()); + assertThat(spec, generatesTo("warmup-provider-rest-json.java")); + } + + @Test + public void warmUpProvider_restXmlProtocol_usesXmlCannedResponse() { + ClassSpec spec = new WarmUpProviderSpec(ClientTestModels.xmlServiceModels()); + assertThat(spec, generatesTo("warmup-provider-xml.java")); + } + + @Test + public void warmUpProvider_smithyRpcV2CborProtocol_usesEmptyCborMapCannedResponse() { + ClassSpec spec = new WarmUpProviderSpec(ClientTestModels.rpcv2ServiceModels()); + assertThat(spec, generatesTo("warmup-provider-rpcv2.java")); + } + + @Test + public void warmUpProvider_cborProtocol_usesEmptyCborMapCannedResponse() { + ClassSpec spec = new WarmUpProviderSpec(ClientTestModels.cborServiceModels()); + assertThat(spec, generatesTo("warmup-provider-cbor.java")); + } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/customization-skip-sync.config b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/customization-skip-sync.config new file mode 100644 index 000000000000..a76859089932 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/customization-skip-sync.config @@ -0,0 +1,3 @@ +{ + "skipSyncClientGeneration": true +} 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 new file mode 100644 index 000000000000..9265d20bff3d --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-async-only.java @@ -0,0 +1,29 @@ +package software.amazon.awssdk.services.query.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.crac.SdkWarmUpProvider; +import software.amazon.awssdk.core.internal.crac.CannedResponseAsyncHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.query.QueryAsyncClient; + +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +public final class QueryWarmUpProvider implements SdkWarmUpProvider { + private static final byte[] CANNED_RESPONSE = "".getBytes(StandardCharsets.UTF_8); + + @Override + public void warmUp() { + SdkAsyncHttpClient asyncHttpClient = CannedResponseAsyncHttpClient.builder().responseBody(CANNED_RESPONSE) + .statusCode(200).build(); + 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()) { + } + } +} 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 new file mode 100644 index 000000000000..0825c84b3238 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-cbor.java @@ -0,0 +1,36 @@ +package software.amazon.awssdk.services.json.internal.crac; + +import java.net.URI; +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.crac.SdkWarmUpProvider; +import software.amazon.awssdk.core.internal.crac.CannedResponseAsyncHttpClient; +import software.amazon.awssdk.core.internal.crac.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; + +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +public final class JsonWarmUpProvider implements SdkWarmUpProvider { + private static final byte[] CANNED_RESPONSE = new byte[] { (byte) 0xA0 }; + + @Override + public void warmUp() { + SdkHttpClient httpClient = CannedResponseHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); + 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()) { + } + SdkAsyncHttpClient asyncHttpClient = CannedResponseAsyncHttpClient.builder().responseBody(CANNED_RESPONSE) + .statusCode(200).build(); + 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()) { + } + } +} 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 new file mode 100644 index 000000000000..be2a37495850 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-rest-json.java @@ -0,0 +1,37 @@ +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.core.crac.SdkWarmUpProvider; +import software.amazon.awssdk.core.internal.crac.CannedResponseAsyncHttpClient; +import software.amazon.awssdk.core.internal.crac.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; + +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +public final class JsonWarmUpProvider implements SdkWarmUpProvider { + private static final byte[] CANNED_RESPONSE = "{}".getBytes(StandardCharsets.UTF_8); + + @Override + public void warmUp() { + SdkHttpClient httpClient = CannedResponseHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); + 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()) { + } + SdkAsyncHttpClient asyncHttpClient = CannedResponseAsyncHttpClient.builder().responseBody(CANNED_RESPONSE) + .statusCode(200).build(); + 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()) { + } + } +} 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 new file mode 100644 index 000000000000..70fb5564a252 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-rpcv2.java @@ -0,0 +1,36 @@ +package software.amazon.awssdk.services.smithyrpcv2protocol.internal.crac; + +import java.net.URI; +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.crac.SdkWarmUpProvider; +import software.amazon.awssdk.core.internal.crac.CannedResponseAsyncHttpClient; +import software.amazon.awssdk.core.internal.crac.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.smithyrpcv2protocol.SmithyRpcV2ProtocolAsyncClient; +import software.amazon.awssdk.services.smithyrpcv2protocol.SmithyRpcV2ProtocolClient; + +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +public final class SmithyRpcV2ProtocolWarmUpProvider implements SdkWarmUpProvider { + private static final byte[] CANNED_RESPONSE = new byte[] { (byte) 0xA0 }; + + @Override + public void warmUp() { + SdkHttpClient httpClient = CannedResponseHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); + 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()) { + } + SdkAsyncHttpClient asyncHttpClient = CannedResponseAsyncHttpClient.builder().responseBody(CANNED_RESPONSE) + .statusCode(200).build(); + try (SmithyRpcV2ProtocolAsyncClient asyncClient = SmithyRpcV2ProtocolAsyncClient.builder().httpClient(asyncHttpClient) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .region(Region.US_EAST_1).endpointOverride(URI.create("http://localhost")).build()) { + } + } +} 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 new file mode 100644 index 000000000000..5bb50541d874 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider-xml.java @@ -0,0 +1,37 @@ +package software.amazon.awssdk.services.xml.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.crac.SdkWarmUpProvider; +import software.amazon.awssdk.core.internal.crac.CannedResponseAsyncHttpClient; +import software.amazon.awssdk.core.internal.crac.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.xml.XmlAsyncClient; +import software.amazon.awssdk.services.xml.XmlClient; + +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +public final class XmlWarmUpProvider implements SdkWarmUpProvider { + private static final byte[] CANNED_RESPONSE = "".getBytes(StandardCharsets.UTF_8); + + @Override + public void warmUp() { + SdkHttpClient httpClient = CannedResponseHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); + 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()) { + } + SdkAsyncHttpClient asyncHttpClient = CannedResponseAsyncHttpClient.builder().responseBody(CANNED_RESPONSE) + .statusCode(200).build(); + 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()) { + } + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider.java index 9b92d51d2d71..2c243bd4cd67 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/crac/warmup-provider.java @@ -1,13 +1,37 @@ package software.amazon.awssdk.services.query.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.crac.SdkWarmUpProvider; +import software.amazon.awssdk.core.internal.crac.CannedResponseAsyncHttpClient; +import software.amazon.awssdk.core.internal.crac.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.query.QueryAsyncClient; +import software.amazon.awssdk.services.query.QueryClient; @Generated("software.amazon.awssdk:codegen") @SdkInternalApi public final class QueryWarmUpProvider implements SdkWarmUpProvider { + private static final byte[] CANNED_RESPONSE = "".getBytes(StandardCharsets.UTF_8); + @Override public void warmUp() { + SdkHttpClient httpClient = CannedResponseHttpClient.builder().responseBody(CANNED_RESPONSE).statusCode(200).build(); + 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()) { + } + SdkAsyncHttpClient asyncHttpClient = CannedResponseAsyncHttpClient.builder().responseBody(CANNED_RESPONSE) + .statusCode(200).build(); + 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()) { + } } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/CannedResponseAsyncHttpClient.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/CannedResponseAsyncHttpClient.java new file mode 100644 index 000000000000..bba18aa7693a --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/CannedResponseAsyncHttpClient.java @@ -0,0 +1,116 @@ +/* + * 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.internal.crac; + +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.utils.async.SimplePublisher; +import software.amazon.awssdk.utils.builder.SdkBuilder; + +/** + * An {@link SdkAsyncHttpClient} that completes every request with caller-supplied response bytes without performing any + * network I/O. It is the async counterpart of {@link CannedResponseHttpClient}, used to warm the async request pipeline for a + * CRaC checkpoint. + * + *

+ * Each {@link #execute(AsyncExecuteRequest)} signals the configured status code, publishes the response bytes through a + * {@link SimplePublisher}, and returns an already-completed future. + */ +@SdkInternalApi +@Immutable +@ThreadSafe +public final class CannedResponseAsyncHttpClient implements SdkAsyncHttpClient { + + private static final int DEFAULT_STATUS_CODE = 200; + private static final byte[] EMPTY_BODY = new byte[0]; + + private final byte[] responseBody; + private final int statusCode; + + private CannedResponseAsyncHttpClient(DefaultBuilder builder) { + this.responseBody = builder.responseBody == null ? EMPTY_BODY : builder.responseBody.clone(); + this.statusCode = builder.statusCode == null ? DEFAULT_STATUS_CODE : builder.statusCode; + } + + public static Builder builder() { + return new DefaultBuilder(); + } + + @Override + public CompletableFuture execute(AsyncExecuteRequest request) { + request.responseHandler().onHeaders(SdkHttpResponse.builder().statusCode(statusCode).build()); + + SimplePublisher bodyPublisher = new SimplePublisher<>(); + request.responseHandler().onStream(bodyPublisher); + if (responseBody.length > 0) { + bodyPublisher.send(ByteBuffer.wrap(responseBody)); + } + bodyPublisher.complete(); + + return CompletableFuture.completedFuture(null); + } + + @Override + public String clientName() { + return "CannedResponseAsync"; + } + + @Override + public void close() { + } + + public interface Builder extends SdkBuilder { + + /** + * The response body published by every {@link #execute(AsyncExecuteRequest)}. Optional; defaults to an empty body. + */ + Builder responseBody(byte[] responseBody); + + /** + * The HTTP status code signalled by every {@link #execute(AsyncExecuteRequest)}. Defaults to {@code 200}. + */ + Builder statusCode(int statusCode); + } + + private static final class DefaultBuilder implements Builder { + + private byte[] responseBody; + private Integer statusCode; + + @Override + public Builder responseBody(byte[] responseBody) { + this.responseBody = responseBody; + return this; + } + + @Override + public Builder statusCode(int statusCode) { + this.statusCode = statusCode; + return this; + } + + @Override + public CannedResponseAsyncHttpClient build() { + return new CannedResponseAsyncHttpClient(this); + } + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/CannedResponseAsyncHttpClientTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/CannedResponseAsyncHttpClientTest.java new file mode 100644 index 000000000000..4b1430240c96 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/CannedResponseAsyncHttpClientTest.java @@ -0,0 +1,143 @@ +/* + * 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.internal.crac; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; +import software.amazon.awssdk.http.async.SimpleSubscriber; + +class CannedResponseAsyncHttpClientTest { + + private static final byte[] PAYLOAD = "{\"StringMember\":\"warmup\"}".getBytes(StandardCharsets.UTF_8); + + @Test + void execute_whenNoStatusGiven_signalsDefault200() { + CannedResponseAsyncHttpClient client = CannedResponseAsyncHttpClient.builder().responseBody(PAYLOAD).build(); + RecordingResponseHandler handler = new RecordingResponseHandler(); + + client.execute(requestWith(handler)).join(); + + assertThat(handler.headers.statusCode()).isEqualTo(200); + } + + @Test + void execute_whenStatusGiven_signalsConfiguredStatus() { + CannedResponseAsyncHttpClient client = + CannedResponseAsyncHttpClient.builder().responseBody(PAYLOAD).statusCode(500).build(); + RecordingResponseHandler handler = new RecordingResponseHandler(); + + client.execute(requestWith(handler)).join(); + + assertThat(handler.headers.statusCode()).isEqualTo(500); + } + + @Test + void execute_whenInvoked_streamsConfiguredBody() { + CannedResponseAsyncHttpClient client = CannedResponseAsyncHttpClient.builder().responseBody(PAYLOAD).build(); + RecordingResponseHandler handler = new RecordingResponseHandler(); + + client.execute(requestWith(handler)).join(); + + assertThat(handler.drainBody()).isEqualTo(PAYLOAD); + } + + @Test + void execute_whenNoBodyGiven_signalsDefault200AndEmptyBody() { + CannedResponseAsyncHttpClient client = CannedResponseAsyncHttpClient.builder().build(); + RecordingResponseHandler handler = new RecordingResponseHandler(); + + client.execute(requestWith(handler)).join(); + + assertThat(handler.headers.statusCode()).isEqualTo(200); + assertThat(handler.drainBody()).isEmpty(); + } + + @Test + void execute_whenInvoked_returnsCompletedFuture() { + CannedResponseAsyncHttpClient client = CannedResponseAsyncHttpClient.builder().responseBody(PAYLOAD).build(); + + CompletableFuture future = client.execute(requestWith(new RecordingResponseHandler())); + + assertThat(future).isCompleted(); + } + + @Test + void close_isSafeNoOp() { + CannedResponseAsyncHttpClient client = CannedResponseAsyncHttpClient.builder().build(); + + assertThatCode(client::close).doesNotThrowAnyException(); + } + + private static AsyncExecuteRequest requestWith(SdkAsyncHttpResponseHandler handler) { + return AsyncExecuteRequest.builder() + .responseHandler(handler) + .build(); + } + + private static final class RecordingResponseHandler implements SdkAsyncHttpResponseHandler { + + private SdkHttpResponse headers; + private Publisher stream; + + @Override + public void onHeaders(SdkHttpResponse headers) { + this.headers = headers; + } + + @Override + public void onStream(Publisher stream) { + this.stream = stream; + } + + @Override + public void onError(Throwable error) { + } + + private byte[] drainBody() { + ByteArrayOutputStream collected = new ByteArrayOutputStream(); + CompletableFuture done = new CompletableFuture<>(); + stream.subscribe(new SimpleSubscriber(byteBuffer -> { + byte[] chunk = new byte[byteBuffer.remaining()]; + byteBuffer.get(chunk); + collected.write(chunk, 0, chunk.length); + }) { + @Override + public void onComplete() { + super.onComplete(); + done.complete(null); + } + + @Override + public void onError(Throwable t) { + super.onError(t); + done.completeExceptionally(t); + } + }); + done.join(); + return collected.toByteArray(); + } + } +}