Skip to content

Commit bb0d90d

Browse files
committed
Add fullBufferingEnabled opt-in option to BufferedSplittableAsyncRequestBody
1 parent 3aea7f1 commit bb0d90d

4 files changed

Lines changed: 629 additions & 4 deletions

File tree

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

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,46 @@
5454
@SdkPublicApi
5555
public final class BufferedSplittableAsyncRequestBody implements AsyncRequestBody {
5656
private final AsyncRequestBody delegate;
57+
private final boolean fullBufferingEnabled;
5758

58-
private BufferedSplittableAsyncRequestBody(AsyncRequestBody delegate) {
59+
private BufferedSplittableAsyncRequestBody(AsyncRequestBody delegate, boolean fullBufferingEnabled) {
5960
this.delegate = delegate;
61+
this.fullBufferingEnabled = fullBufferingEnabled;
6062
}
6163

6264
/**
6365
* Creates a new {@link BufferedSplittableAsyncRequestBody} that wraps the provided {@link AsyncRequestBody}.
6466
*
67+
* <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).
69+
*
6570
* @param delegate the {@link AsyncRequestBody} to wrap and make retryable. Must not be null.
6671
* @return a new {@link BufferedSplittableAsyncRequestBody} instance
6772
* @throws NullPointerException if delegate is null
6873
*/
6974
public static BufferedSplittableAsyncRequestBody create(AsyncRequestBody delegate) {
7075
Validate.paramNotNull(delegate, "delegate");
71-
return new BufferedSplittableAsyncRequestBody(delegate);
76+
return new BufferedSplittableAsyncRequestBody(delegate, false);
77+
}
78+
79+
/**
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).
86+
*
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
93+
*/
94+
public static BufferedSplittableAsyncRequestBody create(AsyncRequestBody delegate, boolean fullBufferingEnabled) {
95+
Validate.paramNotNull(delegate, "delegate");
96+
return new BufferedSplittableAsyncRequestBody(delegate, fullBufferingEnabled);
7297
}
7398

7499
@Override
@@ -98,6 +123,7 @@ public SdkPublisher<CloseableAsyncRequestBody> splitCloseable(AsyncRequestBodySp
98123
.asyncRequestBody(this)
99124
.splitConfiguration(splitConfiguration)
100125
.retryableSubAsyncRequestBodyEnabled(true)
126+
.fullBufferingEnabled(fullBufferingEnabled)
101127
.build();
102128
}
103129

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public class SplittingPublisher implements SdkPublisher<CloseableAsyncRequestBod
4646
private final long chunkSizeInBytes;
4747
private final long bufferSizeInBytes;
4848
private final boolean retryableSubAsyncRequestBodyEnabled;
49+
private final boolean fullBufferingEnabled;
4950
private final AtomicBoolean currentBodySent = new AtomicBoolean(false);
5051
private final String sourceBodyName;
5152

@@ -64,6 +65,7 @@ private SplittingPublisher(Builder builder) {
6465

6566
this.retryableSubAsyncRequestBodyEnabled = Validate.paramNotNull(builder.retryableSubAsyncRequestBodyEnabled,
6667
"retryableSubAsyncRequestBodyEnabled");
68+
this.fullBufferingEnabled = builder.fullBufferingEnabled;
6769
this.sourceBodyName = builder.asyncRequestBody.body();
6870
if (!upstreamPublisher.contentLength().isPresent()) {
6971
Validate.isTrue(bufferSizeInBytes >= chunkSizeInBytes,
@@ -136,7 +138,7 @@ private SubAsyncRequestBody initializeNextDownstreamBody(boolean contentLengthKn
136138
}
137139

138140
currentBodySent.set(false);
139-
if (contentLengthKnown) {
141+
if (contentLengthKnown && !fullBufferingEnabled) {
140142
sendCurrentBody(body);
141143
}
142144
return body;
@@ -234,7 +236,7 @@ private void completeCurrentBody() {
234236

235237
// Current body could be completed in either onNext or onComplete, so we need to guard against sending the last body
236238
// twice.
237-
if (upstreamSize == null && currentBodySent.compareAndSet(false, true)) {
239+
if ((upstreamSize == null || fullBufferingEnabled) && currentBodySent.compareAndSet(false, true)) {
238240
sendCurrentBody(currentBody);
239241
}
240242
}
@@ -307,6 +309,7 @@ public static final class Builder {
307309
private AsyncRequestBody asyncRequestBody;
308310
private AsyncRequestBodySplitConfiguration splitConfiguration;
309311
private Boolean retryableSubAsyncRequestBodyEnabled;
312+
private boolean fullBufferingEnabled = false;
310313

311314
private Builder() {
312315
}
@@ -335,6 +338,16 @@ public Builder retryableSubAsyncRequestBodyEnabled(Boolean retryableSubAsyncRequ
335338
return this;
336339
}
337340

341+
/**
342+
* Sets whether to enable full buffering before sending parts downstream.
343+
* When enabled, parts are only sent to the downstream subscriber after
344+
* all data for that part has been received and complete() has been called.
345+
*/
346+
public Builder fullBufferingEnabled(boolean fullBufferingEnabled) {
347+
this.fullBufferingEnabled = fullBufferingEnabled;
348+
return this;
349+
}
350+
338351
/**
339352
* Builds a {@link SplittingPublisher} object based on the values held by this builder.
340353
*/

0 commit comments

Comments
 (0)