3838import com .google .api .gax .resumable .ChunkUploadRequest ;
3939import com .google .api .gax .resumable .ChunkUploadResponse ;
4040import com .google .api .gax .resumable .ResumableUploadStatus ;
41+ import com .google .api .gax .retrying .ExponentialRetryAlgorithm ;
42+ import com .google .api .gax .retrying .RetryAlgorithm ;
43+ import com .google .api .gax .retrying .RetrySettings ;
44+ import com .google .api .gax .retrying .RetryingFuture ;
45+ import com .google .api .gax .retrying .ScheduledRetryingExecutor ;
4146import com .google .common .io .ByteStreams ;
4247import com .google .common .util .concurrent .MoreExecutors ;
4348import java .io .IOException ;
4449import java .io .InputStream ;
50+ import java .time .Duration ;
4551import java .util .Arrays ;
4652import java .util .concurrent .CancellationException ;
53+ import java .util .concurrent .atomic .AtomicBoolean ;
54+ import java .util .concurrent .atomic .AtomicLong ;
4755import org .jspecify .annotations .NullMarked ;
4856import org .jspecify .annotations .Nullable ;
4957
5563@ NullMarked
5664final class ResumableUploadChunkCoordinator <ResponseT > {
5765
66+ static final RetrySettings DEFAULT_CHUNK_RETRY_SETTINGS =
67+ RetrySettings .newBuilder ()
68+ .setInitialRetryDelayDuration (Duration .ofMillis (100 ))
69+ .setRetryDelayMultiplier (1.3 )
70+ .setMaxRetryDelayDuration (Duration .ofMinutes (1 ))
71+ .setInitialRpcTimeoutDuration (Duration .ofSeconds (30 ))
72+ .setRpcTimeoutMultiplier (1.0 )
73+ .setMaxRpcTimeoutDuration (Duration .ofSeconds (30 ))
74+ .setTotalTimeoutDuration (Duration .ofMinutes (5 ))
75+ .setMaxAttempts (5 )
76+ .build ();
77+
5878 private static final byte [] EMPTY_PAYLOAD = new byte [0 ];
5979
60- private final UnaryCallable <ChunkUploadRequest , ChunkUploadResponse <ResponseT >>
61- uploadChunkCallable ;
80+ private final AtomicBoolean dispatching = new AtomicBoolean (false );
81+ private final AtomicLong nextChunkOffset = new AtomicLong (-1L );
82+
83+ private final RetryingCallable <ChunkUploadRequest , ChunkUploadResponse <ResponseT >>
84+ retryingChunkCallable ;
6285 private final String uploadUrl ;
6386 private final InputStream payload ;
6487 private final byte [] buffer ;
@@ -72,14 +95,25 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
7295 String uploadUrl ,
7396 InputStream payload ,
7497 int chunkSize ,
75- ApiCallContext callContext ) {
76- this . uploadChunkCallable =
77- checkNotNull (uploadChunkCallable , "uploadChunkCallable must not be null" );
98+ ApiCallContext callContext ,
99+ ClientContext clientContext ) {
100+ checkNotNull (uploadChunkCallable , "uploadChunkCallable must not be null" );
78101 this .uploadUrl = checkNotNull (uploadUrl , "uploadUrl must not be null" );
79102 this .payload = checkNotNull (payload , "payload must not be null" );
80103 this .chunkSize = chunkSize ;
81104 this .callContext = checkNotNull (callContext , "callContext must not be null" );
105+ checkNotNull (clientContext , "clientContext must not be null" );
82106 this .buffer = new byte [chunkSize ];
107+
108+ RetryAlgorithm <ChunkUploadResponse <ResponseT >> retryAlgorithm =
109+ new RetryAlgorithm <>(
110+ new ResumableUploadResultRetryAlgorithm <>(ResumableUploadCommand .UPLOAD ),
111+ new ExponentialRetryAlgorithm (DEFAULT_CHUNK_RETRY_SETTINGS , clientContext .getClock ()));
112+ ScheduledRetryingExecutor <ChunkUploadResponse <ResponseT >> retryingExecutor =
113+ new ScheduledRetryingExecutor <>(retryAlgorithm , clientContext .getExecutor ());
114+ this .retryingChunkCallable =
115+ new RetryingCallable <>(
116+ clientContext .getDefaultCallContext (), uploadChunkCallable , retryingExecutor );
83117 }
84118
85119 ApiFuture <ResponseT > start () {
@@ -93,11 +127,28 @@ ApiFuture<ResponseT> start() {
93127 }
94128 },
95129 MoreExecutors .directExecutor ());
96- transmitChunk (0L );
130+ scheduleNextChunk (0L );
97131 return result ;
98132 }
99133
100- private void transmitChunk (long currentOffset ) {
134+ private void scheduleNextChunk (long offset ) {
135+ nextChunkOffset .set (offset );
136+ if (dispatching .compareAndSet (false , true )) {
137+ driveLoop ();
138+ }
139+ }
140+
141+ private void driveLoop () {
142+ do {
143+ long offset = nextChunkOffset .getAndSet (-1L );
144+ if (offset >= 0 ) {
145+ transmitSingleChunk (offset );
146+ }
147+ dispatching .set (false );
148+ } while (nextChunkOffset .get () >= 0 && dispatching .compareAndSet (false , true ));
149+ }
150+
151+ private void transmitSingleChunk (long currentOffset ) {
101152 if (result .isDone ()) {
102153 return ;
103154 }
@@ -130,12 +181,12 @@ private void transmitChunk(long currentOffset) {
130181
131182 long chunkLength = chunkPayload .length ;
132183 try {
133- ApiFuture <ChunkUploadResponse <ResponseT >> chunkFuture =
134- uploadChunkCallable .futureCall (chunkRequest , callContext );
135- this .currentChunkFuture = chunkFuture ;
184+ RetryingFuture <ChunkUploadResponse <ResponseT >> retryingFuture =
185+ retryingChunkCallable .futureCall (chunkRequest , callContext );
186+ this .currentChunkFuture = retryingFuture ;
136187
137188 ApiFutures .addCallback (
138- chunkFuture ,
189+ retryingFuture ,
139190 new ApiFutureCallback <ChunkUploadResponse <ResponseT >>() {
140191 @ Override
141192 public void onSuccess (ChunkUploadResponse <ResponseT > response ) {
@@ -149,9 +200,10 @@ public void onSuccess(ChunkUploadResponse<ResponseT> response) {
149200 result .setException (
150201 new IllegalStateException (
151202 "Upload stream ended and final chunk was transmitted, but server returned"
152- + " incomplete status" ));
203+ + " incomplete status for upload URL: "
204+ + uploadUrl ));
153205 } else {
154- transmitChunk (nextOffset );
206+ scheduleNextChunk (nextOffset );
155207 }
156208 }
157209
0 commit comments