4141import com .google .api .gax .resumable .QueryStatusRequest ;
4242import com .google .api .gax .resumable .QueryStatusResponse ;
4343import com .google .api .gax .resumable .ResumableUploadStatus ;
44+ import com .google .api .gax .retrying .RetrySettings ;
4445import com .google .api .gax .rpc .ResumableUploadErrorClassifier .Category ;
4546import com .google .common .util .concurrent .MoreExecutors ;
4647import java .io .IOException ;
4748import java .io .InputStream ;
49+ import java .time .Duration ;
4850import java .util .concurrent .CancellationException ;
4951import java .util .concurrent .Executor ;
5052import org .jspecify .annotations .NullMarked ;
@@ -72,7 +74,10 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
7274 queryStatusCallable ;
7375 private final String uploadUrl ;
7476 private final RewindableStreamBuffer buffer ;
77+ private final ResumableUploadCallSettings settings ;
78+ private final long deadlineNanos ;
7579 private final ApiCallContext callContext ;
80+ private final ClientContext clientContext ;
7681 private final SettableApiFuture <ResponseT > result = SettableApiFuture .create ();
7782 private volatile @ Nullable ApiFuture <?> currentChunkFuture ;
7883
@@ -81,16 +86,20 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
8186 UnaryCallable <QueryStatusRequest , QueryStatusResponse <ResponseT >> queryStatusCallable ,
8287 String uploadUrl ,
8388 InputStream payload ,
84- int chunkSize ,
85- ApiCallContext callContext ) {
89+ ResumableUploadCallSettings settings ,
90+ long deadlineNanos ,
91+ ClientContext clientContext ) {
8692 this .uploadChunkCallable =
8793 checkNotNull (uploadChunkCallable , "uploadChunkCallable must not be null" );
8894 this .queryStatusCallable =
8995 checkNotNull (queryStatusCallable , "queryStatusCallable must not be null" );
9096 this .uploadUrl = checkNotNull (uploadUrl , "uploadUrl must not be null" );
9197 checkNotNull (payload , "payload must not be null" );
92- this .callContext = checkNotNull (callContext , "callContext must not be null" );
93- this .buffer = new RewindableStreamBuffer (payload , chunkSize , uploadUrl );
98+ this .settings = checkNotNull (settings , "settings must not be null" );
99+ this .deadlineNanos = deadlineNanos ;
100+ this .clientContext = checkNotNull (clientContext , "clientContext must not be null" );
101+ this .callContext = clientContext .getDefaultCallContext ();
102+ this .buffer = new RewindableStreamBuffer (payload , settings .getChunkSize (), uploadUrl );
94103 }
95104
96105 ApiFuture <ResponseT > start () {
@@ -126,7 +135,7 @@ private void dispatchCurrentChunk() {
126135 ChunkUploadRequest chunkRequest = buildCurrentChunkRequest ();
127136 // Dispatch the chunk upload call and register the in-flight future for cancellation.
128137 ApiFuture <ChunkUploadResponse <ResponseT >> chunkFuture =
129- uploadChunkCallable .futureCall (chunkRequest , callContext );
138+ uploadChunkCallable .futureCall (chunkRequest , chunkCallContext () );
130139 if (!trackInFlight (chunkFuture )) {
131140 return ;
132141 }
@@ -171,7 +180,7 @@ private void recover() {
171180 try {
172181 // Dispatch the query status call and register the in-flight future for cancellation.
173182 ApiFuture <QueryStatusResponse <ResponseT >> queryFuture =
174- queryStatusCallable .futureCall (QueryStatusRequest .create (uploadUrl ), callContext );
183+ queryStatusCallable .futureCall (QueryStatusRequest .create (uploadUrl ), queryCallContext () );
175184 if (!trackInFlight (queryFuture )) {
176185 return ;
177186 }
@@ -272,6 +281,60 @@ public StatusCode.Code getCode() {
272281 }
273282 };
274283
284+ /**
285+ * Returns the call context for a chunk upload, bounding its retry cycle and each of its attempts
286+ * to the chunk's local deadline: at most half the global timeout, never past the global deadline.
287+ */
288+ private ApiCallContext chunkCallContext () {
289+ Duration remaining = remainingGlobalTimeout ();
290+ if (remaining == null ) {
291+ return callContext ;
292+ }
293+ Duration deadline = min (settings .getGlobalTimeout ().dividedBy (2 ), remaining );
294+ return boundedCallContext (
295+ deadline ,
296+ callContext .getRetrySettings ().toBuilder ()
297+ .setInitialRpcTimeoutDuration (deadline )
298+ .setRpcTimeoutMultiplier (1.0 )
299+ .setMaxRpcTimeoutDuration (deadline ));
300+ }
301+
302+ /** Returns the call context for a status query, bounded by the remaining global timeout. */
303+ private ApiCallContext queryCallContext () {
304+ Duration remaining = remainingGlobalTimeout ();
305+ if (remaining == null ) {
306+ return callContext ;
307+ }
308+ RetrySettings baseSettings = callContext .getRetrySettings ();
309+ return boundedCallContext (
310+ remaining ,
311+ baseSettings .toBuilder ()
312+ .setInitialRpcTimeoutDuration (
313+ min (baseSettings .getInitialRpcTimeoutDuration (), remaining ))
314+ .setMaxRpcTimeoutDuration (min (baseSettings .getMaxRpcTimeoutDuration (), remaining )));
315+ }
316+
317+ /**
318+ * Returns the time left before the global upload deadline, or null if deadlines cannot be derived
319+ * because no global timeout or no base retry settings were configured.
320+ */
321+ private @ Nullable Duration remainingGlobalTimeout () {
322+ if (settings .getGlobalTimeout () == null || callContext .getRetrySettings () == null ) {
323+ return null ;
324+ }
325+ return Duration .ofNanos (Math .max (1L , deadlineNanos - clientContext .getClock ().nanoTime ()));
326+ }
327+
328+ private ApiCallContext boundedCallContext (Duration deadline , RetrySettings .Builder settings ) {
329+ return callContext
330+ .withRetrySettings (settings .setTotalTimeoutDuration (deadline ).build ())
331+ .withTimeoutDuration (deadline );
332+ }
333+
334+ private static Duration min (Duration left , Duration right ) {
335+ return left .compareTo (right ) <= 0 ? left : right ;
336+ }
337+
275338 private static FailedPreconditionException protocolViolation (String message ) {
276339 return new FailedPreconditionException (message , null , FAILED_PRECONDITION_STATUS_CODE , false );
277340 }
0 commit comments