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 ;
@@ -62,6 +65,8 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
6265
6366 private final UnaryCallable <ChunkUploadRequest , ChunkUploadResponse <ResponseT >>
6467 uploadChunkCallable ;
68+ private final UnaryCallable <QueryStatusRequest , QueryStatusResponse <ResponseT >>
69+ queryStatusCallable ;
6570 private final String uploadUrl ;
6671 private final RewindableStreamBuffer buffer ;
6772 private final ApiCallContext callContext ;
@@ -70,12 +75,15 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
7075
7176 ResumableUploadChunkCoordinator (
7277 UnaryCallable <ChunkUploadRequest , ChunkUploadResponse <ResponseT >> uploadChunkCallable ,
78+ UnaryCallable <QueryStatusRequest , QueryStatusResponse <ResponseT >> queryStatusCallable ,
7379 String uploadUrl ,
7480 InputStream payload ,
7581 int chunkSize ,
7682 ApiCallContext callContext ) {
7783 this .uploadChunkCallable =
7884 checkNotNull (uploadChunkCallable , "uploadChunkCallable must not be null" );
85+ this .queryStatusCallable =
86+ checkNotNull (queryStatusCallable , "queryStatusCallable must not be null" );
7987 this .uploadUrl = checkNotNull (uploadUrl , "uploadUrl must not be null" );
8088 checkNotNull (payload , "payload must not be null" );
8189 this .callContext = checkNotNull (callContext , "callContext must not be null" );
@@ -100,37 +108,25 @@ private void transmitChunk(long currentOffset) {
100108 if (result .isDone ()) {
101109 return ;
102110 }
103-
104111 // Read the next chunk slice from the payload stream.
105112 try {
106113 buffer .fill (currentOffset );
107- } catch ( IOException e ) {
108- result . setException ( e );
109- return ;
114+ dispatchCurrentChunk ();
115+ } catch ( Throwable t ) {
116+ result . setException ( t ) ;
110117 }
118+ }
111119
112- // Determine if this is the final chunk and build the chunk request.
113- ChunkUploadRequest chunkRequest =
114- ChunkUploadRequest .newBuilder ()
115- .setUploadUrl (uploadUrl )
116- .setPayload (buffer .getBuffer ())
117- .setPayloadLength (buffer .getPayloadLength ())
118- .setOffset (buffer .getBufferBaseOffset ())
119- .setFinal (buffer .isFinal ())
120- .build ();
121-
122- // Dispatch the chunk upload call and register the in-flight future for cancellation.
123- long chunkLength = chunkRequest .getPayloadLength ();
124- boolean isFinal = chunkRequest .isFinal ();
120+ private void dispatchCurrentChunk () {
125121 try {
122+ ChunkUploadRequest chunkRequest = buildCurrentChunkRequest ();
123+
124+ // Dispatch the chunk upload call and register the in-flight future for cancellation.
126125 ApiFuture <ChunkUploadResponse <ResponseT >> chunkFuture =
127126 uploadChunkCallable .futureCall (chunkRequest , callContext );
128- this .currentChunkFuture = chunkFuture ;
129- if (result .isCancelled ()) {
130- chunkFuture .cancel (true );
127+ if (!tryRegisterInFlight (chunkFuture )) {
131128 return ;
132129 }
133-
134130 ApiFutures .addCallback (
135131 chunkFuture ,
136132 new ApiFutureCallback <ChunkUploadResponse <ResponseT >>() {
@@ -139,17 +135,55 @@ public void onSuccess(ChunkUploadResponse<ResponseT> response) {
139135 if (result .isDone ()) {
140136 return ;
141137 }
142- long nextOffset = currentOffset + chunkLength ;
143- if (response .getUploadStatus () == ResumableUploadStatus .FINAL ) {
144- result .set (response .getResponse ());
145- } else if (isFinal ) {
146- result .setException (
147- new IllegalStateException (
148- "Upload stream ended and final chunk was transmitted, but server returned"
149- + " incomplete status for upload URL: "
150- + uploadUrl ));
138+ if (response .getUploadStatus () == ResumableUploadStatus .UNKNOWN ) {
139+ recover ();
151140 } else {
152- chunkExecutor .execute (() -> transmitChunk (nextOffset ));
141+ onChunkUploaded (response );
142+ }
143+ }
144+
145+ @ Override
146+ public void onFailure (Throwable t ) {
147+ if (t instanceof CancellationException || result .isDone ()) {
148+ return ;
149+ }
150+ Category category =
151+ ResumableUploadErrorClassifier .classify (t , ResumableUploadCommand .UPLOAD );
152+ if (category == Category .RECOVERABLE ) {
153+ recover ();
154+ } else {
155+ // Category.TRANSIENT errors reaching here have already exhausted their retry budget
156+ // in the underlying RetryingCallable and become fatal per protocol specification.
157+ result .setException (t );
158+ }
159+ }
160+ },
161+ chunkExecutor );
162+ } catch (Throwable t ) {
163+ result .setException (t );
164+ }
165+ }
166+
167+ private void recover () {
168+ try {
169+ // Dispatch the query status call and register the in-flight future for cancellation.
170+ ApiFuture <QueryStatusResponse <ResponseT >> queryFuture =
171+ queryStatusCallable .futureCall (QueryStatusRequest .create (uploadUrl ), callContext );
172+ if (!tryRegisterInFlight (queryFuture )) {
173+ return ;
174+ }
175+ ApiFutures .addCallback (
176+ queryFuture ,
177+ new ApiFutureCallback <QueryStatusResponse <ResponseT >>() {
178+ @ Override
179+ public void onSuccess (QueryStatusResponse <ResponseT > queryResponse ) {
180+ if (result .isDone ()) {
181+ return ;
182+ }
183+ try {
184+ handleQueryResponse (queryResponse );
185+ } catch (Throwable t ) {
186+ result .setException (t );
153187 }
154188 }
155189
@@ -161,10 +195,83 @@ public void onFailure(Throwable t) {
161195 result .setException (t );
162196 }
163197 },
164- MoreExecutors . directExecutor () );
198+ chunkExecutor );
165199 } catch (Throwable t ) {
166200 result .setException (t );
167201 }
168202 }
169- }
170203
204+ /** Registers the in-flight future for cancellation, returning false if already cancelled. */
205+ private boolean tryRegisterInFlight (ApiFuture <?> future ) {
206+ this .currentChunkFuture = future ;
207+ if (result .isCancelled ()) {
208+ future .cancel (true );
209+ return false ;
210+ }
211+ return true ;
212+ }
213+
214+ private void handleQueryResponse (QueryStatusResponse <ResponseT > queryResponse )
215+ throws IOException {
216+ if (queryResponse .getUploadStatus () == ResumableUploadStatus .UNKNOWN ) {
217+ throw protocolViolation (
218+ "Query status response missing X-Goog-Upload-Status header for upload URL: " + uploadUrl );
219+ }
220+ if (queryResponse .getUploadStatus () == ResumableUploadStatus .FINAL ) {
221+ onChunkUploaded (
222+ ChunkUploadResponse .create (ResumableUploadStatus .FINAL , queryResponse .getResponse ()));
223+ return ;
224+ }
225+ Long committedOffset = queryResponse .getCommittedOffset ();
226+ if (committedOffset == null ) {
227+ throw protocolViolation (
228+ "Incomplete query status response did not include a committed offset for upload URL: "
229+ + uploadUrl );
230+ }
231+ buffer .realignTo (committedOffset );
232+ dispatchCurrentChunk ();
233+ }
234+
235+ private void onChunkUploaded (ChunkUploadResponse <ResponseT > response ) {
236+ long nextOffset = buffer .getBufferBaseOffset () + buffer .getPayloadLength ();
237+ if (response .getUploadStatus () == ResumableUploadStatus .FINAL ) {
238+ result .set (response .getResponse ());
239+ } else if (buffer .isFinal ()) {
240+ result .setException (
241+ new IllegalStateException (
242+ "Upload stream ended and final chunk was transmitted, but server returned"
243+ + " incomplete status for upload URL: "
244+ + uploadUrl ));
245+ } else {
246+ chunkExecutor .execute (() -> transmitChunk (nextOffset ));
247+ }
248+ }
249+
250+ private ChunkUploadRequest buildCurrentChunkRequest () {
251+ // Determine if this is the final chunk and build the chunk request.
252+ return ChunkUploadRequest .newBuilder ()
253+ .setUploadUrl (uploadUrl )
254+ .setPayload (buffer .getBuffer ())
255+ .setPayloadLength (buffer .getPayloadLength ())
256+ .setOffset (buffer .getBufferBaseOffset ())
257+ .setFinal (buffer .isFinal ())
258+ .build ();
259+ }
260+
261+ private static final StatusCode FAILED_PRECONDITION_STATUS_CODE =
262+ new StatusCode () {
263+ @ Override
264+ public StatusCode .Code getCode () {
265+ return StatusCode .Code .FAILED_PRECONDITION ;
266+ }
267+
268+ @ Override
269+ public @ Nullable Object getTransportCode () {
270+ return null ;
271+ }
272+ };
273+
274+ private static FailedPreconditionException protocolViolation (String message ) {
275+ return new FailedPreconditionException (message , null , FAILED_PRECONDITION_STATUS_CODE , false );
276+ }
277+ }
0 commit comments