Repository navigation
refactor(gax): remove circular ref between resumable upload future and chunk coordinator - #14421
Conversation
540dbd9 to
8f03a72
Compare
8f03a72 to
f4bc308
Compare
f4bc308 to
3979d5c
Compare
3979d5c to
dcd33e6
Compare
dcd33e6 to
572db5b
Compare
d885bb2 to
f2cc9b8
Compare
f2cc9b8 to
778fac8
Compare
|
/gemini review |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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);
}| synchronized (lock) { | ||
| if (resultFuture.isDone()) { | ||
| return; | ||
| } | ||
| inFlightFuture = uploadFuture; | ||
| } |
There was a problem hiding this comment.
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
- When concurrent operations (such as cancellation and lazy initialization) are protected by a common lock (e.g.,
synchronized (this)), atomic state transitions (likecompareAndSet) are not strictly necessary as the synchronization already prevents concurrent interleaving.
778fac8 to
5f27eaa
Compare
5f27eaa to
4459004
Compare
4459004 to
8fa30bb
Compare
5262566 to
291aebe
Compare
291aebe to
b0a46d3
Compare
b0a46d3 to
1683e9f
Compare
1683e9f to
4107698
Compare
|
|
||
| void start() { | ||
| ApiFuture<ResponseT> start() { | ||
| result.addListener( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Renamed to uploadResultFuture.
| if (resultFuture.isDone()) { | ||
| alreadyDone = true; | ||
| } else { | ||
| inFlightFuture = uploadFuture; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
I guess the only possibility that resultFuture is already done at this moment is that it is cancelled?
There was a problem hiding this comment.
For this PR yes; once we add in the global timeout (#14425) this would also apply in that case.
4107698 to
6b4c235
Compare
6b4c235 to
94394d8
Compare
94394d8 to
0a1f445
Compare
0a1f445 to
6a3866b
Compare
| MoreExecutors.directExecutor()); | ||
| } catch (Throwable t) { | ||
| sessionFuture.fail(t); | ||
| uploadResultFuture.setException(t); |
There was a problem hiding this comment.
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.
6a3866b to
3c38cdd
Compare
|
|




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.