Skip to content

Commit 135f4b5

Browse files
committed
Minor cleanups + add changelog
1 parent bb0d90d commit 135f4b5

5 files changed

Lines changed: 90 additions & 25 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "Amazon S3",
4+
"contributor": "",
5+
"description": "Added BufferedSplittableAsyncRequestBody.builder() with fullBufferingEnabled option that fully buffers each multipart upload part before sending, fixing NonRetryableException when retrying parts from slow streaming sources."
6+
}

core/sdk-core/src/main/java/software/amazon/awssdk/core/async/BufferedSplittableAsyncRequestBody.java

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,19 @@
3434
* If the first subscriber fails to consume all the data (e.g., due to early cancellation or errors),
3535
* subsequent retry attempts will fail since the complete data set is not available for resubscription.</p>
3636
*
37-
* <p><b>Usage Example:</b></p>
37+
* <p><b>Usage Examples:</b></p>
3838
* {@snippet :
39+
* // Simple usage (default behavior, immediate send):
3940
* AsyncRequestBody originalBody = AsyncRequestBody.fromString("Hello World");
4041
* BufferedSplittableAsyncRequestBody retryableBody =
4142
* BufferedSplittableAsyncRequestBody.create(originalBody);
43+
*
44+
* // With full buffering enabled for slow streaming sources:
45+
* BufferedSplittableAsyncRequestBody fullBufferedBody =
46+
* BufferedSplittableAsyncRequestBody.builder()
47+
* .asyncRequestBody(originalBody)
48+
* .fullBufferingEnabled(true)
49+
* .build();
4250
* }
4351
*
4452
* <p><b>Performance Considerations:</b></p>
@@ -56,44 +64,33 @@ public final class BufferedSplittableAsyncRequestBody implements AsyncRequestBod
5664
private final AsyncRequestBody delegate;
5765
private final boolean fullBufferingEnabled;
5866

59-
private BufferedSplittableAsyncRequestBody(AsyncRequestBody delegate, boolean fullBufferingEnabled) {
60-
this.delegate = delegate;
61-
this.fullBufferingEnabled = fullBufferingEnabled;
67+
private BufferedSplittableAsyncRequestBody(Builder builder) {
68+
this.delegate = Validate.paramNotNull(builder.asyncRequestBody, "asyncRequestBody");
69+
this.fullBufferingEnabled = builder.fullBufferingEnabled;
6270
}
6371

6472
/**
6573
* Creates a new {@link BufferedSplittableAsyncRequestBody} that wraps the provided {@link AsyncRequestBody}.
6674
*
6775
* <p>Full buffering is disabled by default. Each part is sent to the downstream subscriber immediately
68-
* upon initialization in the known-content-length path (existing behavior).
76+
* upon initialization in the known-content-length path.
6977
*
7078
* @param delegate the {@link AsyncRequestBody} to wrap and make retryable. Must not be null.
7179
* @return a new {@link BufferedSplittableAsyncRequestBody} instance
7280
* @throws NullPointerException if delegate is null
7381
*/
7482
public static BufferedSplittableAsyncRequestBody create(AsyncRequestBody delegate) {
7583
Validate.paramNotNull(delegate, "delegate");
76-
return new BufferedSplittableAsyncRequestBody(delegate, false);
84+
return builder().asyncRequestBody(delegate).build();
7785
}
7886

7987
/**
80-
* Creates a new {@link BufferedSplittableAsyncRequestBody} that wraps the provided {@link AsyncRequestBody},
81-
* with an option to enable full buffering before sending parts downstream.
82-
*
83-
* <p>When {@code fullBufferingEnabled} is {@code true}, each part is fully buffered before being sent to the
84-
* downstream subscriber. This guarantees that the retry buffer is always populated before the HTTP layer
85-
* subscribes, making per-part retry deterministically successful for slow streaming sources (e.g., SFTP).
88+
* Returns a new {@link Builder} for creating a {@link BufferedSplittableAsyncRequestBody} with configuration options.
8689
*
87-
* <p>When {@code fullBufferingEnabled} is {@code false}, behavior is identical to {@link #create(AsyncRequestBody)}.
88-
*
89-
* @param delegate the {@link AsyncRequestBody} to wrap and make retryable. Must not be null.
90-
* @param fullBufferingEnabled whether to enable full buffering before sending parts downstream
91-
* @return a new {@link BufferedSplittableAsyncRequestBody} instance
92-
* @throws NullPointerException if delegate is null
90+
* @return a new builder instance
9391
*/
94-
public static BufferedSplittableAsyncRequestBody create(AsyncRequestBody delegate, boolean fullBufferingEnabled) {
95-
Validate.paramNotNull(delegate, "delegate");
96-
return new BufferedSplittableAsyncRequestBody(delegate, fullBufferingEnabled);
92+
public static Builder builder() {
93+
return new Builder();
9794
}
9895

9996
@Override
@@ -136,4 +133,55 @@ public void subscribe(Subscriber<? super ByteBuffer> s) {
136133
public String body() {
137134
return delegate.body();
138135
}
136+
137+
/**
138+
* Builder for {@link BufferedSplittableAsyncRequestBody}.
139+
*/
140+
public static final class Builder {
141+
private AsyncRequestBody asyncRequestBody;
142+
private boolean fullBufferingEnabled = false;
143+
144+
private Builder() {
145+
}
146+
147+
/**
148+
* Sets the {@link AsyncRequestBody} to wrap and make retryable.
149+
*
150+
* @param asyncRequestBody the request body to wrap. Must not be null.
151+
* @return this builder for method chaining
152+
*/
153+
public Builder asyncRequestBody(AsyncRequestBody asyncRequestBody) {
154+
this.asyncRequestBody = asyncRequestBody;
155+
return this;
156+
}
157+
158+
/**
159+
* Configures whether to fully buffer each part before sending it downstream.
160+
*
161+
* <p>When enabled, each part is fully buffered before being sent to the downstream subscriber.
162+
* This guarantees that the retry buffer is always populated before the HTTP layer subscribes,
163+
* making per-part retry deterministically successful for slow streaming sources (e.g., SFTP).
164+
*
165+
* <p>When disabled (the default), each part is sent immediately upon initialization in the
166+
* known-content-length path, allowing the HTTP connection to open while data is still arriving.
167+
*
168+
* @param fullBufferingEnabled whether to enable full buffering before sending parts downstream.
169+
* Defaults to {@code false}.
170+
* @return this builder for method chaining
171+
*/
172+
public Builder fullBufferingEnabled(boolean fullBufferingEnabled) {
173+
this.fullBufferingEnabled = fullBufferingEnabled;
174+
return this;
175+
}
176+
177+
/**
178+
* Builds a new {@link BufferedSplittableAsyncRequestBody} instance.
179+
*
180+
* @return a new instance configured by this builder
181+
* @throws NullPointerException if asyncRequestBody is null
182+
*/
183+
public BufferedSplittableAsyncRequestBody build() {
184+
return new BufferedSplittableAsyncRequestBody(this);
185+
}
186+
}
139187
}

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/SplittingPublisher.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
*
3737
* <p>If content length is known, each {@link AsyncRequestBody} is sent to the subscriber right after it's initialized.
3838
* Otherwise, it is sent after the entire content for that chunk is buffered. This is required to get content length.
39+
* When {@code fullBufferingEnabled} is set to {@code true}, the known-content-length path also defers sending until the
40+
* part is fully buffered, guaranteeing that the retry buffer is populated before the downstream subscriber receives it.
3941
*/
4042
@SdkInternalApi
4143
public class SplittingPublisher implements SdkPublisher<CloseableAsyncRequestBody> {

core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/async/SplittingPublisherTest.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,11 @@ public void cancel() {
335335
}
336336
};
337337

338-
// Use create(body, true) to enable full buffering
339-
BufferedSplittableAsyncRequestBody bufferedBody = BufferedSplittableAsyncRequestBody.create(sourceBody, true);
338+
// Use builder to enable full buffering
339+
BufferedSplittableAsyncRequestBody bufferedBody = BufferedSplittableAsyncRequestBody.builder()
340+
.asyncRequestBody(sourceBody)
341+
.fullBufferingEnabled(true)
342+
.build();
340343

341344
// Verify that content length is propagated
342345
assertThat(bufferedBody.contentLength()).hasValue((long) data.length);
@@ -457,7 +460,10 @@ void bufferedSplittable_createWithFullBufferingTrue_partsAreRetryable() throws E
457460
}
458461
AsyncRequestBody sourceBody = AsyncRequestBody.fromBytes(data);
459462

460-
BufferedSplittableAsyncRequestBody bufferedBody = BufferedSplittableAsyncRequestBody.create(sourceBody, true);
463+
BufferedSplittableAsyncRequestBody bufferedBody = BufferedSplittableAsyncRequestBody.builder()
464+
.asyncRequestBody(sourceBody)
465+
.fullBufferingEnabled(true)
466+
.build();
461467

462468
AsyncRequestBodySplitConfiguration splitConfig = AsyncRequestBodySplitConfiguration.builder()
463469
.chunkSizeInBytes(5L)

services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/multipart/S3MultipartClientPutObjectWiremockTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,10 @@ public void cancel() {
302302

303303
// Wrap with full buffering enabled — this ensures retry buffer is populated before HTTP subscribe
304304
BufferedSplittableAsyncRequestBody bufferedBody =
305-
BufferedSplittableAsyncRequestBody.create(slowStreamingBody, true);
305+
BufferedSplittableAsyncRequestBody.builder()
306+
.asyncRequestBody(slowStreamingBody)
307+
.fullBufferingEnabled(true)
308+
.build();
306309

307310
// The upload should complete successfully — retry works because full buffering
308311
// ensures the retry buffer is populated before the HTTP layer subscribes

0 commit comments

Comments
 (0)