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
8fa30bb to
01efb2f
Compare
01efb2f to
5262566
Compare
5262566 to
291aebe
Compare
291aebe to
b0a46d3
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.