Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -69,6 +71,18 @@ public static Optional<OperationModel> 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<MemberModel> 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,
Expand All @@ -94,7 +108,22 @@ private static Comparator<OperationModel> byVerifiedSimpleFirst(List<String> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 =
Expand All @@ -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");

Expand Down Expand Up @@ -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)));

Expand All @@ -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()" : "";
Expand All @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,21 @@
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;
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
import software.amazon.awssdk.codegen.model.intermediate.MemberModel;
import software.amazon.awssdk.codegen.model.intermediate.Metadata;
import software.amazon.awssdk.codegen.model.intermediate.OperationModel;
import software.amazon.awssdk.codegen.model.intermediate.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 {

Expand Down Expand Up @@ -167,10 +173,72 @@ private static Stream<Scenario> 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<String> memberNames = WarmUpOperationSelector.membersRequiringDummyValue(scenario.operation).stream()
.map(MemberModel::getName)
.collect(Collectors.toList());
assertThat(memberNames).containsExactlyElementsOf(scenario.expectedMemberNames);
}

private static Stream<DummyValueScenario> 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<String> expectedMemberNames;

private DummyValueScenario(String name, OperationModel operation, List<String> 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);
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down
Loading