Skip to content

Commit 19e6862

Browse files
authored
Expose futureCompletionExecutor on S3 CRT client (#4880)
* Expose futureCompletionExecutor on S3 CRT client * Adderss feedback and fix build
1 parent 0ab7f75 commit 19e6862

File tree

4 files changed

+123
-35
lines changed

4 files changed

+123
-35
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS CRT-based S3 Client",
4+
"contributor": "",
5+
"description": "Allow users to configure future completion executor on the AWS CRT-based S3 client via `S3CrtAsyncClientBuilder#futureCompletionExecutor`. See [#4879](https://github.com/aws/aws-sdk-java-v2/issues/4879)"
6+
}

services/s3/src/main/java/software/amazon/awssdk/services/s3/S3CrtAsyncClientBuilder.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
import java.net.URI;
1919
import java.nio.file.Path;
20+
import java.util.concurrent.CompletableFuture;
21+
import java.util.concurrent.Executor;
22+
import java.util.concurrent.ThreadPoolExecutor;
2023
import java.util.function.Consumer;
2124
import software.amazon.awssdk.annotations.SdkPublicApi;
2225
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
@@ -281,6 +284,30 @@ default S3CrtAsyncClientBuilder retryConfiguration(Consumer<S3CrtRetryConfigurat
281284
*/
282285
S3CrtAsyncClientBuilder thresholdInBytes(Long thresholdInBytes);
283286

287+
/**
288+
* Configure the {@link Executor} that should be used to complete the {@link CompletableFuture} that is returned by the async
289+
* service client. By default, this is a dedicated, per-client {@link ThreadPoolExecutor} that is managed by the SDK.
290+
* <p>
291+
* The configured {@link Executor} will be invoked by the async HTTP client's I/O threads (e.g., EventLoops), which must be
292+
* reserved for non-blocking behavior. Blocking an I/O thread can cause severe performance degradation, including across
293+
* multiple clients, as clients are configured, by default, to share a single I/O thread pool (e.g., EventLoopGroup).
294+
* <p>
295+
* You should typically only want to customize the future-completion {@link Executor} for a few possible reasons:
296+
* <ol>
297+
* <li>You want more fine-grained control over the {@link ThreadPoolExecutor} used, such as configuring the pool size
298+
* or sharing a single pool between multiple clients.
299+
* <li>You want to add instrumentation (i.e., metrics) around how the {@link Executor} is used.
300+
* <li>You know, for certain, that all of your {@link CompletableFuture} usage is strictly non-blocking, and you wish to
301+
* remove the minor overhead incurred by using a separate thread. In this case, you can use
302+
* {@code Runnable::run} to execute the future-completion directly from within the I/O thread.
303+
* </ol>
304+
*
305+
* @param futureCompletionExecutor the executor
306+
* @return an instance of this builder.
307+
*/
308+
S3CrtAsyncClientBuilder futureCompletionExecutor(Executor futureCompletionExecutor);
309+
310+
284311
@Override
285312
S3AsyncClient build();
286313
}

services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package software.amazon.awssdk.services.s3.internal.crt;
1717

1818
import static software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME;
19+
import static software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR;
1920
import static software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute.AUTH_SCHEMES;
2021
import static software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute.SDK_HTTP_EXECUTION_ATTRIBUTES;
2122
import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.HTTP_CHECKSUM;
@@ -30,6 +31,7 @@
3031
import java.util.ArrayList;
3132
import java.util.List;
3233
import java.util.concurrent.CompletableFuture;
34+
import java.util.concurrent.Executor;
3335
import software.amazon.awssdk.annotations.SdkInternalApi;
3436
import software.amazon.awssdk.annotations.SdkTestInternalApi;
3537
import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute;
@@ -56,6 +58,7 @@
5658
import software.amazon.awssdk.regions.Region;
5759
import software.amazon.awssdk.services.s3.DelegatingS3AsyncClient;
5860
import software.amazon.awssdk.services.s3.S3AsyncClient;
61+
import software.amazon.awssdk.services.s3.S3AsyncClientBuilder;
5962
import software.amazon.awssdk.services.s3.S3Configuration;
6063
import software.amazon.awssdk.services.s3.S3CrtAsyncClientBuilder;
6164
import software.amazon.awssdk.services.s3.crt.S3CrtHttpConfiguration;
@@ -120,22 +123,30 @@ private static S3AsyncClient initializeS3AsyncClient(DefaultS3CrtClientBuilder b
120123
builder.executionInterceptors.forEach(overrideConfigurationBuilder::addExecutionInterceptor);
121124
}
122125

123-
return S3AsyncClient.builder()
124-
// Disable checksum for streaming operations, it is handled in CRT. Checksumming for non-streaming
125-
// operations is still handled in HttpChecksumStage
126-
.serviceConfiguration(S3Configuration.builder()
127-
.checksumValidationEnabled(false)
128-
.build())
129-
.region(builder.region)
130-
.endpointOverride(builder.endpointOverride)
131-
.credentialsProvider(builder.credentialsProvider)
132-
.overrideConfiguration(overrideConfigurationBuilder.build())
133-
.accelerate(builder.accelerate)
134-
.forcePathStyle(builder.forcePathStyle)
135-
.crossRegionAccessEnabled(builder.crossRegionAccessEnabled)
136-
.putAuthScheme(new CrtS3ExpressNoOpAuthScheme())
137-
.httpClientBuilder(initializeS3CrtAsyncHttpClient(builder))
138-
.build();
126+
S3AsyncClientBuilder s3AsyncClientBuilder =
127+
S3AsyncClient.builder()
128+
// Disable checksum for streaming operations, it is handled in
129+
// CRT. Checksumming for non-streaming
130+
// operations is still handled in HttpChecksumStage
131+
.serviceConfiguration(S3Configuration.builder()
132+
.checksumValidationEnabled(false)
133+
.build())
134+
.region(builder.region)
135+
.endpointOverride(builder.endpointOverride)
136+
.credentialsProvider(builder.credentialsProvider)
137+
.overrideConfiguration(overrideConfigurationBuilder.build())
138+
.accelerate(builder.accelerate)
139+
.forcePathStyle(builder.forcePathStyle)
140+
.crossRegionAccessEnabled(builder.crossRegionAccessEnabled)
141+
.putAuthScheme(new CrtS3ExpressNoOpAuthScheme())
142+
.httpClientBuilder(initializeS3CrtAsyncHttpClient(builder));
143+
144+
145+
if (builder.futureCompletionExecutor != null) {
146+
s3AsyncClientBuilder.asyncConfiguration(b -> b.advancedOption(FUTURE_COMPLETION_EXECUTOR,
147+
builder.futureCompletionExecutor));
148+
}
149+
return s3AsyncClientBuilder.build();
139150
}
140151

141152
private static S3CrtAsyncHttpClient.Builder initializeS3CrtAsyncHttpClient(DefaultS3CrtClientBuilder builder) {
@@ -186,6 +197,7 @@ public static final class DefaultS3CrtClientBuilder implements S3CrtAsyncClientB
186197
private S3CrtRetryConfiguration retryConfiguration;
187198
private boolean crossRegionAccessEnabled;
188199
private Long thresholdInBytes;
200+
private Executor futureCompletionExecutor;
189201

190202
@Override
191203
public S3CrtAsyncClientBuilder credentialsProvider(
@@ -281,6 +293,12 @@ public S3CrtAsyncClientBuilder thresholdInBytes(Long thresholdInBytes) {
281293
return this;
282294
}
283295

296+
@Override
297+
public S3CrtAsyncClientBuilder futureCompletionExecutor(Executor futureCompletionExecutor) {
298+
this.futureCompletionExecutor = futureCompletionExecutor;
299+
return this;
300+
}
301+
284302
@Override
285303
public S3CrtAsyncClient build() {
286304
return new DefaultS3CrtAsyncClient(this);

services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtClientWiremockTest.java

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,23 @@
1818
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
1919
import static com.github.tomakehurst.wiremock.client.WireMock.any;
2020
import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
21+
import static com.github.tomakehurst.wiremock.client.WireMock.head;
2122
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
2223
import static org.assertj.core.api.Assertions.assertThat;
24+
import static org.mockito.ArgumentMatchers.any;
25+
import static org.mockito.Mockito.verify;
2326

27+
import com.github.tomakehurst.wiremock.client.WireMock;
2428
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
2529
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
2630
import java.net.URI;
31+
import java.util.concurrent.Executor;
2732
import org.junit.jupiter.api.AfterEach;
2833
import org.junit.jupiter.api.BeforeAll;
2934
import org.junit.jupiter.api.BeforeEach;
3035
import org.junit.jupiter.api.Test;
36+
import org.mockito.ArgumentMatchers;
37+
import org.mockito.Mockito;
3138
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
3239
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
3340
import software.amazon.awssdk.crt.CrtResource;
@@ -42,7 +49,21 @@
4249
@WireMockTest
4350
public class S3CrtClientWiremockTest {
4451

52+
private static final String LOCATION = "http://Example-Bucket.s3.amazonaws.com/Example-Object";
53+
private static final String BUCKET = "Example-Bucket";
54+
private static final String KEY = "Example-Object";
55+
private static final String E_TAG = "\"3858f62230ac3c915f300c664312c11f-9\"";
56+
private static final String XML_RESPONSE_BODY = String.format(
57+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
58+
+ "<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\n"
59+
+ "<Location>%s</Location>\n"
60+
+ "<Bucket>%s</Bucket>\n"
61+
+ "<Key>%s</Key>\n"
62+
+ "<ETag>%s</ETag>\n"
63+
+ "</CompleteMultipartUploadResult>", LOCATION, BUCKET, KEY, E_TAG);
4564
private S3AsyncClient s3AsyncClient;
65+
private S3AsyncClient clientWithCustomExecutor;
66+
private SpyableExecutor mockExecutor;
4667

4768
@BeforeAll
4869
public static void setUpBeforeAll() {
@@ -68,27 +89,43 @@ public void tearDown() {
6889

6990
@Test
7091
public void completeMultipartUpload_completeResponse() {
71-
String location = "http://Example-Bucket.s3.amazonaws.com/Example-Object";
72-
String bucket = "Example-Bucket";
73-
String key = "Example-Object";
74-
String eTag = "\"3858f62230ac3c915f300c664312c11f-9\"";
75-
String xmlResponseBody = String.format(
76-
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
77-
+ "<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\n"
78-
+ "<Location>%s</Location>\n"
79-
+ "<Bucket>%s</Bucket>\n"
80-
+ "<Key>%s</Key>\n"
81-
+ "<ETag>%s</ETag>\n"
82-
+ "</CompleteMultipartUploadResult>", location, bucket, key, eTag);
83-
84-
stubFor(any(anyUrl()).willReturn(aResponse().withStatus(200).withBody(xmlResponseBody)));
92+
stubFor(any(anyUrl()).willReturn(aResponse().withStatus(200).withBody(XML_RESPONSE_BODY)));
8593

8694
CompleteMultipartUploadResponse response = s3AsyncClient.completeMultipartUpload(
87-
r -> r.bucket(bucket).key(key).uploadId("upload-id")).join();
95+
r -> r.bucket(BUCKET).key(KEY).uploadId("upload-id")).join();
8896

89-
assertThat(response.location()).isEqualTo(location);
90-
assertThat(response.bucket()).isEqualTo(bucket);
91-
assertThat(response.key()).isEqualTo(key);
92-
assertThat(response.eTag()).isEqualTo(eTag);
97+
assertThat(response.location()).isEqualTo(LOCATION);
98+
assertThat(response.bucket()).isEqualTo(BUCKET);
99+
assertThat(response.key()).isEqualTo(KEY);
100+
assertThat(response.eTag()).isEqualTo(E_TAG);
101+
}
102+
103+
@Test
104+
void overrideResponseCompletionExecutor_shouldCompleteWithCustomExecutor(WireMockRuntimeInfo wiremock) {
105+
106+
mockExecutor = Mockito.spy(new SpyableExecutor());
107+
108+
try (S3AsyncClient s3AsyncClient = S3AsyncClient.crtBuilder()
109+
.region(Region.US_EAST_1)
110+
.endpointOverride(URI.create("http://localhost:" + wiremock.getHttpPort()))
111+
.futureCompletionExecutor(mockExecutor)
112+
.credentialsProvider(
113+
StaticCredentialsProvider.create(AwsBasicCredentials.create("key",
114+
"secret")))
115+
.build()) {
116+
stubFor(any(anyUrl()).willReturn(aResponse().withStatus(200).withBody(XML_RESPONSE_BODY)));
117+
118+
CompleteMultipartUploadResponse response = s3AsyncClient.completeMultipartUpload(
119+
r -> r.bucket(BUCKET).key(KEY).uploadId("upload-id")).join();
120+
121+
verify(mockExecutor).execute(any(Runnable.class));
122+
}
123+
}
124+
125+
private static class SpyableExecutor implements Executor {
126+
@Override
127+
public void execute(Runnable command) {
128+
command.run();
129+
}
93130
}
94131
}

0 commit comments

Comments
 (0)