Repository navigation
feat(gax): enable stall control for resumable uploads #14989
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -42,11 +42,13 @@ | |||||||||||||||||||||||||||||||||||||||||
| import com.google.api.gax.resumable.QueryStatusResponse; | ||||||||||||||||||||||||||||||||||||||||||
| import com.google.api.gax.resumable.ResumableUploadStatus; | ||||||||||||||||||||||||||||||||||||||||||
| import com.google.api.gax.retrying.ExponentialRetryAlgorithm; | ||||||||||||||||||||||||||||||||||||||||||
| import com.google.api.gax.retrying.RetrySettings; | ||||||||||||||||||||||||||||||||||||||||||
| import com.google.api.gax.retrying.TimedAttemptSettings; | ||||||||||||||||||||||||||||||||||||||||||
| import com.google.api.gax.rpc.ResumableUploadErrorClassifier.Category; | ||||||||||||||||||||||||||||||||||||||||||
| import com.google.common.util.concurrent.MoreExecutors; | ||||||||||||||||||||||||||||||||||||||||||
| import java.io.IOException; | ||||||||||||||||||||||||||||||||||||||||||
| import java.io.InputStream; | ||||||||||||||||||||||||||||||||||||||||||
| import java.time.Duration; | ||||||||||||||||||||||||||||||||||||||||||
| import java.util.Optional; | ||||||||||||||||||||||||||||||||||||||||||
| import java.util.concurrent.CancellationException; | ||||||||||||||||||||||||||||||||||||||||||
| import java.util.concurrent.Future; | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -75,6 +77,7 @@ final class ResumableUploadChunkCoordinator<ResponseT> { | |||||||||||||||||||||||||||||||||||||||||
| private final ExponentialRetryAlgorithm recoveryAlgorithm; | ||||||||||||||||||||||||||||||||||||||||||
| private final ScheduledExecutorService executor; | ||||||||||||||||||||||||||||||||||||||||||
| private final ResumableUploadProgressTracker progressTracker; | ||||||||||||||||||||||||||||||||||||||||||
| private final @Nullable ResumableUploadStallController stallController; | ||||||||||||||||||||||||||||||||||||||||||
| private final SettableApiFuture<ResponseT> uploadResultFuture = SettableApiFuture.create(); | ||||||||||||||||||||||||||||||||||||||||||
| private volatile @Nullable Future<?> inFlightFuture; | ||||||||||||||||||||||||||||||||||||||||||
| private TimedAttemptSettings recoverySettings; | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -87,7 +90,8 @@ final class ResumableUploadChunkCoordinator<ResponseT> { | |||||||||||||||||||||||||||||||||||||||||
| InputStream payload, | ||||||||||||||||||||||||||||||||||||||||||
| int chunkSize, | ||||||||||||||||||||||||||||||||||||||||||
| ClientContext clientContext, | ||||||||||||||||||||||||||||||||||||||||||
| ResumableUploadProgressTracker progressTracker) { | ||||||||||||||||||||||||||||||||||||||||||
| ResumableUploadProgressTracker progressTracker, | ||||||||||||||||||||||||||||||||||||||||||
| @Nullable ResumableUploadStallController stallController) { | ||||||||||||||||||||||||||||||||||||||||||
| this.uploadChunkCallable = | ||||||||||||||||||||||||||||||||||||||||||
| checkNotNull(uploadChunkCallable, "uploadChunkCallable must not be null"); | ||||||||||||||||||||||||||||||||||||||||||
| this.queryStatusCallable = | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -101,11 +105,23 @@ final class ResumableUploadChunkCoordinator<ResponseT> { | |||||||||||||||||||||||||||||||||||||||||
| new ExponentialRetryAlgorithm( | ||||||||||||||||||||||||||||||||||||||||||
| ResumableUploadCallableImpl.RETRY_SETTINGS, clientContext.getClock()); | ||||||||||||||||||||||||||||||||||||||||||
| this.executor = checkNotNull(clientContext.getExecutor(), "executor must not be null"); | ||||||||||||||||||||||||||||||||||||||||||
| this.recoverySettings = recoveryAlgorithm.createFirstAttempt(); | ||||||||||||||||||||||||||||||||||||||||||
| this.progressTracker = checkNotNull(progressTracker, "progressTracker must not be null"); | ||||||||||||||||||||||||||||||||||||||||||
| this.stallController = stallController; | ||||||||||||||||||||||||||||||||||||||||||
| this.recoverySettings = createFirstRecoveryAttempt(); | ||||||||||||||||||||||||||||||||||||||||||
| this.buffer = new RewindableStreamBuffer(payload, chunkSize); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| private TimedAttemptSettings createFirstRecoveryAttempt() { | ||||||||||||||||||||||||||||||||||||||||||
| if (stallController != null) { | ||||||||||||||||||||||||||||||||||||||||||
| return recoveryAlgorithm.createFirstAttempt( | ||||||||||||||||||||||||||||||||||||||||||
| callContext.withRetrySettings( | ||||||||||||||||||||||||||||||||||||||||||
| ResumableUploadCallableImpl.RETRY_SETTINGS.toBuilder() | ||||||||||||||||||||||||||||||||||||||||||
| .setTotalTimeoutDuration(Duration.ZERO) | ||||||||||||||||||||||||||||||||||||||||||
| .build())); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| return recoveryAlgorithm.createFirstAttempt(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+114
to
+123
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ApiFuture<ResponseT> getFuture() { | ||||||||||||||||||||||||||||||||||||||||||
| return uploadResultFuture; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -149,10 +165,16 @@ private void transmitChunk() { | |||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ChunkUploadRequest chunkRequest = buildCurrentChunkRequest(); | ||||||||||||||||||||||||||||||||||||||||||
| ApiCallContext chunkCallContext = callContext; | ||||||||||||||||||||||||||||||||||||||||||
| if (stallController != null) { | ||||||||||||||||||||||||||||||||||||||||||
| chunkCallContext = | ||||||||||||||||||||||||||||||||||||||||||
| callContext.withRetrySettings( | ||||||||||||||||||||||||||||||||||||||||||
| stallController.onStartChunk(chunkRequest.getPayload().length)); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Dispatch the chunk upload call and register the in-flight future for cancellation. | ||||||||||||||||||||||||||||||||||||||||||
| ApiFuture<ChunkUploadResponse<ResponseT>> chunkFuture = | ||||||||||||||||||||||||||||||||||||||||||
| uploadChunkCallable.futureCall(chunkRequest, callContext); | ||||||||||||||||||||||||||||||||||||||||||
| uploadChunkCallable.futureCall(chunkRequest, chunkCallContext); | ||||||||||||||||||||||||||||||||||||||||||
| if (!tryRegisterInFlightFuture(chunkFuture)) { | ||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -189,6 +211,9 @@ public void onFailure(Throwable t) { | |||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||
| // Category.TRANSIENT errors reaching here have already exhausted their retry budget | ||||||||||||||||||||||||||||||||||||||||||
| // in the underlying RetryingCallable and become fatal per protocol specification. | ||||||||||||||||||||||||||||||||||||||||||
| if (stallController != null) { | ||||||||||||||||||||||||||||||||||||||||||
| t = stallController.translateException(t, ResumableUploadCommand.UPLOAD); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| uploadResultFuture.setException(t); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -203,8 +228,7 @@ private void recover(Throwable cause) { | |||||||||||||||||||||||||||||||||||||||||
| progressTracker.onRecovering(); | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| if (madeProgressSinceRecovery) { | ||||||||||||||||||||||||||||||||||||||||||
| recoverySettings = | ||||||||||||||||||||||||||||||||||||||||||
| recoveryAlgorithm.createNextAttempt(recoveryAlgorithm.createFirstAttempt()); | ||||||||||||||||||||||||||||||||||||||||||
| recoverySettings = recoveryAlgorithm.createNextAttempt(createFirstRecoveryAttempt()); | ||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||
| recoverySettings = recoveryAlgorithm.createNextAttempt(recoverySettings); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
230
to
234
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initialize if (recoverySettings == null) {
recoverySettings = createFirstRecoveryAttempt(callContext);
}
if (madeProgressSinceRecovery) {
recoverySettings = recoveryAlgorithm.createNextAttempt(createFirstRecoveryAttempt(callContext));
} else {
recoverySettings = recoveryAlgorithm.createNextAttempt(recoverySettings);
} |
||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -213,24 +237,34 @@ private void recover(Throwable cause) { | |||||||||||||||||||||||||||||||||||||||||
| uploadResultFuture.setException(cause); | ||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| Duration backoffDelay = recoverySettings.getRandomizedRetryDelayDuration(); | ||||||||||||||||||||||||||||||||||||||||||
| RetrySettings queryRetrySettings = null; | ||||||||||||||||||||||||||||||||||||||||||
| if (stallController != null) { | ||||||||||||||||||||||||||||||||||||||||||
| queryRetrySettings = stallController.onStartQuery(backoffDelay); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| RetrySettings finalQueryRetrySettings = queryRetrySettings; | ||||||||||||||||||||||||||||||||||||||||||
| tryRegisterInFlightFuture( | ||||||||||||||||||||||||||||||||||||||||||
| executor.schedule( | ||||||||||||||||||||||||||||||||||||||||||
| this::queryStatus, | ||||||||||||||||||||||||||||||||||||||||||
| recoverySettings.getRandomizedRetryDelayDuration().toNanos(), | ||||||||||||||||||||||||||||||||||||||||||
| () -> queryStatus(finalQueryRetrySettings), | ||||||||||||||||||||||||||||||||||||||||||
| backoffDelay.toNanos(), | ||||||||||||||||||||||||||||||||||||||||||
| TimeUnit.NANOSECONDS)); | ||||||||||||||||||||||||||||||||||||||||||
| } catch (Throwable t) { | ||||||||||||||||||||||||||||||||||||||||||
| uploadResultFuture.setException(t); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| private void queryStatus() { | ||||||||||||||||||||||||||||||||||||||||||
| private void queryStatus(@Nullable RetrySettings queryRetrySettings) { | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| if (uploadResultFuture.isDone()) { | ||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| ApiCallContext queryCallContext = callContext; | ||||||||||||||||||||||||||||||||||||||||||
| if (queryRetrySettings != null) { | ||||||||||||||||||||||||||||||||||||||||||
| queryCallContext = callContext.withRetrySettings(queryRetrySettings); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| // Dispatch the query status call and register the in-flight future for cancellation. | ||||||||||||||||||||||||||||||||||||||||||
| ApiFuture<QueryStatusResponse<ResponseT>> queryFuture = | ||||||||||||||||||||||||||||||||||||||||||
| queryStatusCallable.futureCall(QueryStatusRequest.create(uploadUrl), callContext); | ||||||||||||||||||||||||||||||||||||||||||
| queryStatusCallable.futureCall(QueryStatusRequest.create(uploadUrl), queryCallContext); | ||||||||||||||||||||||||||||||||||||||||||
| if (!tryRegisterInFlightFuture(queryFuture)) { | ||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -254,6 +288,9 @@ public void onFailure(Throwable t) { | |||||||||||||||||||||||||||||||||||||||||
| if (t instanceof CancellationException || uploadResultFuture.isDone()) { | ||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| if (stallController != null) { | ||||||||||||||||||||||||||||||||||||||||||
| t = stallController.translateException(t, ResumableUploadCommand.QUERY); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| uploadResultFuture.setException(t); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -294,6 +331,10 @@ private void handleQueryResponse(QueryStatusResponse<ResponseT> queryResponse) | |||||||||||||||||||||||||||||||||||||||||
| throw new IllegalStateException( | ||||||||||||||||||||||||||||||||||||||||||
| "Incomplete query status response did not include a committed offset"); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| long previousBaseOffset = buffer.getBufferBaseOffset(); | ||||||||||||||||||||||||||||||||||||||||||
| if (stallController != null) { | ||||||||||||||||||||||||||||||||||||||||||
| stallController.onRecoveryOffsetReceived(committedOffset - previousBaseOffset); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| progressTracker.onOffsetReceived(committedOffset, chunkSize); | ||||||||||||||||||||||||||||||||||||||||||
| buffer.realignTo(committedOffset); | ||||||||||||||||||||||||||||||||||||||||||
| transmitChunk(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -309,6 +350,9 @@ private void handleChunkResponse(ChunkUploadResponse<ResponseT> response) throws | |||||||||||||||||||||||||||||||||||||||||
| "Upload stream ended and final chunk was transmitted, but server returned" | ||||||||||||||||||||||||||||||||||||||||||
| + " incomplete status")); | ||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||
| if (stallController != null) { | ||||||||||||||||||||||||||||||||||||||||||
| stallController.onChunkSuccess(buffer.getPayload().length); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| madeProgressSinceRecovery = true; | ||||||||||||||||||||||||||||||||||||||||||
| buffer.fill(); | ||||||||||||||||||||||||||||||||||||||||||
| progressTracker.onChunkUploaded(buffer.getBufferBaseOffset()); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,24 @@ public abstract class ResumableUploadOptions { | |
| */ | ||
| public abstract @Nullable ResumableUploadProgressListener getProgressListener(); | ||
|
|
||
| /** | ||
| * Returns the minimum acceptable transfer rate in MiB/s (1 MiB = 1,048,576 bytes) for stall | ||
| * detection, or {@code null} if unset. Stall control is only active when both {@link | ||
| * #getTransferStallMinimumRate()} and {@link #getTransferStallTimeout()} are set. | ||
| */ | ||
| public abstract @Nullable Double getTransferStallMinimumRate(); | ||
|
|
||
| /** | ||
| * Returns the maximum duration the upload may remain below {@link #getTransferStallMinimumRate()} | ||
| * before being aborted as stalled, or {@code null} if unset. Stall control is only active when | ||
| * both {@link #getTransferStallMinimumRate()} and {@link #getTransferStallTimeout()} are set. | ||
| */ | ||
| public abstract @Nullable Duration getTransferStallTimeout(); | ||
|
|
||
| boolean isStallControlEnabled() { | ||
| return getTransferStallMinimumRate() != null && getTransferStallTimeout() != null; | ||
| } | ||
|
|
||
| /** | ||
| * Merges another {@code ResumableUploadOptions} instance with this one. Fields set in {@code | ||
| * other} override fields in this instance. | ||
|
|
@@ -76,8 +94,17 @@ public ResumableUploadOptions merge(@Nullable ResumableUploadOptions other) { | |
| if (other.getChunkSize() > 0) { | ||
| builder.setChunkSize(other.getChunkSize()); | ||
| } | ||
| if (other.getTransferStallMinimumRate() != null) { | ||
| builder.setTransferStallMinimumRate(other.getTransferStallMinimumRate()); | ||
| } | ||
| if (other.getTransferStallTimeout() != null) { | ||
| builder.setTransferStallTimeout(other.getTransferStallTimeout()); | ||
| } | ||
| if (other.getGlobalTimeout() != null) { | ||
| builder.setGlobalTimeout(other.getGlobalTimeout()); | ||
| } else if (builder.getTransferStallMinimumRate() != null | ||
| && builder.getTransferStallTimeout() != null) { | ||
| builder.setGlobalTimeout(null); | ||
| } | ||
|
Comment on lines
+105
to
108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic causes a side-effect where merging an options instance that has both stall control and a custom References
|
||
| ResumableUploadProgressListener otherProgressListener = other.getProgressListener(); | ||
| if (otherProgressListener != null) { | ||
|
|
@@ -128,6 +155,33 @@ public abstract static class Builder { | |
| public abstract Builder setProgressListener( | ||
| @Nullable ResumableUploadProgressListener progressListener); | ||
|
|
||
| /** | ||
| * Sets the minimum acceptable transfer rate in MiB/s (1 MiB = 1,048,576 bytes) for stall | ||
| * detection. Must be finite and positive. Stall control is only active when both {@code | ||
| * transferStallMinimumRate} and {@code transferStallTimeout} are set; enabling stall control | ||
| * without an explicit {@link #setGlobalTimeout(Duration)} disables the default global timeout. | ||
| */ | ||
| public abstract Builder setTransferStallMinimumRate(@Nullable Double transferStallMinimumRate); | ||
|
|
||
| /** | ||
| * Returns the minimum acceptable transfer rate in MiB/s, or {@code null} if unset. See {@link | ||
| * #setTransferStallMinimumRate(Double)}. | ||
| */ | ||
| public abstract @Nullable Double getTransferStallMinimumRate(); | ||
|
|
||
| /** | ||
| * Sets the maximum duration the upload may remain below {@link #getTransferStallMinimumRate()} | ||
| * before being aborted as stalled. Must be positive. Stall control is only active when both | ||
| * {@code transferStallMinimumRate} and {@code transferStallTimeout} are set. | ||
| */ | ||
| public abstract Builder setTransferStallTimeout(@Nullable Duration transferStallTimeout); | ||
|
|
||
| /** | ||
| * Returns the stall timeout duration, or {@code null} if unset. See {@link | ||
| * #setTransferStallTimeout(Duration)}. | ||
| */ | ||
| public abstract @Nullable Duration getTransferStallTimeout(); | ||
|
|
||
| abstract ResumableUploadOptions autoBuild(); | ||
|
|
||
| public ResumableUploadOptions build() { | ||
|
|
@@ -137,6 +191,16 @@ public ResumableUploadOptions build() { | |
| !getGlobalTimeout().isNegative() && !getGlobalTimeout().isZero(), | ||
| "globalTimeout must be positive"); | ||
| } | ||
| if (getTransferStallMinimumRate() != null) { | ||
| Preconditions.checkArgument( | ||
| Double.isFinite(getTransferStallMinimumRate()) && getTransferStallMinimumRate() > 0.0, | ||
| "transferStallMinimumRate must be finite and positive"); | ||
| } | ||
| if (getTransferStallTimeout() != null) { | ||
| Preconditions.checkArgument( | ||
| !getTransferStallTimeout().isNegative() && !getTransferStallTimeout().isZero(), | ||
| "transferStallTimeout must be positive"); | ||
| } | ||
| return autoBuild(); | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the initialization of
recoverySettingsfrom the constructor becausecallContextis not yet initialized at construction time (it isnull). CallingcreateFirstRecoveryAttempt()here will throw aNullPointerExceptionwhenstallControlleris not null.