Skip to content

feat(gax): add resumable upload error classification and retry algorithm - #14419

Open
whowes wants to merge 1 commit into
mainfrom
whowes/resumable-upload-error-classification
Open

whowes wants to merge 1 commit into
mainfrom
whowes/resumable-upload-error-classification

Conversation

@whowes

@whowes whowes commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

The intent is to centralize the logic that determines the appropriate action (fail fast, retry, chunk recovery) for the various types/codes of errors that a resumable upload operation might encounter. Since the requirements for which error should trigger which action during which operation are quite complex it's helpful for the logic to be consolidated in one place.

gemini-code-assist[bot]

This comment was marked as outdated.

@whowes
whowes force-pushed the whowes/resumable-upload-error-classification branch from ebf651d to 58cb833 Compare September 17, 2026 22:08
@whowes
whowes added this pull request to stack #14429 September 17, 2026 22:16
@whowes
whowes force-pushed the whowes/resumable-upload-error-classification branch 5 times, most recently from a6995e8 to a2b7fd6 Compare September 19, 2026 21:11
@whowes
whowes removed this pull request from stack #14429 September 20, 2026 07:20
@whowes
whowes added this pull request to stack #14454 September 20, 2026 07:21
@whowes
whowes force-pushed the whowes/resumable-upload-error-classification branch from a2b7fd6 to 2419e04 Compare September 20, 2026 07:45
@whowes

whowes commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@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 a resumable upload error classification mechanism to GAX, adding the ResumableUploadCommand enum, the ResumableUploadErrorClassifier to categorize exceptions (as transient, recoverable, or fatal), and the ResumableUploadResultRetryAlgorithm to integrate with GAX's retry logic, along with comprehensive unit tests. The review feedback points out a potential risk in ResumableUploadErrorClassifier where querying HTTP_STATUS_MAP with statusCode.getTransportCode() could lead to a NullPointerException or type mismatch, and suggests adding an explicit instanceof Integer check to ensure robustness.

return Category.FATAL;
}

Category category = HTTP_STATUS_MAP.getOrDefault(statusCode.getTransportCode(), Category.FATAL);

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

Since StatusCode.getTransportCode() returns Object, it can potentially return null or a non-Integer type (e.g., in non-HTTP transports or uninitialized states). Querying an ImmutableMap with a null key or an incompatible type is risky and can lead to unexpected behavior or NullPointerException depending on the map implementation. It is safer and more robust to perform an explicit instanceof Integer check before looking up the status code in HTTP_STATUS_MAP.

Suggested change
Category category = HTTP_STATUS_MAP.getOrDefault(statusCode.getTransportCode(), Category.FATAL);
Object transportCode = statusCode.getTransportCode();
Category category = Category.FATAL;
if (transportCode instanceof Integer) {
category = HTTP_STATUS_MAP.getOrDefault((Integer) transportCode, Category.FATAL);
}

@whowes whowes Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Using getOrDefault should handle both null and non-Integer keys safely.

@whowes
whowes force-pushed the whowes/resumable-upload-error-classification branch from 2419e04 to 6f082cf Compare September 21, 2026 18:32
@whowes
whowes marked this pull request as ready for review September 21, 2026 18:33
@whowes
whowes requested review from a team as code owners September 21, 2026 18:33
@whowes
whowes requested a review from blakeli0 September 21, 2026 18:33

private static boolean isRecoverableCommand(ResumableUploadCommand command) {
switch (command) {
case UPLOAD:

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.

We can add an isRecoverable field to the command enum. Same thing for the classifyMissingStatusHeader above if we need it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great suggestion - done.


// HttpJsonApiExceptionFactory wraps non-HTTP errors as UNKNOWN.
if (statusCode.getCode() == StatusCode.Code.UNKNOWN) {
if (apiException.getCause() instanceof IOException) {

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.

Is this for client side exceptions such as unable to open a filestream?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, it's not the plan to have wholly client side failures like reading the InputStream subject to this retry classification (i.e. via ResumableUploadResultRetryAlgorithm.) The requirements doc specifies that those should fail fast.

IOExceptions caught here would be something like network failures, which the requirements dictate should be considered transient.

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.

The requirements doc specifies that those should fail fast.

I agree. Is this already implemented?

IOExceptions caught here would be something like network failures

I'm a little worried that it maybe too broad. For example, an UnknownHostException might be from no internet or misconfigured endpoint. Do we have a list of retryable IOExceptions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is this already implemented?

Yes, stream reading exceptions fail the entire session when caught. The exact shape of this will change over the next few PRs with non-happy path features layered on, but it will continue to fail fast here.

I'm a little worried that it maybe too broad.

That makes sense - looking at the requirements, the exceptions enumerated as retriable other than those associated with specific HTTP codes are just "TCP/Socket Timeout". So I narrowed down to just SocketTimeoutException; if we need to expand the list we can adjust that in the future.

@whowes
whowes force-pushed the whowes/resumable-upload-error-classification branch 2 times, most recently from c36f9e0 to c0d2d5e Compare September 21, 2026 22:59
}
// Transient errors are retried directly with the identical request, others are not.
Category category = ResumableUploadErrorClassifier.classify(previousThrowable, command);
return category == TRANSIENT;

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.

It seems only TRANSIENT is used now for retrying. I guess RECOVERABLE will be used for deciding if we should query and recover later?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct. I've been having a bit of a back-and-forth on whether that will be captured in the retry algorithm in this milestone (maybe a topic for an offline discussion) but in either case the RECOVERABLE value is a marker for kicking off the retry/recovery handshake.

@whowes
whowes force-pushed the whowes/resumable-upload-error-classification branch from c0d2d5e to 3445d8e Compare September 22, 2026 06:46
Introduce UploadCommand, UploadErrorClassifier, and UploadResultRetryAlgorithm
to classify HTTP response codes and transport-level exceptions during resumable
upload sessions into protocol error categories (TRANSIENT, RECOVERABLE, FATAL).

Implements the classification order:
1. CancellationException is terminal (FATAL) and never retried.
2. ApiException with StatusCode.Code.UNKNOWN unwraps cause. GAX wraps
   unrecognized runtime throwables into Code.UNKNOWN, which carries a
   synthetic HTTP 500 transport code. Without this explicit step, local bugs
   and NPEs would be misclassified as transient 500s and retried indefinitely.
   Real wire 500 responses arrive with Code.INTERNAL and are TRANSIENT.
3. Table lookup on raw HTTP transport code (408, 429, 500, 502, 503, 504 are
   TRANSIENT; 400, 409, 412, 416 are RECOVERABLE; 401, 403, 404, 405, 410,
   413, 415 are FATAL). Note that wire 408 and 412 both map to FAILED_PRECONDITION
   under HttpJsonStatusCode, but diverge based on raw HTTP transport code.
4. Plain I/O or timeout exceptions that bypassed ApiException wrapping are
   TRANSIENT; anything else unrecognized is FATAL.
@whowes
whowes force-pushed the whowes/resumable-upload-error-classification branch from 3445d8e to 4225876 Compare September 22, 2026 07:02
@sonarqubecloud

Copy link
Copy Markdown

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

Failed conditions
B Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@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%)
B Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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.

2 participants