-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(gax): add resumable upload error classification and retry algorithm #14419
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| /* | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Redistribution and use in source and binary forms, with or without | ||
| * modification, are permitted provided that the following conditions are | ||
| * met: | ||
| * | ||
| * * Redistributions of source code must retain the above copyright | ||
| * notice, this list of conditions and the following disclaimer. | ||
| * * Redistributions in binary form must reproduce the above | ||
| * copyright notice, this list of conditions and the following disclaimer | ||
| * in the documentation and/or other materials provided with the | ||
| * distribution. | ||
| * * Neither the name of Google LLC nor the names of its | ||
| * contributors may be used to endorse or promote products derived from | ||
| * this software without specific prior written permission. | ||
| * | ||
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| */ | ||
| package com.google.api.gax.rpc; | ||
|
|
||
| import org.jspecify.annotations.NullMarked; | ||
|
|
||
| /** Enumeration of the supported resumable upload wire commands. */ | ||
| @NullMarked | ||
| enum ResumableUploadCommand { | ||
| START, | ||
| UPLOAD, | ||
| FINALIZE, | ||
| UPLOAD_FINALIZE, | ||
| QUERY | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,137 @@ | ||||||||||||||
| /* | ||||||||||||||
| * Copyright 2026 Google LLC | ||||||||||||||
| * | ||||||||||||||
| * Redistribution and use in source and binary forms, with or without | ||||||||||||||
| * modification, are permitted provided that the following conditions are | ||||||||||||||
| * met: | ||||||||||||||
| * | ||||||||||||||
| * * Redistributions of source code must retain the above copyright | ||||||||||||||
| * notice, this list of conditions and the following disclaimer. | ||||||||||||||
| * * Redistributions in binary form must reproduce the above | ||||||||||||||
| * copyright notice, this list of conditions and the following disclaimer | ||||||||||||||
| * in the documentation and/or other materials provided with the | ||||||||||||||
| * distribution. | ||||||||||||||
| * * Neither the name of Google LLC nor the names of its | ||||||||||||||
| * contributors may be used to endorse or promote products derived from | ||||||||||||||
| * this software without specific prior written permission. | ||||||||||||||
| * | ||||||||||||||
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||||||||||||
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||||||||||||
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||||||||||||
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||||||||||||
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||||||||||||
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||||||||||||
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||||||||||||
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||||||||||||
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||||||||||||
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||||||||||||
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||||||||||||
| */ | ||||||||||||||
| package com.google.api.gax.rpc; | ||||||||||||||
|
|
||||||||||||||
| import com.google.common.collect.ImmutableMap; | ||||||||||||||
| import java.io.IOException; | ||||||||||||||
| import java.util.Objects; | ||||||||||||||
| import org.jspecify.annotations.NullMarked; | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Classifies exceptions encountered during resumable upload commands to determine whether/how to | ||||||||||||||
| * recover. | ||||||||||||||
| */ | ||||||||||||||
| @NullMarked | ||||||||||||||
| final class ResumableUploadErrorClassifier { | ||||||||||||||
|
|
||||||||||||||
| enum Category { | ||||||||||||||
| /** An errored command that can be retried directly. */ | ||||||||||||||
| TRANSIENT, | ||||||||||||||
|
|
||||||||||||||
| /** An errored command that requires status from the server before recovery. */ | ||||||||||||||
| RECOVERABLE, | ||||||||||||||
|
|
||||||||||||||
| /** An errored command that immediately fails the upload operation. */ | ||||||||||||||
| FATAL | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private static final ImmutableMap<Integer, Category> HTTP_STATUS_MAP = | ||||||||||||||
| ImmutableMap.<Integer, Category>builder() | ||||||||||||||
| .put(408, Category.TRANSIENT) | ||||||||||||||
| .put(429, Category.TRANSIENT) | ||||||||||||||
| .put(500, Category.TRANSIENT) | ||||||||||||||
| .put(502, Category.TRANSIENT) | ||||||||||||||
| .put(503, Category.TRANSIENT) | ||||||||||||||
| .put(504, Category.TRANSIENT) | ||||||||||||||
| .put(400, Category.RECOVERABLE) | ||||||||||||||
| .put(409, Category.RECOVERABLE) | ||||||||||||||
| .put(412, Category.RECOVERABLE) | ||||||||||||||
| .put(416, Category.RECOVERABLE) | ||||||||||||||
| .build(); | ||||||||||||||
|
|
||||||||||||||
| private ResumableUploadErrorClassifier() {} | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Classifies an exception for the given upload command according to resumable upload protocol | ||||||||||||||
| * rules. | ||||||||||||||
| * | ||||||||||||||
| * @param t the error to classify | ||||||||||||||
| * @param command the upload command that produced the error | ||||||||||||||
| * @return the classified error category | ||||||||||||||
| */ | ||||||||||||||
| static Category classify(Throwable t, ResumableUploadCommand command) { | ||||||||||||||
| Objects.requireNonNull(t, "t must not be null"); | ||||||||||||||
| Objects.requireNonNull(command, "command must not be null"); | ||||||||||||||
|
|
||||||||||||||
| if (!(t instanceof ApiException)) { | ||||||||||||||
| return Category.FATAL; | ||||||||||||||
| } | ||||||||||||||
| ApiException apiException = (ApiException) t; | ||||||||||||||
| StatusCode statusCode = apiException.getStatusCode(); | ||||||||||||||
|
|
||||||||||||||
| // HttpJsonApiExceptionFactory wraps non-HTTP errors as UNKNOWN. | ||||||||||||||
| if (statusCode.getCode() == StatusCode.Code.UNKNOWN) { | ||||||||||||||
| if (apiException.getCause() instanceof IOException) { | ||||||||||||||
| return Category.TRANSIENT; | ||||||||||||||
| } | ||||||||||||||
| return Category.FATAL; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| Category category = HTTP_STATUS_MAP.getOrDefault(statusCode.getTransportCode(), Category.FATAL); | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using getOrDefault should handle both null and non-Integer keys safely. |
||||||||||||||
| if (category == Category.RECOVERABLE && !isRecoverableCommand(command)) { | ||||||||||||||
| return Category.FATAL; | ||||||||||||||
| } | ||||||||||||||
| return category; | ||||||||||||||
|
Check warning on line 101 in sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadErrorClassifier.java
|
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Classifies a missing upload status response header based on the wire command. | ||||||||||||||
| * | ||||||||||||||
| * @param command the upload command that received a response lacking the status header | ||||||||||||||
| * @return the classified error category | ||||||||||||||
| */ | ||||||||||||||
| static Category classifyMissingStatusHeader(ResumableUploadCommand command) { | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this method is only used in tests? |
||||||||||||||
| Objects.requireNonNull(command, "command must not be null"); | ||||||||||||||
| switch (command) { | ||||||||||||||
| case START: | ||||||||||||||
| return Category.TRANSIENT; | ||||||||||||||
| case UPLOAD: | ||||||||||||||
| case FINALIZE: | ||||||||||||||
| case UPLOAD_FINALIZE: | ||||||||||||||
| return Category.RECOVERABLE; | ||||||||||||||
| case QUERY: | ||||||||||||||
| default: | ||||||||||||||
| return Category.FATAL; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private static boolean isRecoverableCommand(ResumableUploadCommand command) { | ||||||||||||||
| switch (command) { | ||||||||||||||
| case UPLOAD: | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can add an |
||||||||||||||
| case FINALIZE: | ||||||||||||||
| case UPLOAD_FINALIZE: | ||||||||||||||
| return true; | ||||||||||||||
| case START: | ||||||||||||||
| case QUERY: | ||||||||||||||
| default: | ||||||||||||||
| return false; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| /* | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Redistribution and use in source and binary forms, with or without | ||
| * modification, are permitted provided that the following conditions are | ||
| * met: | ||
| * | ||
| * * Redistributions of source code must retain the above copyright | ||
| * notice, this list of conditions and the following disclaimer. | ||
| * * Redistributions in binary form must reproduce the above | ||
| * copyright notice, this list of conditions and the following disclaimer | ||
| * in the documentation and/or other materials provided with the | ||
| * distribution. | ||
| * * Neither the name of Google LLC nor the names of its | ||
| * contributors may be used to endorse or promote products derived from | ||
| * this software without specific prior written permission. | ||
| * | ||
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| */ | ||
| package com.google.api.gax.rpc; | ||
|
|
||
| import static com.google.api.gax.rpc.ResumableUploadErrorClassifier.Category.TRANSIENT; | ||
|
|
||
| import com.google.api.gax.retrying.BasicResultRetryAlgorithm; | ||
| import com.google.api.gax.rpc.ResumableUploadErrorClassifier.Category; | ||
| import java.util.Objects; | ||
| import org.jspecify.annotations.NullMarked; | ||
| import org.jspecify.annotations.Nullable; | ||
|
|
||
| /** | ||
| * An adapter that integrates {@link ResumableUploadErrorClassifier} into GAX retrying machinery. | ||
| * | ||
| * <p>Only transient errors should retry with an identical request; other recoverable errors will | ||
| * need to query the upload server to determine the appropriate next request. | ||
| * | ||
| * @param <ResponseT> the response type of the upload attempt | ||
| */ | ||
| @NullMarked | ||
| final class ResumableUploadResultRetryAlgorithm<ResponseT> | ||
|
Check warning on line 49 in sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadResultRetryAlgorithm.java
|
||
| extends BasicResultRetryAlgorithm<ResponseT> { | ||
|
|
||
| private final ResumableUploadCommand command; | ||
|
|
||
| ResumableUploadResultRetryAlgorithm(ResumableUploadCommand command) { | ||
| this.command = Objects.requireNonNull(command); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean shouldRetry( | ||
| @Nullable Throwable previousThrowable, @Nullable ResponseT previousResponse) { | ||
| // Successful commands should not retry. | ||
| if (previousThrowable == null) { | ||
| return false; | ||
| } | ||
| // Transient errors are retried directly with the identical request, others are not. | ||
| Category category = ResumableUploadErrorClassifier.classify(previousThrowable, command); | ||
| return category == TRANSIENT; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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?