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 ;
4856
4957/**
5462@ NullMarked
5563final class ResumableUploadChunkCoordinator <ResponseT > {
5664
65+ static final RetrySettings DEFAULT_CHUNK_RETRY_SETTINGS =
66+ RetrySettings .newBuilder ()
67+ .setInitialRetryDelayDuration (Duration .ofMillis (100 ))
68+ .setRetryDelayMultiplier (1.3 )
69+ .setMaxRetryDelayDuration (Duration .ofMinutes (1 ))
70+ .setInitialRpcTimeoutDuration (Duration .ofSeconds (30 ))
71+ .setRpcTimeoutMultiplier (1.0 )
72+ .setMaxRpcTimeoutDuration (Duration .ofSeconds (30 ))
73+ .setTotalTimeoutDuration (Duration .ofMinutes (5 ))
74+ .setMaxAttempts (5 )
75+ .build ();
76+
5777 private static final byte [] EMPTY_PAYLOAD = new byte [0 ];
5878
59- private final UnaryCallable <ChunkUploadRequest , ChunkUploadResponse <ResponseT >>
60- uploadChunkCallable ;
79+ private final AtomicBoolean dispatching = new AtomicBoolean (false );
80+ private final AtomicLong nextChunkOffset = new AtomicLong (-1L );
81+
82+ private final RetryingCallable <ChunkUploadRequest , ChunkUploadResponse <ResponseT >>
83+ retryingChunkCallable ;
6184 private final String uploadUrl ;
6285 private final InputStream payload ;
6386 private final byte [] buffer ;
@@ -70,22 +93,50 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
7093 String uploadUrl ,
7194 InputStream payload ,
7295 int chunkSize ,
73- ApiCallContext callContext ) {
74- this . uploadChunkCallable =
75- checkNotNull (uploadChunkCallable , "uploadChunkCallable must not be null" );
96+ ApiCallContext callContext ,
97+ ClientContext clientContext ) {
98+ checkNotNull (uploadChunkCallable , "uploadChunkCallable must not be null" );
7699 this .uploadUrl = checkNotNull (uploadUrl , "uploadUrl must not be null" );
77100 this .payload = checkNotNull (payload , "payload must not be null" );
78101 this .chunkSize = chunkSize ;
79102 this .callContext = checkNotNull (callContext , "callContext must not be null" );
103+ checkNotNull (clientContext , "clientContext must not be null" );
80104 this .buffer = new byte [chunkSize ];
105+
106+ RetryAlgorithm <ChunkUploadResponse <ResponseT >> retryAlgorithm =
107+ new RetryAlgorithm <>(
108+ new ResumableUploadResultRetryAlgorithm <>(ResumableUploadCommand .UPLOAD ),
109+ new ExponentialRetryAlgorithm (DEFAULT_CHUNK_RETRY_SETTINGS , clientContext .getClock ()));
110+ ScheduledRetryingExecutor <ChunkUploadResponse <ResponseT >> retryingExecutor =
111+ new ScheduledRetryingExecutor <>(retryAlgorithm , clientContext .getExecutor ());
112+ this .retryingChunkCallable =
113+ new RetryingCallable <>(
114+ clientContext .getDefaultCallContext (), uploadChunkCallable , retryingExecutor );
81115 }
82116
83117 ApiFuture <ResponseT > start () {
84- transmitChunk (0L );
118+ scheduleNextChunk (0L );
85119 return result ;
86120 }
87121
88- private void transmitChunk (long currentOffset ) {
122+ private void scheduleNextChunk (long offset ) {
123+ nextChunkOffset .set (offset );
124+ if (dispatching .compareAndSet (false , true )) {
125+ driveLoop ();
126+ }
127+ }
128+
129+ private void driveLoop () {
130+ do {
131+ long offset = nextChunkOffset .getAndSet (-1L );
132+ if (offset >= 0 ) {
133+ transmitSingleChunk (offset );
134+ }
135+ dispatching .set (false );
136+ } while (nextChunkOffset .get () >= 0 && dispatching .compareAndSet (false , true ));
137+ }
138+
139+ private void transmitSingleChunk (long currentOffset ) {
89140 if (result .isDone ()) {
90141 return ;
91142 }
@@ -118,18 +169,18 @@ private void transmitChunk(long currentOffset) {
118169
119170 long chunkLength = chunkPayload .length ;
120171 try {
121- ApiFuture <ChunkUploadResponse <ResponseT >> chunkFuture =
122- uploadChunkCallable .futureCall (chunkRequest , callContext );
172+ RetryingFuture <ChunkUploadResponse <ResponseT >> retryingFuture =
173+ retryingChunkCallable .futureCall (chunkRequest , callContext );
123174 result .addListener (
124175 () -> {
125176 if (result .isCancelled ()) {
126- chunkFuture .cancel (true );
177+ retryingFuture .cancel (true );
127178 }
128179 },
129180 MoreExecutors .directExecutor ());
130181
131182 ApiFutures .addCallback (
132- chunkFuture ,
183+ retryingFuture ,
133184 new ApiFutureCallback <ChunkUploadResponse <ResponseT >>() {
134185 @ Override
135186 public void onSuccess (ChunkUploadResponse <ResponseT > response ) {
@@ -143,9 +194,10 @@ public void onSuccess(ChunkUploadResponse<ResponseT> response) {
143194 result .setException (
144195 new IllegalStateException (
145196 "Upload stream ended and final chunk was transmitted, but server returned"
146- + " incomplete status" ));
197+ + " incomplete status for upload URL: "
198+ + uploadUrl ));
147199 } else {
148- transmitChunk (nextOffset );
200+ scheduleNextChunk (nextOffset );
149201 }
150202 }
151203
0 commit comments