Skip to content

Commit 3e6e70f

Browse files
authored
Support streaming with unknown content length (#4226)
* Support uploading with unknown content length * Refactoring
1 parent 11c6362 commit 3e6e70f

File tree

13 files changed

+883
-400
lines changed

13 files changed

+883
-400
lines changed

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

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import java.nio.file.Path;
2525
import java.util.Arrays;
2626
import java.util.Optional;
27-
import java.util.concurrent.CompletableFuture;
2827
import java.util.concurrent.ExecutorService;
2928
import org.reactivestreams.Publisher;
3029
import org.reactivestreams.Subscriber;
@@ -420,24 +419,20 @@ static AsyncRequestBody empty() {
420419
* @param maxMemoryUsageInBytes the max memory the SDK will use to buffer the content
421420
* @return SplitAsyncRequestBodyResult
422421
*/
423-
default SplitAsyncRequestBodyResponse split(long chunkSizeInBytes, long maxMemoryUsageInBytes) {
422+
default SdkPublisher<AsyncRequestBody> split(long chunkSizeInBytes, long maxMemoryUsageInBytes) {
424423
Validate.isPositive(chunkSizeInBytes, "chunkSizeInBytes");
425424
Validate.isPositive(maxMemoryUsageInBytes, "maxMemoryUsageInBytes");
426425

427-
if (!this.contentLength().isPresent()) {
426+
if (!contentLength().isPresent()) {
428427
Validate.isTrue(maxMemoryUsageInBytes >= chunkSizeInBytes,
429428
"maxMemoryUsageInBytes must be larger than or equal to " +
430429
"chunkSizeInBytes if the content length is unknown");
431430
}
432431

433-
CompletableFuture<Void> future = new CompletableFuture<>();
434-
SplittingPublisher splittingPublisher = SplittingPublisher.builder()
435-
.asyncRequestBody(this)
436-
.chunkSizeInBytes(chunkSizeInBytes)
437-
.maxMemoryUsageInBytes(maxMemoryUsageInBytes)
438-
.resultFuture(future)
439-
.build();
440-
441-
return SplitAsyncRequestBodyResponse.create(splittingPublisher, future);
432+
return SplittingPublisher.builder()
433+
.asyncRequestBody(this)
434+
.chunkSizeInBytes(chunkSizeInBytes)
435+
.maxMemoryUsageInBytes(maxMemoryUsageInBytes)
436+
.build();
442437
}
443438
}

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

Lines changed: 0 additions & 80 deletions
This file was deleted.

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

Lines changed: 39 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import java.nio.ByteBuffer;
1919
import java.util.Optional;
20-
import java.util.concurrent.CompletableFuture;
2120
import java.util.concurrent.atomic.AtomicBoolean;
2221
import java.util.concurrent.atomic.AtomicInteger;
2322
import java.util.concurrent.atomic.AtomicLong;
@@ -45,24 +44,12 @@ public class SplittingPublisher implements SdkPublisher<AsyncRequestBody> {
4544
private final SimplePublisher<AsyncRequestBody> downstreamPublisher = new SimplePublisher<>();
4645
private final long chunkSizeInBytes;
4746
private final long maxMemoryUsageInBytes;
48-
private final CompletableFuture<Void> future;
4947

5048
private SplittingPublisher(Builder builder) {
5149
this.upstreamPublisher = Validate.paramNotNull(builder.asyncRequestBody, "asyncRequestBody");
5250
this.chunkSizeInBytes = Validate.isPositive(builder.chunkSizeInBytes, "chunkSizeInBytes");
5351
this.splittingSubscriber = new SplittingSubscriber(upstreamPublisher.contentLength().orElse(null));
5452
this.maxMemoryUsageInBytes = Validate.isPositive(builder.maxMemoryUsageInBytes, "maxMemoryUsageInBytes");
55-
this.future = builder.future;
56-
57-
// We need to cancel upstream subscription if the future gets cancelled.
58-
future.whenComplete((r, t) -> {
59-
if (t != null) {
60-
if (splittingSubscriber.upstreamSubscription != null) {
61-
log.trace(() -> "Cancelling subscription because return future completed exceptionally ", t);
62-
splittingSubscriber.upstreamSubscription.cancel();
63-
}
64-
}
65-
});
6653
}
6754

6855
public static Builder builder() {
@@ -117,26 +104,35 @@ public void onNext(ByteBuffer byteBuffer) {
117104
byteBufferSizeHint = byteBuffer.remaining();
118105

119106
while (true) {
107+
108+
if (!byteBuffer.hasRemaining()) {
109+
break;
110+
}
111+
120112
int amountRemainingInChunk = amountRemainingInChunk();
121113

122114
// If we have fulfilled this chunk,
123-
// we should create a new DownstreamBody if needed
115+
// complete the current body
124116
if (amountRemainingInChunk == 0) {
125-
completeCurrentBody();
117+
completeCurrentBodyAndCreateNewIfNeeded(byteBuffer);
118+
amountRemainingInChunk = amountRemainingInChunk();
119+
}
126120

127-
if (shouldCreateNewDownstreamRequestBody(byteBuffer)) {
128-
int currentChunk = chunkNumber.incrementAndGet();
129-
long chunkSize = calculateChunkSize(totalDataRemaining());
130-
currentBody = initializeNextDownstreamBody(upstreamSize != null, chunkSize, currentChunk);
131-
}
121+
// If the current ByteBuffer < this chunk, send it as-is
122+
if (amountRemainingInChunk > byteBuffer.remaining()) {
123+
currentBody.send(byteBuffer.duplicate());
124+
break;
132125
}
133126

134-
amountRemainingInChunk = amountRemainingInChunk();
135-
if (amountRemainingInChunk >= byteBuffer.remaining()) {
127+
// If the current ByteBuffer == this chunk, send it as-is and
128+
// complete the current body
129+
if (amountRemainingInChunk == byteBuffer.remaining()) {
136130
currentBody.send(byteBuffer.duplicate());
131+
completeCurrentBodyAndCreateNewIfNeeded(byteBuffer);
137132
break;
138133
}
139134

135+
// If the current ByteBuffer > this chunk, split this ByteBuffer
140136
ByteBuffer firstHalf = byteBuffer.duplicate();
141137
int newLimit = firstHalf.position() + amountRemainingInChunk;
142138
firstHalf.limit(newLimit);
@@ -147,20 +143,30 @@ public void onNext(ByteBuffer byteBuffer) {
147143
maybeRequestMoreUpstreamData();
148144
}
149145

146+
private void completeCurrentBodyAndCreateNewIfNeeded(ByteBuffer byteBuffer) {
147+
completeCurrentBody();
148+
int currentChunk = chunkNumber.incrementAndGet();
149+
boolean shouldCreateNewDownstreamRequestBody;
150+
Long dataRemaining = totalDataRemaining();
150151

151-
/**
152-
* If content length is known, we should create new DownstreamRequestBody if there's remaining data.
153-
* If content length is unknown, we should create new DownstreamRequestBody if upstream is not completed yet.
154-
*/
155-
private boolean shouldCreateNewDownstreamRequestBody(ByteBuffer byteBuffer) {
156-
return !upstreamComplete || byteBuffer.remaining() > 0;
152+
if (upstreamSize == null) {
153+
shouldCreateNewDownstreamRequestBody = !upstreamComplete || byteBuffer.hasRemaining();
154+
} else {
155+
shouldCreateNewDownstreamRequestBody = dataRemaining != null && dataRemaining > 0;
156+
}
157+
158+
if (shouldCreateNewDownstreamRequestBody) {
159+
long chunkSize = calculateChunkSize(dataRemaining);
160+
currentBody = initializeNextDownstreamBody(upstreamSize != null, chunkSize, currentChunk);
161+
}
157162
}
158163

159164
private int amountRemainingInChunk() {
160165
return Math.toIntExact(currentBody.maxLength - currentBody.transferredLength);
161166
}
162167

163168
private void completeCurrentBody() {
169+
log.debug(() -> "completeCurrentBody for chunk " + chunkNumber.get());
164170
currentBody.complete();
165171
if (upstreamSize == null) {
166172
sendCurrentBody(currentBody);
@@ -172,12 +178,13 @@ public void onComplete() {
172178
upstreamComplete = true;
173179
log.trace(() -> "Received onComplete()");
174180
completeCurrentBody();
175-
downstreamPublisher.complete().thenRun(() -> future.complete(null));
181+
downstreamPublisher.complete();
176182
}
177183

178184
@Override
179185
public void onError(Throwable t) {
180-
currentBody.error(t);
186+
log.trace(() -> "Received onError()", t);
187+
downstreamPublisher.error(t);
181188
}
182189

183190
private void sendCurrentBody(AsyncRequestBody body) {
@@ -206,7 +213,7 @@ private void maybeRequestMoreUpstreamData() {
206213
}
207214

208215
private boolean shouldRequestMoreData(long buffered) {
209-
return buffered == 0 || buffered + byteBufferSizeHint < maxMemoryUsageInBytes;
216+
return buffered == 0 || buffered + byteBufferSizeHint <= maxMemoryUsageInBytes;
210217
}
211218

212219
private Long totalDataRemaining() {
@@ -240,7 +247,7 @@ public Optional<Long> contentLength() {
240247
}
241248

242249
public void send(ByteBuffer data) {
243-
log.trace(() -> "Sending bytebuffer " + data);
250+
log.trace(() -> String.format("Sending bytebuffer %s to chunk %d", data, chunkNumber));
244251
int length = data.remaining();
245252
transferredLength += length;
246253
addDataBuffered(length);
@@ -283,7 +290,6 @@ public static final class Builder {
283290
private AsyncRequestBody asyncRequestBody;
284291
private Long chunkSizeInBytes;
285292
private Long maxMemoryUsageInBytes;
286-
private CompletableFuture<Void> future;
287293

288294
/**
289295
* Configures the asyncRequestBody to split
@@ -322,18 +328,6 @@ public Builder maxMemoryUsageInBytes(long maxMemoryUsageInBytes) {
322328
return this;
323329
}
324330

325-
/**
326-
* Sets the result future. The future will be completed when all request bodies
327-
* have been sent.
328-
*
329-
* @param future The new future value.
330-
* @return This object for method chaining.
331-
*/
332-
public Builder resultFuture(CompletableFuture<Void> future) {
333-
this.future = future;
334-
return this;
335-
}
336-
337331
public SplittingPublisher build() {
338332
return new SplittingPublisher(this);
339333
}

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

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)