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

Also catches {@link LinkageError}: a task can fail to link (missing dependency or native library) and that + * is an {@link Error}, not an {@link Exception}. Other errors still propagate. + */ + public static boolean runSafely(String description, Runnable task) { + try { + task.run(); + return true; + } catch (RuntimeException | LinkageError e) { + log.warn(() -> "Warm-up failed for " + description + " and was skipped.", e); + return false; + } + } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpProviderTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpProviderTest.java new file mode 100644 index 000000000000..43c9e405fa32 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpProviderTest.java @@ -0,0 +1,111 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.core.crac; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.util.ArrayList; +import java.util.List; +import org.apache.logging.log4j.Level; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.testutils.LogCaptor; + +/** + * Tests the default {@link SdkWarmUpProvider#warmUp()} method, which warms the sync and async clients independently so + * a failure warming one does not stop the other. + */ +class SdkWarmUpProviderTest { + + @Test + void warmUp_warmsSyncThenAsync() { + RecordingProvider provider = new RecordingProvider(); + + provider.warmUp(); + + assertThat(provider.warmedTypes).containsExactly(ClientType.SYNC, ClientType.ASYNC); + } + + @Test + void warmUp_whenSyncFails_stillWarmsAsyncAndDoesNotThrow() { + RecordingProvider provider = new RecordingProvider() { + @Override + public void warmUpClient(ClientType clientType) { + if (clientType == ClientType.SYNC) { + throw new RuntimeException("sync boom"); + } + super.warmUpClient(clientType); + } + }; + + assertThatCode(provider::warmUp).doesNotThrowAnyException(); + assertThat(provider.warmedTypes).containsExactly(ClientType.ASYNC); + } + + @Test + void warmUp_whenSyncFailsToLink_stillWarmsAsync() { + RecordingProvider provider = new RecordingProvider() { + @Override + public void warmUpClient(ClientType clientType) { + if (clientType == ClientType.SYNC) { + throw new NoClassDefFoundError("missing signer"); + } + super.warmUpClient(clientType); + } + }; + + assertThatCode(provider::warmUp).doesNotThrowAnyException(); + assertThat(provider.warmedTypes).containsExactly(ClientType.ASYNC); + } + + @Test + void warmUp_whenClientFails_logsAtWarn() { + RecordingProvider provider = new RecordingProvider() { + @Override + public void warmUpClient(ClientType clientType) { + throw new RuntimeException("boom"); + } + }; + + try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) { + provider.warmUp(); + + assertThat(logCaptor.loggedEvents()) + .anyMatch(event -> event.getLevel() == Level.WARN + && event.getMessage().getFormattedMessage().contains("Warm-up failed for")); + } + } + + private static class RecordingProvider implements SdkWarmUpProvider { + private final List warmedTypes = new ArrayList<>(); + + @Override + public String syncClientClassName() { + return "software.amazon.awssdk.services.example.ExampleClient"; + } + + @Override + public String asyncClientClassName() { + return "software.amazon.awssdk.services.example.ExampleAsyncClient"; + } + + @Override + public void warmUpClient(ClientType clientType) { + warmedTypes.add(clientType); + } + } +}