Skip to content

feat(gax): introduce ResumableUploadStallController - #14987

Draft
whowes wants to merge 1 commit into
whowes/api-exception-status-codefrom
whowes/resumable-stall-controller
Draft

whowes wants to merge 1 commit into
whowes/api-exception-status-codefrom
whowes/resumable-stall-controller

Conversation

@whowes

@whowes whowes commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

WIP, not ready for review

@whowes
whowes added this pull request to stack #14988 November 12, 2026 17:16

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the ResumableUploadStallController class along with its unit tests to track chunk transfer progress against a minimum transfer rate and dynamically compute per-chunk and per-query retry deadlines. The review feedback focuses on improving robustness by adding input and state validation using Preconditions (such as validating non-null parameters, positive rates, and non-negative chunk sizes) and avoiding redundant exception wrapping in translateException when dealing with already-translated DeadlineExceededException instances.

Comment on lines +70 to +81
ResumableUploadStallController(
ApiClock clock,
double minimumRateMiBPerSec,
Duration stallTimeout,
@Nullable Duration globalDeadline,
RetrySettings baseRetrySettings) {
this.clock = clock;
this.minimumRateBytesPerSec = minimumRateMiBPerSec * BYTES_PER_MIB;
this.stallTimeout = stallTimeout;
this.globalDeadline = globalDeadline;
this.baseRetrySettings = baseRetrySettings;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Validate that minimumRateMiBPerSec and stallTimeout are strictly positive, and that non-nullable parameters are not null. This prevents division by zero, negative durations, or NullPointerExceptions later in the execution.

  ResumableUploadStallController(
      ApiClock clock,
      double minimumRateMiBPerSec,
      Duration stallTimeout,
      @Nullable Duration globalDeadline,
      RetrySettings baseRetrySettings) {
    Preconditions.checkNotNull(clock, "clock cannot be null");
    Preconditions.checkNotNull(stallTimeout, "stallTimeout cannot be null");
    Preconditions.checkNotNull(baseRetrySettings, "baseRetrySettings cannot be null");
    Preconditions.checkArgument(minimumRateMiBPerSec > 0.0, "minimumRateMiBPerSec must be positive");
    Preconditions.checkArgument(!stallTimeout.isNegative() && !stallTimeout.isZero(), "stallTimeout must be positive");
    this.clock = clock;
    this.minimumRateBytesPerSec = minimumRateMiBPerSec * BYTES_PER_MIB;
    this.stallTimeout = stallTimeout;
    this.globalDeadline = globalDeadline;
    this.baseRetrySettings = baseRetrySettings;
  }

* Updates lag and stallStartedAt after chunk bytes are acknowledged; throws if stall timeout
* expired.
*/
void onChunkSuccess(long chunkBytes) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Add state and argument validation to onChunkSuccess:

  1. Ensure onChunkSuccess is not called before onStartChunk has initialized the chunk tracking state (i.e., currentChunkDeadline must not be null).
  2. Ensure chunkBytes is non-negative.
Suggested change
void onChunkSuccess(long chunkBytes) {
void onChunkSuccess(long chunkBytes) {
Preconditions.checkState(currentChunkDeadline != null, "onChunkSuccess called before onStartChunk");
Preconditions.checkArgument(chunkBytes >= 0L, "chunkBytes must be non-negative");

import com.google.api.gax.retrying.RetrySettings;
import com.google.api.gax.rpc.ResumableUploadErrorClassifier.Category;
import com.google.api.gax.rpc.StatusCode.Code;
import com.google.common.base.MoreObjects;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import Preconditions to enable argument and state validation in the constructor and other methods.

Suggested change
import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;

}

/** Computes per-chunk RetrySettings (local deadline + 2x expected per-attempt timeout). */
RetrySettings onStartChunk(long chunkBytes) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a precondition check to ensure chunkBytes is non-negative. A negative value would indicate a bug in the caller and would corrupt the expected duration and lag calculations.

Suggested change
RetrySettings onStartChunk(long chunkBytes) {
RetrySettings onStartChunk(long chunkBytes) {
Preconditions.checkArgument(chunkBytes >= 0L, "chunkBytes must be non-negative");

* Computes RetrySettings for queryStatus, enforcing that mid-upload recovery shares the active
* chunk's remaining local deadline (or uses stallTimeout on an initial resume()).
*/
RetrySettings onStartQuery(Duration recoveryBackoffDelay) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Validate that recoveryBackoffDelay is non-null and non-negative to prevent invalid calculations.

Suggested change
RetrySettings onStartQuery(Duration recoveryBackoffDelay) {
RetrySettings onStartQuery(Duration recoveryBackoffDelay) {
Preconditions.checkNotNull(recoveryBackoffDelay, "recoveryBackoffDelay cannot be null");
Preconditions.checkArgument(!recoveryBackoffDelay.isNegative(), "recoveryBackoffDelay cannot be negative");

Comment on lines +191 to +197
Throwable translateException(Throwable t, ResumableUploadCommand command) {
Category category = ResumableUploadErrorClassifier.classify(t, command);
if (category != Category.TRANSIENT && !(t instanceof DeadlineExceededException)) {
return t;
}
return createStallOrGlobalTimeoutException(t);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If t is already a DeadlineExceededException thrown by this controller, calling translateException will wrap it again. Check if the exception is already a DeadlineExceededException created by this controller and return it directly to avoid redundant nesting.

Suggested change
Throwable translateException(Throwable t, ResumableUploadCommand command) {
Category category = ResumableUploadErrorClassifier.classify(t, command);
if (category != Category.TRANSIENT && !(t instanceof DeadlineExceededException)) {
return t;
}
return createStallOrGlobalTimeoutException(t);
}
Throwable translateException(Throwable t, ResumableUploadCommand command) {
if (t instanceof DeadlineExceededException && t.getMessage() != null
&& t.getMessage().startsWith("Resumable upload")) {
return t;
}
Category category = ResumableUploadErrorClassifier.classify(t, command);
if (category != Category.TRANSIENT && !(t instanceof DeadlineExceededException)) {
return t;
}
return createStallOrGlobalTimeoutException(t);
}

@sonarqubecloud

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed for 'gapic-generator-java-root'

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@whowes whowes added the release-please:force-run To run release-please label Nov 12, 2026
@release-please release-please Bot removed the release-please:force-run To run release-please label Nov 12, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant