Skip to content

refactor(gax): remove circular ref between resumable upload future and chunk coordinator - #14421

Merged
whowes merged 1 commit into
mainfrom
whowes/resumable-upload-coordinator-refactor
Sep 25, 2026
Merged

whowes merged 1 commit into
mainfrom
whowes/resumable-upload-coordinator-refactor

Conversation

@whowes

@whowes whowes commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

This cleans up and helps clarify the layering and responsibility structure ahead of introducing non-happy path features.

In general the Future coordinates the overall upload lifecycle while delegating details of specific operations (e.g. chunk uploads, status listeners, global timeout) to the layer below. Actors on that layer don't maintain explicit references to the Future.

@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from 540dbd9 to 8f03a72 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-coordinator-refactor branch from 8f03a72 to f4bc308 Compare September 18, 2026 02:23
@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from f4bc308 to 3979d5c Compare September 18, 2026 03:21
@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from 3979d5c to dcd33e6 Compare September 18, 2026 15:04
@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from dcd33e6 to 572db5b Compare September 19, 2026 01:36
@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch 3 times, most recently from d885bb2 to f2cc9b8 Compare September 20, 2026 00:19
@whowes whowes changed the title refactor(gax): invert resumable upload future and coordinator ownership refactor(gax): remove circular dep between resumable upload future and chunk coordinator Sep 20, 2026
@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-coordinator-refactor branch from f2cc9b8 to 778fac8 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 refactors the resumable upload coordination by decoupling ResumableUploadChunkCoordinator from ResumableUploadFutureImpl. The coordinator now manages its own internal state and returns an ApiFuture upon starting, while ResumableUploadFutureImpl has been updated to observe this future and has removed the setInFlightFuture method. However, the review identifies two critical race conditions introduced by this refactoring: one in ResumableUploadChunkCoordinator where a cancellation of the result future after the initial check but before assigning currentChunkFuture can leak a background upload task, and another in ResumableUploadFutureImpl where a cancellation during coordinator.start() is not properly propagated to the newly created upload future. Both issues require adding explicit cancellation checks and propagation logic to prevent background resource leaks.

ApiFuture<ChunkUploadResponse<ResponseT>> chunkFuture =
uploadChunkCallable.futureCall(chunkRequest, callContext);
sessionFuture.setInFlightFuture(chunkFuture);
this.currentChunkFuture = chunkFuture;

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

There is a race condition where result can be cancelled after result.isDone() is checked at the beginning of transmitChunk, but before currentChunkFuture is assigned. In this scenario, the cancellation listener registered in start() will have already executed (finding currentChunkFuture to be null or a previous chunk), and the newly created chunkFuture will never be cancelled, leading to a leaked background upload task.

To prevent this, check if result has been cancelled immediately after assigning currentChunkFuture and cancel the chunk future if so.

      this.currentChunkFuture = chunkFuture;
      if (result.isCancelled()) {
        chunkFuture.cancel(true);
      }

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.

Fixed.

Comment on lines +142 to 147
synchronized (lock) {
if (resultFuture.isDone()) {
return;
}
inFlightFuture = uploadFuture;
}

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

With the removal of setInFlightFuture, there is a race condition where resultFuture can be cancelled while coordinator.start() is executing. If this happens, resultFuture.isDone() will be true when entering the synchronized block, and the method will return early without cancelling the newly started uploadFuture. This can leak the upload process in the background.

To fix this, check if resultFuture was cancelled when it is done, and propagate the cancellation to uploadFuture accordingly.

            boolean shouldCancel = false;
            synchronized (lock) {
              if (resultFuture.isDone()) {
                shouldCancel = resultFuture.isCancelled();
              } else {
                inFlightFuture = uploadFuture;
              }
            }
            if (shouldCancel) {
              uploadFuture.cancel(true);
              return;
            }
References
  1. When concurrent operations (such as cancellation and lazy initialization) are protected by a common lock (e.g., synchronized (this)), atomic state transitions (like compareAndSet) are not strictly necessary as the synchronization already prevents concurrent interleaving.

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.

Fixed.

@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from 778fac8 to 5f27eaa Compare September 21, 2026 18:32
@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from 5f27eaa to 4459004 Compare September 21, 2026 20:25
@whowes whowes changed the title refactor(gax): remove circular dep between resumable upload future and chunk coordinator refactor(gax): remove circular ref between resumable upload future and chunk coordinator Sep 21, 2026
@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from 4459004 to 8fa30bb Compare September 21, 2026 22:16
@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from 5262566 to 291aebe Compare September 22, 2026 06:46
@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from 291aebe to b0a46d3 Compare September 22, 2026 07:03
@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-coordinator-refactor branch from b0a46d3 to 1683e9f Compare September 22, 2026 18:52
@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from 1683e9f to 4107698 Compare September 22, 2026 19:56

void start() {
ApiFuture<ResponseT> start() {
result.addListener(

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: rename result to something more readable, otherwise it's not easy to comprehend. There might be a more concise name for it, but IIUC, it's basically a uploadAllChunksResultFuture?

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.

Renamed to uploadResultFuture.

if (resultFuture.isDone()) {
alreadyDone = true;
} else {
inFlightFuture = uploadFuture;

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 the purpose of inFlightFuture to keep track of the in flight future so we know which future to cancel when cancel() is called?

In general, I feel we have a lot of complex logics just to support correct cancellation of this future. Maybe we can restructure the code to make it simpler.

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.

Yes, your understanding of inFlightFuture is correct.

I simplified the handoff by exposing coordinator.getFuture() before calling coordinator.start(), so we swap inFlightFuture in a single synchronized block and eliminate the extra post-start cancellation check. I looked into a few other options (some transformAsync, some using listeners) but I feel like they led to denser, less-readable code overall, particularly when adding the features in later PRs.

}
boolean alreadyDone = false;
synchronized (lock) {
if (resultFuture.isDone()) {

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 guess the only possibility that resultFuture is already done at this moment is that it is cancelled?

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.

For this PR yes; once we add in the global timeout (#14425) this would also apply in that case.

@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from 4107698 to 6b4c235 Compare September 24, 2026 00:45
@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from 6b4c235 to 94394d8 Compare September 24, 2026 01:06
@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from 94394d8 to 0a1f445 Compare September 24, 2026 16:10
Base automatically changed from whowes/resumable-upload-status-header to main September 24, 2026 17:40
@whowes
whowes force-pushed the whowes/resumable-upload-coordinator-refactor branch from 0a1f445 to 6a3866b Compare September 24, 2026 17:40
@whowes
whowes marked this pull request as ready for review September 24, 2026 17:40
@whowes
whowes requested review from a team as code owners September 24, 2026 17:40
MoreExecutors.directExecutor());
} catch (Throwable t) {
sessionFuture.fail(t);
uploadResultFuture.setException(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.

Shall we wrap the whole transmitChunk method with try catch? Otherwise the uploadResultFuture may never complete if there are unhandled runtime exceptions outside of this try catch block.

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-coordinator-refactor branch from 6a3866b to 3c38cdd Compare September 24, 2026 20:42
@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

@whowes
whowes requested a review from blakeli0 September 25, 2026 14:55
@whowes
whowes merged commit fd4401e into main Sep 25, 2026
301 of 303 checks passed
@whowes
whowes deleted the whowes/resumable-upload-coordinator-refactor branch September 25, 2026 19:39
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