Skip to content

feat(gax): retry chunk upload on transient errors - #14422

Merged
whowes merged 1 commit into
mainfrom
whowes/resumable-upload-chunk-retry
Sep 25, 2026
Merged

whowes merged 1 commit into
mainfrom
whowes/resumable-upload-chunk-retry

Conversation

@whowes

@whowes whowes commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Wraps chunk uploads in a RetryingCallable to retry transient errors.

Note: the retry settings used here are intentionally different from those that will be used for starting the upload; our requirement for this milestone is that "the retry policy [specified by the user] is only applicable to the initial request."

@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch from 809615f to d186a68 Compare September 17, 2026 22:08
gemini-code-assist[bot]

This comment was marked as outdated.

@whowes
whowes added this pull request to stack #14429 September 17, 2026 22:16
@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch from d186a68 to cbbeb54 Compare September 18, 2026 02:23
@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch 2 times, most recently from 7c209f2 to ce8a9f5 Compare September 18, 2026 03:21
@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch from ce8a9f5 to 896cd2f Compare September 18, 2026 15:04
@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch 2 times, most recently from 41d94b6 to 6b71690 Compare September 19, 2026 21:12
@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch from 6b71690 to 0f8b144 Compare September 19, 2026 22:57
@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch from 0f8b144 to d1e3b92 Compare September 19, 2026 23:17
@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch 5 times, most recently from 79b57f6 to f2e3c65 Compare September 20, 2026 05:23
@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-chunk-retry branch from f2e3c65 to 18e2288 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 automatic chunk retries with exponential backoff for resumable uploads by wrapping the chunk upload callable in a RetryingCallable and executing chunk transmissions sequentially. It also adds comprehensive unit tests to verify retry behavior, exhaustion, and cancellation. The review feedback highlights a security concern regarding the inclusion of the full uploadUrl in an exception message, as it may contain sensitive session tokens that could be leaked in logs.

Comment on lines +180 to +184
result.setException(
new IllegalStateException(
"Upload stream ended and final chunk was transmitted, but server returned"
+ " incomplete status"));
+ " incomplete status for upload URL: "
+ uploadUrl));

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.

security-medium medium

Including the full uploadUrl in the exception message poses a security risk. Resumable upload URLs contain sensitive session IDs or tokens (such as upload_id in Google Cloud Storage) that act as bearer credentials. If this exception is logged or propagated to client applications, it could leak these credentials. Please revert to the original exception message or redact the sensitive query parameters from the URL before including it in the exception.

                result.setException(
                    new IllegalStateException(
                        "Upload stream ended and final chunk was transmitted, but server returned"
                            + " incomplete status"));

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.

Leaving the URL in - it's a requirement that the URL is included in failure messages when possible. (I actually have a subsequent PR which expands that behavior).

@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch from 506f1b7 to ea6839b Compare September 22, 2026 15:49
@whowes
whowes removed this pull request from stack #14454 September 22, 2026 16:21
@whowes
whowes added this pull request to stack #14476 September 22, 2026 16:22
@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch from ea6839b to d7d1947 Compare September 22, 2026 18:52
@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch from d7d1947 to 08e807a Compare September 22, 2026 19:56
extends ResumableUploadCallable<RequestT, ResponseT> {

private static final RetrySettings RETRY_SETTINGS =
RetrySettings.newBuilder()

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 should at least set initialRpcTimeoutDuration and totalTimeoutDuration. Otherwise the call could hang indefinitely (before global timeout kicks in). These are the default generated retrySetting values.

Separately, check if there is a cross-language sensible default.

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.

The cross-language docs aren't very prescriptive here - no suggested values that I could see.

I did take a look at the Python resumable upload implementation and set initialRpcTimeoutDuration to 1m and totalTimeoutDuration to 2m as a result. I also adjusted the backoff params to mirror Python's too (now 1s initial delay, 2x multiplier, 1m max delay) - the former settings I had looked like they'd exhaust the retries in < 1s if the wire commands were failing fast.


// Serializes buffer mutations across multiple threads (i.e. from retry/recovery)
private final Executor chunkExecutor =
MoreExecutors.newSequentialExecutor(MoreExecutors.directExecutor());

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 to prevent stackoverflow of the sequential transmitChunk call? If it is, I don't think it would happen because each call will be run in a separate IOExecutor thread.

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.

Yeah, IIRC a Gemini review pointed out the overflow risk - but I think you're right, other than artificial executors in unit tests the calls will go on IO threads. Removed this.


private static final byte[] EMPTY_PAYLOAD = new byte[0];

// Serializes buffer mutations across multiple threads (i.e. from retry/recovery)

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.

I don't think there is a case for "serializing buffer mutations across multiple threads"? retry/recovery is always sequential.

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.

Removed this executor.

@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch from 08e807a to 4257f81 Compare September 24, 2026 00:45
@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch 2 times, most recently from 6b6d432 to 920d662 Compare September 24, 2026 05:52
@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch 2 times, most recently from a456ecb to 6d6f62a Compare September 24, 2026 17:40
throw new UnsupportedOperationException("Session resumption is not yet implemented.");
}

private static <ReqT, RespT> UnaryCallable<ReqT, RespT> createRetryingCallable(

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.

nit: use <RequestT, ResponseT> to be consistent with the class.

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.

Done.

@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch from 6d6f62a to de5ae9a Compare September 24, 2026 20:40
@whowes
whowes marked this pull request as ready for review September 24, 2026 20:45
@whowes
whowes requested review from a team as code owners September 24, 2026 20:45
Base automatically changed from whowes/resumable-upload-coordinator-refactor to main September 25, 2026 19:39
Wraps chunk uploads in a retrying executor to retry transient network and server errors using exponential backoff. Retries individual chunks without restarting the entire upload session.
@whowes
whowes force-pushed the whowes/resumable-upload-chunk-retry branch from de5ae9a to 5f8515f Compare September 25, 2026 19:40
@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 merged commit e72365e into main Sep 25, 2026
301 of 304 checks passed
@whowes
whowes deleted the whowes/resumable-upload-chunk-retry branch September 25, 2026 23:04
blakeli0 pushed a commit that referenced this pull request Sep 30, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>1.93.0</summary>

##
[1.93.0](v1.92.0...v1.93.0)
(2026-09-30)


### ⚠ BREAKING CHANGES

* **automl:** remove java-automl library
([#14540](#14540))

### Features

* **automl:** remove java-automl library
([#14540](#14540))
([6f13a1d](6f13a1d))
* **bigquery-jdbc:** infer undeclared fields in PreparedStatement
([#14400](#14400))
([e989a72](e989a72))
* **gax,gapic-generator-java:** retry start upload call and rename
ResumableUploadCallSettings to ResumableUploadOptions
([#14511](#14511))
([98c2073](98c2073))
* **gax:** add chunk upload recovery loop for resumable uploads
([#14424](#14424))
([8ecd1e5](8ecd1e5))
* **gax:** add ResumableUploadProgressListener and related
infrastructure
([#14426](#14426))
([c7f10d8](c7f10d8))
* **gax:** add RewindableStreamBuffer in prep for chunk upload recovery
([#14423](#14423))
([349c315](349c315))
* **gax:** enforce global timeout for resumable uploads
([#14425](#14425))
([3cb9805](3cb9805))
* **gax:** include upload-status header in resumable upload command
response objects
([#14420](#14420))
([23e49c1](23e49c1))
* **gax:** replace InputStream with InputStreamSupplier in resumable
upload public surfaces
([#14521](#14521))
([3c79730](3c79730))
* **gax:** retry chunk upload on transient errors
([#14422](#14422))
([e72365e](e72365e))
* **gax:** treat resumable upload server rejection as terminal
([#14516](#14516))
([b288788](b288788))
* **gax:** wire up ResumableUploadProgressTracker
([#14427](#14427))
([e7ba3e8](e7ba3e8))
* **grpc-gcp:** Add shared fallback state and probing recovery options
to GcpFallbackChannel
([#14013](#14013))
([675f639](675f639))
* **pubsub:** add publish start time to client telemetry header
([#14496](#14496))
([4b6a671](4b6a671))


### Bug Fixes

* **auth:** exclude javax.annotation-api from api-common dependency
([#14538](#14538))
([668fd18](668fd18)),
refs
[#12363](#12363)
* **bigquery-jdbc:** abort session when connection is closed
([#14273](#14273))
([5bd4520](5bd4520)),
refs
[#13922](#13922)
* **storage:** add App Hub storage.googleapis.com prefix to
destination.id
([#14527](#14527))
([0f6f9b5](0f6f9b5))


### Documentation

* Add gRPC Post-Quantum Cryptography Guide
([#14245](#14245))
([340a239](340a239))
* **samples:** add zonal bucket pre-warmed writer pool sample
([#14517](#14517))
([32e6f87](32e6f87))
* **samples:** pre-warm writer pool channels with flush() after open()
([#14537](#14537))
([df06290](df06290))
* update PQC guide to follow standard template format
([#14333](#14333))
([0b969d5](0b969d5))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
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