Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the ResumableUploadStallController class along with its unit tests to track chunk transfer progress against a minimum transfer rate and dynamically compute per-chunk and per-query retry deadlines. The review feedback focuses on improving robustness by adding input and state validation using Preconditions (such as validating non-null parameters, positive rates, and non-negative chunk sizes) and avoiding redundant exception wrapping in translateException when dealing with already-translated DeadlineExceededException instances.
| ResumableUploadStallController( | ||
| ApiClock clock, | ||
| double minimumRateMiBPerSec, | ||
| Duration stallTimeout, | ||
| @Nullable Duration globalDeadline, | ||
| RetrySettings baseRetrySettings) { | ||
| this.clock = clock; | ||
| this.minimumRateBytesPerSec = minimumRateMiBPerSec * BYTES_PER_MIB; | ||
| this.stallTimeout = stallTimeout; | ||
| this.globalDeadline = globalDeadline; | ||
| this.baseRetrySettings = baseRetrySettings; | ||
| } |
There was a problem hiding this comment.
Validate that minimumRateMiBPerSec and stallTimeout are strictly positive, and that non-nullable parameters are not null. This prevents division by zero, negative durations, or NullPointerExceptions later in the execution.
ResumableUploadStallController(
ApiClock clock,
double minimumRateMiBPerSec,
Duration stallTimeout,
@Nullable Duration globalDeadline,
RetrySettings baseRetrySettings) {
Preconditions.checkNotNull(clock, "clock cannot be null");
Preconditions.checkNotNull(stallTimeout, "stallTimeout cannot be null");
Preconditions.checkNotNull(baseRetrySettings, "baseRetrySettings cannot be null");
Preconditions.checkArgument(minimumRateMiBPerSec > 0.0, "minimumRateMiBPerSec must be positive");
Preconditions.checkArgument(!stallTimeout.isNegative() && !stallTimeout.isZero(), "stallTimeout must be positive");
this.clock = clock;
this.minimumRateBytesPerSec = minimumRateMiBPerSec * BYTES_PER_MIB;
this.stallTimeout = stallTimeout;
this.globalDeadline = globalDeadline;
this.baseRetrySettings = baseRetrySettings;
}| * Updates lag and stallStartedAt after chunk bytes are acknowledged; throws if stall timeout | ||
| * expired. | ||
| */ | ||
| void onChunkSuccess(long chunkBytes) { |
There was a problem hiding this comment.
Add state and argument validation to onChunkSuccess:
- Ensure
onChunkSuccessis not called beforeonStartChunkhas initialized the chunk tracking state (i.e.,currentChunkDeadlinemust not be null). - Ensure
chunkBytesis non-negative.
| void onChunkSuccess(long chunkBytes) { | |
| void onChunkSuccess(long chunkBytes) { | |
| Preconditions.checkState(currentChunkDeadline != null, "onChunkSuccess called before onStartChunk"); | |
| Preconditions.checkArgument(chunkBytes >= 0L, "chunkBytes must be non-negative"); |
| import com.google.api.gax.retrying.RetrySettings; | ||
| import com.google.api.gax.rpc.ResumableUploadErrorClassifier.Category; | ||
| import com.google.api.gax.rpc.StatusCode.Code; | ||
| import com.google.common.base.MoreObjects; |
| } | ||
|
|
||
| /** Computes per-chunk RetrySettings (local deadline + 2x expected per-attempt timeout). */ | ||
| RetrySettings onStartChunk(long chunkBytes) { |
There was a problem hiding this comment.
Add a precondition check to ensure chunkBytes is non-negative. A negative value would indicate a bug in the caller and would corrupt the expected duration and lag calculations.
| RetrySettings onStartChunk(long chunkBytes) { | |
| RetrySettings onStartChunk(long chunkBytes) { | |
| Preconditions.checkArgument(chunkBytes >= 0L, "chunkBytes must be non-negative"); |
| * Computes RetrySettings for queryStatus, enforcing that mid-upload recovery shares the active | ||
| * chunk's remaining local deadline (or uses stallTimeout on an initial resume()). | ||
| */ | ||
| RetrySettings onStartQuery(Duration recoveryBackoffDelay) { |
There was a problem hiding this comment.
Validate that recoveryBackoffDelay is non-null and non-negative to prevent invalid calculations.
| RetrySettings onStartQuery(Duration recoveryBackoffDelay) { | |
| RetrySettings onStartQuery(Duration recoveryBackoffDelay) { | |
| Preconditions.checkNotNull(recoveryBackoffDelay, "recoveryBackoffDelay cannot be null"); | |
| Preconditions.checkArgument(!recoveryBackoffDelay.isNegative(), "recoveryBackoffDelay cannot be negative"); |
| Throwable translateException(Throwable t, ResumableUploadCommand command) { | ||
| Category category = ResumableUploadErrorClassifier.classify(t, command); | ||
| if (category != Category.TRANSIENT && !(t instanceof DeadlineExceededException)) { | ||
| return t; | ||
| } | ||
| return createStallOrGlobalTimeoutException(t); | ||
| } |
There was a problem hiding this comment.
If t is already a DeadlineExceededException thrown by this controller, calling translateException will wrap it again. Check if the exception is already a DeadlineExceededException created by this controller and return it directly to avoid redundant nesting.
| Throwable translateException(Throwable t, ResumableUploadCommand command) { | |
| Category category = ResumableUploadErrorClassifier.classify(t, command); | |
| if (category != Category.TRANSIENT && !(t instanceof DeadlineExceededException)) { | |
| return t; | |
| } | |
| return createStallOrGlobalTimeoutException(t); | |
| } | |
| Throwable translateException(Throwable t, ResumableUploadCommand command) { | |
| if (t instanceof DeadlineExceededException && t.getMessage() != null | |
| && t.getMessage().startsWith("Resumable upload")) { | |
| return t; | |
| } | |
| Category category = ResumableUploadErrorClassifier.classify(t, command); | |
| if (category != Category.TRANSIENT && !(t instanceof DeadlineExceededException)) { | |
| return t; | |
| } | |
| return createStallOrGlobalTimeoutException(t); | |
| } |
|
|





WIP, not ready for review