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 ;
5053/**
5154 * Coordinates chunk transmission steps of a resumable upload session.
5255 *
56+ * <p>Expects {@code uploadChunkCallable} and {@code queryStatusCallable} to be pre-wrapped in
57+ * retrying callables that handle transient errors.
58+ *
5359 * @param <ResponseT> the type of the final response message returned once the upload completes
5460 */
5561@ InternalApi
@@ -62,6 +68,8 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
6268
6369 private final UnaryCallable <ChunkUploadRequest , ChunkUploadResponse <ResponseT >>
6470 uploadChunkCallable ;
71+ private final UnaryCallable <QueryStatusRequest , QueryStatusResponse <ResponseT >>
72+ queryStatusCallable ;
6573 private final String uploadUrl ;
6674 private final RewindableStreamBuffer buffer ;
6775 private final ApiCallContext callContext ;
@@ -70,12 +78,15 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
7078
7179 ResumableUploadChunkCoordinator (
7280 UnaryCallable <ChunkUploadRequest , ChunkUploadResponse <ResponseT >> uploadChunkCallable ,
81+ UnaryCallable <QueryStatusRequest , QueryStatusResponse <ResponseT >> queryStatusCallable ,
7382 String uploadUrl ,
7483 InputStream payload ,
7584 int chunkSize ,
7685 ApiCallContext callContext ) {
7786 this .uploadChunkCallable =
7887 checkNotNull (uploadChunkCallable , "uploadChunkCallable must not be null" );
88+ this .queryStatusCallable =
89+ checkNotNull (queryStatusCallable , "queryStatusCallable must not be null" );
7990 this .uploadUrl = checkNotNull (uploadUrl , "uploadUrl must not be null" );
8091 checkNotNull (payload , "payload must not be null" );
8192 this .callContext = checkNotNull (callContext , "callContext must not be null" );
@@ -96,60 +107,85 @@ ApiFuture<ResponseT> start() {
96107 }
97108
98109 private void transmitChunk (long currentOffset ) {
99- // Abort if the session was already completed or canceled.
100110 if (result .isDone ()) {
101111 return ;
102112 }
103-
104- // Read the next chunk slice from the payload stream.
105113 try {
106114 buffer .fill (currentOffset );
107- } catch ( IOException e ) {
108- result . setException ( e );
109- return ;
115+ dispatchCurrentChunk ();
116+ } catch ( Throwable t ) {
117+ result . setException ( t ) ;
110118 }
119+ }
111120
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 ();
121+ private void dispatchCurrentChunk () {
122+ if (result .isDone ()) {
123+ return ;
124+ }
125125 try {
126+ ChunkUploadRequest chunkRequest = buildCurrentChunkRequest ();
126127 ApiFuture <ChunkUploadResponse <ResponseT >> chunkFuture =
127128 uploadChunkCallable .futureCall (chunkRequest , callContext );
128129 this .currentChunkFuture = chunkFuture ;
129130 if (result .isCancelled ()) {
130131 chunkFuture .cancel (true );
131132 return ;
132133 }
133-
134134 ApiFutures .addCallback (
135135 chunkFuture ,
136136 new ApiFutureCallback <ChunkUploadResponse <ResponseT >>() {
137137 @ Override
138138 public void onSuccess (ChunkUploadResponse <ResponseT > response ) {
139- if (result .isDone ()) {
139+ if (response .getUploadStatus () == ResumableUploadStatus .UNKNOWN ) {
140+ recover ();
141+ } else {
142+ onChunkUploaded (response );
143+ }
144+ }
145+
146+ @ Override
147+ public void onFailure (Throwable t ) {
148+ if (t instanceof CancellationException || result .isDone ()) {
140149 return ;
141150 }
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 ));
151+ Category category =
152+ ResumableUploadErrorClassifier .classify (t , ResumableUploadCommand .UPLOAD );
153+ if (category == Category .RECOVERABLE ) {
154+ recover ();
151155 } else {
152- chunkExecutor .execute (() -> transmitChunk (nextOffset ));
156+ // Category.TRANSIENT errors reaching here have already exhausted their retry budget
157+ // in the underlying RetryingCallable and become fatal per protocol specification.
158+ result .setException (t );
159+ }
160+ }
161+ },
162+ chunkExecutor );
163+ } catch (Throwable t ) {
164+ result .setException (t );
165+ }
166+ }
167+
168+ private void recover () {
169+ if (result .isDone ()) {
170+ return ;
171+ }
172+ try {
173+ ApiFuture <QueryStatusResponse <ResponseT >> queryFuture =
174+ queryStatusCallable .futureCall (QueryStatusRequest .create (uploadUrl ), callContext );
175+ this .currentChunkFuture = queryFuture ;
176+ if (result .isCancelled ()) {
177+ queryFuture .cancel (true );
178+ return ;
179+ }
180+ ApiFutures .addCallback (
181+ queryFuture ,
182+ new ApiFutureCallback <QueryStatusResponse <ResponseT >>() {
183+ @ Override
184+ public void onSuccess (QueryStatusResponse <ResponseT > queryResponse ) {
185+ try {
186+ handleQueryResponse (queryResponse );
187+ } catch (Throwable t ) {
188+ result .setException (t );
153189 }
154190 }
155191
@@ -161,10 +197,78 @@ public void onFailure(Throwable t) {
161197 result .setException (t );
162198 }
163199 },
164- MoreExecutors . directExecutor () );
200+ chunkExecutor );
165201 } catch (Throwable t ) {
166202 result .setException (t );
167203 }
168204 }
169- }
170205
206+ private void handleQueryResponse (QueryStatusResponse <ResponseT > queryResponse ) throws IOException {
207+ if (result .isDone ()) {
208+ return ;
209+ }
210+ if (queryResponse .getUploadStatus () == ResumableUploadStatus .UNKNOWN ) {
211+ throw protocolViolation (
212+ "Query status response missing X-Goog-Upload-Status header for upload URL: " + uploadUrl );
213+ }
214+ if (queryResponse .getUploadStatus () == ResumableUploadStatus .FINAL ) {
215+ onChunkUploaded (
216+ ChunkUploadResponse .create (ResumableUploadStatus .FINAL , queryResponse .getResponse ()));
217+ return ;
218+ }
219+ Long committedOffset = queryResponse .getCommittedOffset ();
220+ if (committedOffset == null ) {
221+ throw protocolViolation (
222+ "Incomplete query status response did not include a committed offset for upload URL: "
223+ + uploadUrl );
224+ }
225+ buffer .realignTo (committedOffset );
226+ dispatchCurrentChunk ();
227+ }
228+
229+ private void onChunkUploaded (ChunkUploadResponse <ResponseT > response ) {
230+ if (result .isDone ()) {
231+ return ;
232+ }
233+ long nextOffset = buffer .getBufferBaseOffset () + buffer .getPayloadLength ();
234+ if (response .getUploadStatus () == ResumableUploadStatus .FINAL ) {
235+ result .set (response .getResponse ());
236+ } else if (buffer .isFinal ()) {
237+ result .setException (
238+ new IllegalStateException (
239+ "Upload stream ended and final chunk was transmitted, but server returned"
240+ + " incomplete status for upload URL: "
241+ + uploadUrl ));
242+ } else {
243+ chunkExecutor .execute (() -> transmitChunk (nextOffset ));
244+ }
245+ }
246+
247+ private ChunkUploadRequest buildCurrentChunkRequest () {
248+ // Determine if this is the final chunk and build the chunk request.
249+ return ChunkUploadRequest .newBuilder ()
250+ .setUploadUrl (uploadUrl )
251+ .setPayload (buffer .getBuffer ())
252+ .setPayloadLength (buffer .getPayloadLength ())
253+ .setOffset (buffer .getBufferBaseOffset ())
254+ .setFinal (buffer .isFinal ())
255+ .build ();
256+ }
257+
258+ private static final StatusCode FAILED_PRECONDITION_STATUS_CODE =
259+ new StatusCode () {
260+ @ Override
261+ public StatusCode .Code getCode () {
262+ return StatusCode .Code .FAILED_PRECONDITION ;
263+ }
264+
265+ @ Override
266+ public @ Nullable Object getTransportCode () {
267+ return null ;
268+ }
269+ };
270+
271+ private static FailedPreconditionException protocolViolation (String message ) {
272+ return new FailedPreconditionException (message , null , FAILED_PRECONDITION_STATUS_CODE , false );
273+ }
274+ }
0 commit comments