33
33
/**
34
34
* Splits an {@link SdkPublisher} to multiple smaller {@link AsyncRequestBody}s, each of which publishes a specific portion of the
35
35
* original data.
36
+ *
37
+ * <p>If content length is known, each {@link AsyncRequestBody} is sent to the subscriber right after it's initialized.
38
+ * Otherwise, it is sent after the entire content for that chunk is buffered. This is required to get content length.
39
+ *
36
40
* // TODO: create a default method in AsyncRequestBody for this
37
- * // TODO: fix the case where content length is null
38
41
*/
39
42
@ SdkInternalApi
40
43
public class SplittingPublisher implements SdkPublisher <AsyncRequestBody > {
@@ -86,6 +89,7 @@ private class SplittingSubscriber implements Subscriber<ByteBuffer> {
86
89
* A hint to determine whether we will exceed maxMemoryUsage by the next OnNext call.
87
90
*/
88
91
private int byteBufferSizeHint ;
92
+ private volatile boolean upstreamComplete ;
89
93
90
94
SplittingSubscriber (Long upstreamSize ) {
91
95
this .upstreamSize = upstreamSize ;
@@ -94,36 +98,49 @@ private class SplittingSubscriber implements Subscriber<ByteBuffer> {
94
98
@ Override
95
99
public void onSubscribe (Subscription s ) {
96
100
this .upstreamSubscription = s ;
97
- this .currentBody = new DownstreamBody (calculateChunkSize (), chunkNumber .get ());
98
- sendCurrentBody ();
101
+ this .currentBody =
102
+ initializeNextDownstreamBody (upstreamSize != null , calculateChunkSize (upstreamSize ),
103
+ chunkNumber .get ());
99
104
// We need to request subscription *after* we set currentBody because onNext could be invoked right away.
100
105
upstreamSubscription .request (1 );
101
106
}
102
107
108
+ private DownstreamBody initializeNextDownstreamBody (boolean contentLengthKnown , long chunkSize , int chunkNumber ) {
109
+ DownstreamBody body = new DownstreamBody (contentLengthKnown , chunkSize , chunkNumber );
110
+ if (contentLengthKnown ) {
111
+ sendCurrentBody (body );
112
+ }
113
+ return body ;
114
+ }
115
+
103
116
@ Override
104
117
public void onNext (ByteBuffer byteBuffer ) {
105
118
hasOpenUpstreamDemand .set (false );
106
119
byteBufferSizeHint = byteBuffer .remaining ();
107
120
108
121
while (true ) {
109
- int amountRemainingInPart = amountRemainingInPart ();
110
- int finalAmountRemainingInPart = amountRemainingInPart ;
111
- if (amountRemainingInPart == 0 ) {
112
- currentBody .complete ();
113
- int currentChunk = chunkNumber .incrementAndGet ();
114
- Long partSize = calculateChunkSize ();
115
- currentBody = new DownstreamBody (partSize , currentChunk );
116
- sendCurrentBody ();
122
+ int amountRemainingInChunk = amountRemainingInChunk ();
123
+
124
+ // If we have fulfilled this chunk,
125
+ // we should create a new DownstreamBody if needed
126
+ if (amountRemainingInChunk == 0 ) {
127
+ completeCurrentBody ();
128
+
129
+ if (shouldCreateNewDownstreamRequestBody (byteBuffer )) {
130
+ int currentChunk = chunkNumber .incrementAndGet ();
131
+ long chunkSize = calculateChunkSize (totalDataRemaining ());
132
+ currentBody = initializeNextDownstreamBody (upstreamSize != null , chunkSize , currentChunk );
133
+ }
117
134
}
118
135
119
- amountRemainingInPart = amountRemainingInPart ();
120
- if (amountRemainingInPart >= byteBuffer .remaining ()) {
136
+ amountRemainingInChunk = amountRemainingInChunk ();
137
+ if (amountRemainingInChunk >= byteBuffer .remaining ()) {
121
138
currentBody .send (byteBuffer .duplicate ());
122
139
break ;
123
140
}
124
141
125
142
ByteBuffer firstHalf = byteBuffer .duplicate ();
126
- int newLimit = firstHalf .position () + amountRemainingInPart ;
143
+ int newLimit = firstHalf .position () + amountRemainingInChunk ;
127
144
firstHalf .limit (newLimit );
128
145
byteBuffer .position (newLimit );
129
146
currentBody .send (firstHalf );
@@ -132,33 +149,50 @@ public void onNext(ByteBuffer byteBuffer) {
132
149
maybeRequestMoreUpstreamData ();
133
150
}
134
151
135
- private int amountRemainingInPart () {
136
- return Math .toIntExact (currentBody .totalLength - currentBody .transferredLength );
152
+
153
+ /**
154
+ * If content length is known, we should create new DownstreamRequestBody if there's remaining data.
155
+ * If content length is unknown, we should create new DownstreamRequestBody if upstream is not completed yet.
156
+ */
157
+ private boolean shouldCreateNewDownstreamRequestBody (ByteBuffer byteBuffer ) {
158
+ return !upstreamComplete || byteBuffer .remaining () > 0 ;
159
+ }
160
+
161
+ private int amountRemainingInChunk () {
162
+ return Math .toIntExact (currentBody .maxLength - currentBody .transferredLength );
163
+ }
164
+
165
+ private void completeCurrentBody () {
166
+ currentBody .complete ();
167
+ if (upstreamSize == null ) {
168
+ sendCurrentBody (currentBody );
169
+ }
137
170
}
138
171
139
172
@ Override
140
173
public void onComplete () {
174
+ upstreamComplete = true ;
141
175
log .trace (() -> "Received onComplete()" );
176
+ completeCurrentBody ();
142
177
downstreamPublisher .complete ().thenRun (() -> future .complete (null ));
143
- currentBody .complete ();
144
178
}
145
179
146
180
@ Override
147
181
public void onError (Throwable t ) {
148
182
currentBody .error (t );
149
183
}
150
184
151
- private void sendCurrentBody () {
152
- downstreamPublisher .send (currentBody ).exceptionally (t -> {
185
+ private void sendCurrentBody (AsyncRequestBody body ) {
186
+ downstreamPublisher .send (body ).exceptionally (t -> {
153
187
downstreamPublisher .error (t );
154
188
return null ;
155
189
});
156
190
}
157
191
158
- private Long calculateChunkSize () {
159
- Long dataRemaining = dataRemaining ();
192
+ private long calculateChunkSize (Long dataRemaining ) {
193
+ // Use default chunk size if the content length is unknown
160
194
if (dataRemaining == null ) {
161
- return null ;
195
+ return chunkSizeInBytes ;
162
196
}
163
197
164
198
return Math .min (chunkSizeInBytes , dataRemaining );
@@ -177,27 +211,34 @@ private boolean shouldRequestMoreData(long buffered) {
177
211
return buffered == 0 || buffered + byteBufferSizeHint < maxMemoryUsageInBytes ;
178
212
}
179
213
180
- private Long dataRemaining () {
214
+ private Long totalDataRemaining () {
181
215
if (upstreamSize == null ) {
182
216
return null ;
183
217
}
184
218
return upstreamSize - (chunkNumber .get () * chunkSizeInBytes );
185
219
}
186
220
187
- private class DownstreamBody implements AsyncRequestBody {
221
+ private final class DownstreamBody implements AsyncRequestBody {
222
+
223
+ /**
224
+ * The maximum length of the content this AsyncRequestBody can hold.
225
+ * If the upstream content length is known, this is the same as totalLength
226
+ */
227
+ private final long maxLength ;
188
228
private final Long totalLength ;
189
229
private final SimplePublisher <ByteBuffer > delegate = new SimplePublisher <>();
190
230
private final int chunkNumber ;
191
231
private volatile long transferredLength = 0 ;
192
232
193
- private DownstreamBody (Long totalLength , int chunkNumber ) {
194
- this .totalLength = totalLength ;
233
+ private DownstreamBody (boolean contentLengthKnown , long maxLength , int chunkNumber ) {
234
+ this .totalLength = contentLengthKnown ? maxLength : null ;
235
+ this .maxLength = maxLength ;
195
236
this .chunkNumber = chunkNumber ;
196
237
}
197
238
198
239
@ Override
199
240
public Optional <Long > contentLength () {
200
- return Optional .ofNullable (totalLength );
241
+ return totalLength != null ? Optional .of (totalLength ) : Optional . of ( transferredLength );
201
242
}
202
243
203
244
public void send (ByteBuffer data ) {
@@ -214,8 +255,12 @@ public void send(ByteBuffer data) {
214
255
}
215
256
216
257
public void complete () {
217
- log .debug (() -> "Received complete() for chunk number: " + chunkNumber );
218
- delegate .complete ();
258
+ log .debug (() -> "Received complete() for chunk number: " + chunkNumber + " length " + transferredLength );
259
+ delegate .complete ().whenComplete ((r , t ) -> {
260
+ if (t != null ) {
261
+ error (t );
262
+ }
263
+ });
219
264
}
220
265
221
266
public void error (Throwable error ) {
0 commit comments