Skip to content

Commit dd51ae6

Browse files
authored
feat(sdk-core): Add SdkWarmUpProvider SPI and CannedResponseHttpClient for CRaC auto-priming (#7042)
* feat(sdk-core): add SdkWarmUpProvider SPI for CRaC auto-priming * Handled review comments * Add CannedResponseHttpClient for Crac/Auto priming feature
1 parent f9de18c commit dd51ae6

4 files changed

Lines changed: 334 additions & 0 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core.crac;
17+
18+
import software.amazon.awssdk.annotations.SdkProtectedApi;
19+
import software.amazon.awssdk.annotations.ThreadSafe;
20+
21+
/**
22+
* Service Provider Interface for warming up an SDK service's request path before a Coordinated Restore at Checkpoint
23+
* (CRaC) checkpoint. The SDK discovers implementations on the classpath with {@link java.util.ServiceLoader}. An
24+
* implementation registers itself in the
25+
* {@code META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider} resource.
26+
*
27+
* <p>
28+
* Implementations must be thread safe.
29+
*/
30+
@ThreadSafe
31+
@SdkProtectedApi
32+
public interface SdkWarmUpProvider {
33+
34+
/**
35+
* Exercises the service's request path so the Just-In-Time compiled code is captured in a CRaC snapshot.
36+
*/
37+
void warmUp();
38+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core.internal.crac;
17+
18+
import java.io.ByteArrayInputStream;
19+
import software.amazon.awssdk.annotations.Immutable;
20+
import software.amazon.awssdk.annotations.SdkInternalApi;
21+
import software.amazon.awssdk.annotations.ThreadSafe;
22+
import software.amazon.awssdk.http.AbortableInputStream;
23+
import software.amazon.awssdk.http.ExecutableHttpRequest;
24+
import software.amazon.awssdk.http.HttpExecuteRequest;
25+
import software.amazon.awssdk.http.HttpExecuteResponse;
26+
import software.amazon.awssdk.http.SdkHttpClient;
27+
import software.amazon.awssdk.http.SdkHttpResponse;
28+
import software.amazon.awssdk.utils.builder.SdkBuilder;
29+
30+
/**
31+
* An {@link SdkHttpClient} that returns caller-supplied response bytes without performing any network I/O.
32+
*
33+
* <p>
34+
* Each {@link ExecutableHttpRequest#call()} returns the configured status code and a fresh stream over the response bytes.
35+
*/
36+
@SdkInternalApi
37+
@Immutable
38+
@ThreadSafe
39+
public final class CannedResponseHttpClient implements SdkHttpClient {
40+
41+
private static final int DEFAULT_STATUS_CODE = 200;
42+
private static final byte[] EMPTY_BODY = new byte[0];
43+
44+
private final byte[] responseBody;
45+
private final int statusCode;
46+
47+
private CannedResponseHttpClient(DefaultBuilder builder) {
48+
this.responseBody = builder.responseBody == null ? EMPTY_BODY : builder.responseBody.clone();
49+
this.statusCode = builder.statusCode == null ? DEFAULT_STATUS_CODE : builder.statusCode;
50+
}
51+
52+
public static Builder builder() {
53+
return new DefaultBuilder();
54+
}
55+
56+
@Override
57+
public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) {
58+
return new CannedExecutableRequest();
59+
}
60+
61+
@Override
62+
public String clientName() {
63+
return "CannedResponseSync";
64+
}
65+
66+
@Override
67+
public void close() {
68+
}
69+
70+
public interface Builder extends SdkBuilder<Builder, CannedResponseHttpClient> {
71+
72+
/**
73+
* The response body returned by every {@link ExecutableHttpRequest#call()}. Optional; defaults to an empty body.
74+
*/
75+
Builder responseBody(byte[] responseBody);
76+
77+
/**
78+
* The HTTP status code returned by every {@link ExecutableHttpRequest#call()}. Defaults to {@code 200}.
79+
*/
80+
Builder statusCode(int statusCode);
81+
}
82+
83+
private static final class DefaultBuilder implements Builder {
84+
85+
private byte[] responseBody;
86+
private Integer statusCode;
87+
88+
@Override
89+
public Builder responseBody(byte[] responseBody) {
90+
this.responseBody = responseBody;
91+
return this;
92+
}
93+
94+
@Override
95+
public Builder statusCode(int statusCode) {
96+
this.statusCode = statusCode;
97+
return this;
98+
}
99+
100+
@Override
101+
public CannedResponseHttpClient build() {
102+
return new CannedResponseHttpClient(this);
103+
}
104+
}
105+
106+
private final class CannedExecutableRequest implements ExecutableHttpRequest {
107+
108+
@Override
109+
public HttpExecuteResponse call() {
110+
AbortableInputStream body = AbortableInputStream.create(new ByteArrayInputStream(responseBody));
111+
return HttpExecuteResponse.builder()
112+
.response(SdkHttpResponse.builder()
113+
.statusCode(statusCode)
114+
.build())
115+
.responseBody(body)
116+
.build();
117+
}
118+
119+
@Override
120+
public void abort() {
121+
}
122+
}
123+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core.internal.crac;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatCode;
20+
21+
import java.io.IOException;
22+
import java.nio.charset.StandardCharsets;
23+
import org.junit.jupiter.api.Test;
24+
import software.amazon.awssdk.http.ExecutableHttpRequest;
25+
import software.amazon.awssdk.http.HttpExecuteRequest;
26+
import software.amazon.awssdk.http.HttpExecuteResponse;
27+
import software.amazon.awssdk.utils.IoUtils;
28+
import utils.ValidSdkObjects;
29+
30+
class CannedResponseHttpClientTest {
31+
32+
private static final byte[] PAYLOAD = "{\"StringMember\":\"warmup\"}".getBytes(StandardCharsets.UTF_8);
33+
34+
private static HttpExecuteRequest request() {
35+
return HttpExecuteRequest.builder()
36+
.request(ValidSdkObjects.sdkHttpFullRequest().build())
37+
.build();
38+
}
39+
40+
private static byte[] readBody(HttpExecuteResponse response) throws IOException {
41+
return IoUtils.toByteArray(response.responseBody().orElseThrow(() -> new AssertionError("expected a body")));
42+
}
43+
44+
@Test
45+
void call_whenNoStatusGiven_returnsDefault200() throws IOException {
46+
CannedResponseHttpClient client = CannedResponseHttpClient.builder().responseBody(PAYLOAD).build();
47+
48+
HttpExecuteResponse response = client.prepareRequest(request()).call();
49+
50+
assertThat(response.httpResponse().statusCode()).isEqualTo(200);
51+
}
52+
53+
@Test
54+
void call_whenStatusGiven_returnsConfiguredStatus() throws IOException {
55+
CannedResponseHttpClient client = CannedResponseHttpClient.builder().responseBody(PAYLOAD).statusCode(404).build();
56+
57+
HttpExecuteResponse response = client.prepareRequest(request()).call();
58+
59+
assertThat(response.httpResponse().statusCode()).isEqualTo(404);
60+
}
61+
62+
@Test
63+
void call_whenInvoked_returnsFullyDrainableBody() throws IOException {
64+
CannedResponseHttpClient client = CannedResponseHttpClient.builder().responseBody(PAYLOAD).build();
65+
66+
byte[] body = readBody(client.prepareRequest(request()).call());
67+
68+
assertThat(body).isEqualTo(PAYLOAD);
69+
}
70+
71+
@Test
72+
void call_whenInvokedRepeatedly_eachCallGetsFreshReadableStream() throws IOException {
73+
CannedResponseHttpClient client = CannedResponseHttpClient.builder().responseBody(PAYLOAD).build();
74+
75+
byte[] first = readBody(client.prepareRequest(request()).call());
76+
byte[] second = readBody(client.prepareRequest(request()).call());
77+
78+
assertThat(first).isEqualTo(PAYLOAD);
79+
assertThat(second).isEqualTo(PAYLOAD);
80+
}
81+
82+
@Test
83+
void closeAndAbort_areSafeNoOps() {
84+
CannedResponseHttpClient client = CannedResponseHttpClient.builder().responseBody(PAYLOAD).build();
85+
ExecutableHttpRequest executableRequest = client.prepareRequest(request());
86+
87+
assertThatCode(executableRequest::abort).doesNotThrowAnyException();
88+
assertThatCode(client::close).doesNotThrowAnyException();
89+
}
90+
91+
@Test
92+
void call_whenNoBodyGiven_returnsDefault200AndEmptyBody() throws IOException {
93+
CannedResponseHttpClient client = CannedResponseHttpClient.builder().build();
94+
95+
HttpExecuteResponse response = client.prepareRequest(request()).call();
96+
97+
assertThat(response.httpResponse().statusCode()).isEqualTo(200);
98+
assertThat(readBody(response)).isEmpty();
99+
}
100+
101+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.crac;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
21+
import java.net.URI;
22+
import java.nio.charset.StandardCharsets;
23+
import org.junit.jupiter.api.Test;
24+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
25+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
26+
import software.amazon.awssdk.core.internal.crac.CannedResponseHttpClient;
27+
import software.amazon.awssdk.regions.Region;
28+
import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClient;
29+
import software.amazon.awssdk.services.protocolrestjson.model.AllTypesResponse;
30+
import software.amazon.awssdk.services.protocolrestjson.model.ProtocolRestJsonException;
31+
32+
/**
33+
* Tests a generated client configured with a {@link CannedResponseHttpClient}.
34+
*/
35+
class CannedResponseSyntheticCallTest {
36+
37+
private static final byte[] RESPONSE_PAYLOAD = "{\"StringMember\":\"warmup\"}".getBytes(StandardCharsets.UTF_8);
38+
39+
private static ProtocolRestJsonClient clientWith(CannedResponseHttpClient httpClient) {
40+
return ProtocolRestJsonClient.builder()
41+
.credentialsProvider(StaticCredentialsProvider.create(
42+
AwsBasicCredentials.create("akid", "skid")))
43+
.region(Region.US_EAST_1)
44+
.endpointOverride(URI.create("http://localhost"))
45+
.httpClient(httpClient)
46+
.build();
47+
}
48+
49+
@Test
50+
void syntheticCall_throughCannedClient_completesAndUnmarshallsBody() {
51+
ProtocolRestJsonClient client = clientWith(CannedResponseHttpClient.builder()
52+
.responseBody(RESPONSE_PAYLOAD)
53+
.build());
54+
55+
AllTypesResponse response = client.allTypes(r -> {
56+
});
57+
58+
assertThat(response.stringMember()).isEqualTo("warmup");
59+
}
60+
61+
@Test
62+
void syntheticCall_whenCannedStatusIsError_throwsServiceExceptionWithThatStatus() {
63+
ProtocolRestJsonClient client = clientWith(CannedResponseHttpClient.builder()
64+
.statusCode(500)
65+
.build());
66+
67+
assertThatThrownBy(() -> client.allTypes(r -> {
68+
}))
69+
.isInstanceOf(ProtocolRestJsonException.class)
70+
.satisfies(e -> assertThat(((ProtocolRestJsonException) e).statusCode()).isEqualTo(500));
71+
}
72+
}

0 commit comments

Comments
 (0)