3838import com .google .api .core .SettableApiFuture ;
3939import com .google .api .gax .resumable .ChunkUploadRequest ;
4040import com .google .api .gax .resumable .ChunkUploadResponse ;
41+ import com .google .api .gax .resumable .QueryStatusRequest ;
42+ import com .google .api .gax .resumable .QueryStatusResponse ;
4143import com .google .api .gax .resumable .ResumableUploadStatus ;
44+ import com .google .api .gax .rpc .ResumableUploadErrorClassifier .Category ;
4245import com .google .common .util .concurrent .MoreExecutors ;
4346import java .io .IOException ;
4447import java .io .InputStream ;
5659@ NullMarked
5760final class ResumableUploadChunkCoordinator <ResponseT > {
5861
62+ private static final StatusCode FAILED_PRECONDITION_STATUS_CODE =
63+ new StatusCode () {
64+ @ Override
65+ public StatusCode .Code getCode () {
66+ return StatusCode .Code .FAILED_PRECONDITION ;
67+ }
68+
69+ @ Override
70+ public @ Nullable Object getTransportCode () {
71+ return null ;
72+ }
73+ };
74+
5975 private final Executor chunkExecutor =
6076 MoreExecutors .newSequentialExecutor (MoreExecutors .directExecutor ());
6177
6278 private final UnaryCallable <ChunkUploadRequest , ChunkUploadResponse <ResponseT >>
6379 uploadChunkCallable ;
80+ private final UnaryCallable <QueryStatusRequest , QueryStatusResponse <ResponseT >>
81+ queryStatusCallable ;
6482 private final String uploadUrl ;
6583 private final RewindableStreamBuffer buffer ;
6684 private final ApiCallContext callContext ;
6785 private final SettableApiFuture <ResponseT > result = SettableApiFuture .create ();
6886 private volatile @ Nullable ApiFuture <?> currentChunkFuture ;
87+ private int recoveryAttempts ;
6988
7089 ResumableUploadChunkCoordinator (
7190 UnaryCallable <ChunkUploadRequest , ChunkUploadResponse <ResponseT >> uploadChunkCallable ,
91+ UnaryCallable <QueryStatusRequest , QueryStatusResponse <ResponseT >> queryStatusCallable ,
7292 String uploadUrl ,
7393 InputStream payload ,
7494 int chunkSize ,
7595 ClientContext clientContext ) {
7696 this .uploadChunkCallable =
7797 checkNotNull (uploadChunkCallable , "uploadChunkCallable must not be null" );
98+ this .queryStatusCallable =
99+ checkNotNull (queryStatusCallable , "queryStatusCallable must not be null" );
78100 this .uploadUrl = checkNotNull (uploadUrl , "uploadUrl must not be null" );
79101 checkNotNull (payload , "payload must not be null" );
80102 checkNotNull (clientContext , "clientContext must not be null" );
@@ -96,20 +118,26 @@ ApiFuture<ResponseT> start() {
96118 }
97119
98120 private void transmitChunk (long currentOffset ) {
99- // Abort if the session was already completed or canceled.
100121 if (result .isDone ()) {
101122 return ;
102123 }
103124
104- // Read the next chunk slice from the payload stream.
105125 try {
106126 buffer .fill (currentOffset );
107127 } catch (IOException e ) {
108128 result .setException (e );
109129 return ;
110130 }
111131
112- // Determine if this is the final chunk and build the chunk request.
132+ recoveryAttempts = 0 ;
133+ sendCurrentBuffer ();
134+ }
135+
136+ private void sendCurrentBuffer () {
137+ if (result .isDone ()) {
138+ return ;
139+ }
140+
113141 ChunkUploadRequest chunkRequest =
114142 ChunkUploadRequest .newBuilder ()
115143 .setUploadUrl (uploadUrl )
@@ -119,9 +147,6 @@ private void transmitChunk(long currentOffset) {
119147 .setFinal (buffer .isFinal ())
120148 .build ();
121149
122- // Dispatch the chunk upload call and register the in-flight future for cancellation.
123- long chunkLength = chunkRequest .getPayloadLength ();
124- boolean isFinal = chunkRequest .isFinal ();
125150 try {
126151 ApiFuture <ChunkUploadResponse <ResponseT >> chunkFuture =
127152 uploadChunkCallable .futureCall (chunkRequest , callContext );
@@ -139,16 +164,18 @@ public void onSuccess(ChunkUploadResponse<ResponseT> response) {
139164 if (result .isDone ()) {
140165 return ;
141166 }
142- long nextOffset = currentOffset + chunkLength ;
143- if (response .getUploadStatus () == ResumableUploadStatus .FINAL ) {
167+ if (response .getUploadStatus () == ResumableUploadStatus .UNKNOWN ) {
168+ chunkExecutor .execute (() -> recoverChunk (null ));
169+ } else if (response .getUploadStatus () == ResumableUploadStatus .FINAL ) {
144170 result .set (response .getResponse ());
145- } else if (isFinal ) {
171+ } else if (buffer . isFinal () ) {
146172 result .setException (
147173 new IllegalStateException (
148174 "Upload stream ended and final chunk was transmitted, but server returned"
149175 + " incomplete status for upload URL: "
150176 + uploadUrl ));
151177 } else {
178+ long nextOffset = buffer .getBufferBaseOffset () + buffer .getPayloadLength ();
152179 chunkExecutor .execute (() -> transmitChunk (nextOffset ));
153180 }
154181 }
@@ -158,6 +185,11 @@ public void onFailure(Throwable t) {
158185 if (t instanceof CancellationException || result .isDone ()) {
159186 return ;
160187 }
188+ if (ResumableUploadErrorClassifier .classify (t , ResumableUploadCommand .UPLOAD )
189+ == Category .RECOVERABLE ) {
190+ chunkExecutor .execute (() -> recoverChunk (t ));
191+ return ;
192+ }
161193 result .setException (t );
162194 }
163195 },
@@ -166,4 +198,89 @@ public void onFailure(Throwable t) {
166198 result .setException (t );
167199 }
168200 }
201+
202+ private void recoverChunk (@ Nullable Throwable failure ) {
203+ if (result .isDone ()) {
204+ return ;
205+ }
206+ int maxAttempts =
207+ callContext .getRetrySettings () != null
208+ ? callContext .getRetrySettings ().getMaxAttempts ()
209+ : 5 ;
210+ if (++recoveryAttempts >= maxAttempts ) {
211+ result .setException (
212+ failure != null
213+ ? failure
214+ : protocolViolation (
215+ "Chunk upload response missing X-Goog-Upload-Status header for upload URL: "
216+ + uploadUrl ));
217+ return ;
218+ }
219+
220+ try {
221+ ApiFuture <QueryStatusResponse <ResponseT >> queryFuture =
222+ queryStatusCallable .futureCall (QueryStatusRequest .create (uploadUrl ), callContext );
223+ this .currentChunkFuture = queryFuture ;
224+ if (result .isCancelled ()) {
225+ queryFuture .cancel (true );
226+ return ;
227+ }
228+
229+ ApiFutures .addCallback (
230+ queryFuture ,
231+ new ApiFutureCallback <QueryStatusResponse <ResponseT >>() {
232+ @ Override
233+ public void onSuccess (QueryStatusResponse <ResponseT > queryResponse ) {
234+ chunkExecutor .execute (() -> handleQueryResponse (queryResponse ));
235+ }
236+
237+ @ Override
238+ public void onFailure (Throwable t ) {
239+ if (t instanceof CancellationException || result .isDone ()) {
240+ return ;
241+ }
242+ result .setException (t );
243+ }
244+ },
245+ MoreExecutors .directExecutor ());
246+ } catch (Throwable t ) {
247+ result .setException (t );
248+ }
249+ }
250+
251+ private void handleQueryResponse (QueryStatusResponse <ResponseT > queryResponse ) {
252+ if (result .isDone ()) {
253+ return ;
254+ }
255+ if (queryResponse .getUploadStatus () == ResumableUploadStatus .UNKNOWN ) {
256+ result .setException (
257+ protocolViolation (
258+ "Query status response missing X-Goog-Upload-Status header for upload URL: "
259+ + uploadUrl ));
260+ return ;
261+ }
262+ if (queryResponse .getUploadStatus () == ResumableUploadStatus .FINAL ) {
263+ result .set (queryResponse .getResponse ());
264+ return ;
265+ }
266+ Long committedOffset = queryResponse .getCommittedOffset ();
267+ if (committedOffset == null ) {
268+ result .setException (
269+ protocolViolation (
270+ "Incomplete query status response did not include a committed offset for upload URL: "
271+ + uploadUrl ));
272+ return ;
273+ }
274+ try {
275+ buffer .realignTo (committedOffset );
276+ } catch (Throwable t ) {
277+ result .setException (t );
278+ return ;
279+ }
280+ sendCurrentBuffer ();
281+ }
282+
283+ private static FailedPreconditionException protocolViolation (String message ) {
284+ return new FailedPreconditionException (message , null , FAILED_PRECONDITION_STATUS_CODE , false );
285+ }
169286}
0 commit comments