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