3939import com .google .api .gax .resumable .ChunkUploadRequest ;
4040import com .google .api .gax .resumable .ChunkUploadResponse ;
4141import com .google .api .gax .resumable .ResumableUploadSession ;
42+ import com .google .api .gax .retrying .ExponentialRetryAlgorithm ;
43+ import com .google .api .gax .retrying .RetryAlgorithm ;
44+ import com .google .api .gax .retrying .RetrySettings ;
45+ import com .google .api .gax .retrying .RetryingFuture ;
46+ import com .google .api .gax .retrying .ScheduledRetryingExecutor ;
4247import com .google .common .io .ByteStreams ;
4348import com .google .common .util .concurrent .MoreExecutors ;
4449import com .google .errorprone .annotations .concurrent .GuardedBy ;
4550import java .io .IOException ;
4651import java .io .InputStream ;
52+ import java .time .Duration ;
4753import java .util .Arrays ;
4854import java .util .concurrent .CancellationException ;
55+ import java .util .concurrent .atomic .AtomicBoolean ;
56+ import java .util .concurrent .atomic .AtomicLong ;
4957import org .jspecify .annotations .NullMarked ;
5058import org .jspecify .annotations .Nullable ;
5159
5765@ NullMarked
5866final class ResumableUploadChunkCoordinator <ResponseT > {
5967
68+ static final RetrySettings DEFAULT_CHUNK_RETRY_SETTINGS =
69+ RetrySettings .newBuilder ()
70+ .setInitialRetryDelayDuration (Duration .ofMillis (100 ))
71+ .setRetryDelayMultiplier (1.3 )
72+ .setMaxRetryDelayDuration (Duration .ofMinutes (1 ))
73+ .setInitialRpcTimeoutDuration (Duration .ofSeconds (30 ))
74+ .setRpcTimeoutMultiplier (1.0 )
75+ .setMaxRpcTimeoutDuration (Duration .ofSeconds (30 ))
76+ .setTotalTimeoutDuration (Duration .ofMinutes (5 ))
77+ .setMaxAttempts (5 )
78+ .build ();
79+
6080 private static final byte [] EMPTY_PAYLOAD = new byte [0 ];
6181
6282 private final Object lock = new Object ();
83+ private final AtomicBoolean dispatching = new AtomicBoolean (false );
84+ private final AtomicLong nextChunkOffset = new AtomicLong (-1L );
6385
6486 private final SettableApiFuture <ResponseT > result ;
6587 private final ApiFuture <ResumableUploadSession > startFuture ;
66- private final UnaryCallable <ChunkUploadRequest , ChunkUploadResponse <ResponseT >>
67- uploadChunkCallable ;
88+ private final RetryingCallable <ChunkUploadRequest , ChunkUploadResponse <ResponseT >>
89+ retryingChunkCallable ;
6890 private final InputStream payload ;
6991 private final byte [] buffer ;
7092 private final int chunkSize ;
7193 private final ApiCallContext callContext ;
94+ private final ClientContext clientContext ;
95+ private final RetrySettings chunkRetrySettings ;
7296
7397 private volatile @ Nullable String uploadSessionUrl ;
7498
@@ -87,17 +111,30 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
87111 UnaryCallable <ChunkUploadRequest , ChunkUploadResponse <ResponseT >> uploadChunkCallable ,
88112 InputStream payload ,
89113 ResumableUploadCallSettings settings ,
90- ApiCallContext callContext ) {
114+ ApiCallContext callContext ,
115+ ClientContext clientContext ) {
91116 this .result = checkNotNull (result , "result must not be null" );
92117 this .startFuture = checkNotNull (startFuture , "startFuture must not be null" );
93- this .uploadChunkCallable =
94- checkNotNull (uploadChunkCallable , "uploadChunkCallable must not be null" );
118+ checkNotNull (uploadChunkCallable , "uploadChunkCallable must not be null" );
95119 this .payload = checkNotNull (payload , "payload must not be null" );
96120 checkNotNull (settings , "settings must not be null" );
97121 checkArgument (settings .getChunkSize () > 0 , "chunkSize must be > 0" );
98122 this .chunkSize = settings .getChunkSize ();
99123 this .callContext = checkNotNull (callContext , "callContext must not be null" );
124+ this .clientContext = checkNotNull (clientContext , "clientContext must not be null" );
125+ this .chunkRetrySettings = DEFAULT_CHUNK_RETRY_SETTINGS ;
100126 this .buffer = new byte [chunkSize ];
127+
128+ RetryAlgorithm <ChunkUploadResponse <ResponseT >> retryAlgorithm =
129+ new RetryAlgorithm <>(
130+ new UploadResultRetryAlgorithm <>(UploadCommand .UPLOAD ),
131+ new ExponentialRetryAlgorithm (chunkRetrySettings , clientContext .getClock ()));
132+ ScheduledRetryingExecutor <ChunkUploadResponse <ResponseT >> retryingExecutor =
133+ new ScheduledRetryingExecutor <>(retryAlgorithm , clientContext .getExecutor ());
134+ this .retryingChunkCallable =
135+ new RetryingCallable <>(
136+ clientContext .getDefaultCallContext (), uploadChunkCallable , retryingExecutor );
137+
101138 synchronized (lock ) {
102139 this .inFlightFuture = startFuture ;
103140 }
@@ -115,7 +152,7 @@ public void onSuccess(ResumableUploadSession session) {
115152 }
116153 }
117154 uploadSessionUrl = session .getUploadUrl ();
118- transmitChunk (0L );
155+ scheduleNextChunk (0L );
119156 }
120157
121158 @ Override
@@ -197,7 +234,24 @@ private void finish(@Nullable ResponseT response, @Nullable Throwable error) {
197234 }
198235 }
199236
200- private void transmitChunk (long currentOffset ) {
237+ private void scheduleNextChunk (long offset ) {
238+ nextChunkOffset .set (offset );
239+ if (dispatching .compareAndSet (false , true )) {
240+ driveLoop ();
241+ }
242+ }
243+
244+ private void driveLoop () {
245+ do {
246+ long offset = nextChunkOffset .getAndSet (-1L );
247+ if (offset >= 0 ) {
248+ transmitSingleChunk (offset );
249+ }
250+ dispatching .set (false );
251+ } while (nextChunkOffset .get () >= 0 && dispatching .compareAndSet (false , true ));
252+ }
253+
254+ private void transmitSingleChunk (long currentOffset ) {
201255 synchronized (lock ) {
202256 if (done ) {
203257 return ;
@@ -236,47 +290,44 @@ private void transmitChunk(long currentOffset) {
236290 .setFinal (isFinal )
237291 .build ();
238292
239- long chunkLength = chunkPayload .length ;
240- try {
241- ApiFuture <ChunkUploadResponse <ResponseT >> chunkFuture =
242- uploadChunkCallable .futureCall (chunkRequest , callContext );
243- setInFlightFuture (chunkFuture );
293+ RetryingFuture <ChunkUploadResponse <ResponseT >> retryingFuture =
294+ retryingChunkCallable .futureCall (chunkRequest , callContext );
295+ setInFlightFuture (retryingFuture );
244296
245- ApiFutures .addCallback (
246- chunkFuture ,
247- new ApiFutureCallback <ChunkUploadResponse <ResponseT >>() {
248- @ Override
249- public void onSuccess (ChunkUploadResponse <ResponseT > response ) {
250- synchronized (lock ) {
251- if (done ) {
252- return ;
253- }
254- }
255- long nextOffset = currentOffset + chunkLength ;
256- if (response .isComplete ()) {
257- finish (response .getResponse (), null );
258- } else if (isFinal ) {
259- finish (
260- null ,
261- new IllegalStateException (
262- "Upload stream ended and final chunk was transmitted, but server returned"
263- + " incomplete status" ));
264- } else {
265- transmitChunk (nextOffset );
297+ long chunkLength = chunkPayload .length ;
298+ ApiFutures .addCallback (
299+ retryingFuture ,
300+ new ApiFutureCallback <ChunkUploadResponse <ResponseT >>() {
301+ @ Override
302+ public void onSuccess (ChunkUploadResponse <ResponseT > response ) {
303+ synchronized (lock ) {
304+ if (done ) {
305+ return ;
266306 }
267307 }
308+ long nextOffset = currentOffset + chunkLength ;
309+ if (response .isComplete ()) {
310+ finish (response .getResponse (), null );
311+ } else if (isFinal ) {
312+ finish (
313+ null ,
314+ new IllegalStateException (
315+ "Upload stream ended and final chunk was transmitted, but server returned"
316+ + " incomplete status for upload URL: "
317+ + url ));
318+ } else {
319+ scheduleNextChunk (nextOffset );
320+ }
321+ }
268322
269- @ Override
270- public void onFailure (Throwable t ) {
271- if (t instanceof CancellationException ) {
272- return ;
273- }
274- finish (null , t );
323+ @ Override
324+ public void onFailure (Throwable t ) {
325+ if (t instanceof CancellationException ) {
326+ return ;
275327 }
276- },
277- MoreExecutors .directExecutor ());
278- } catch (Throwable t ) {
279- finish (null , t );
280- }
328+ finish (null , t );
329+ }
330+ },
331+ MoreExecutors .directExecutor ());
281332 }
282333}
0 commit comments