Repository navigation
feat(gax): enforce global timeout for resumable uploads #14425
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 |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ | |
| */ | ||
| package com.google.api.gax.rpc; | ||
|
|
||
| import static com.google.common.base.MoreObjects.firstNonNull; | ||
| import static com.google.common.base.Preconditions.checkArgument; | ||
| import static com.google.common.base.Preconditions.checkNotNull; | ||
|
|
||
|
|
@@ -46,10 +47,12 @@ | |
| import com.google.errorprone.annotations.concurrent.GuardedBy; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.time.Duration; | ||
| import java.util.concurrent.CancellationException; | ||
| import java.util.concurrent.ExecutionException; | ||
| import java.util.concurrent.Executor; | ||
| import java.util.concurrent.ScheduledExecutorService; | ||
| import java.util.concurrent.ScheduledFuture; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.concurrent.TimeoutException; | ||
| import org.jspecify.annotations.NullMarked; | ||
|
|
@@ -64,6 +67,8 @@ | |
| @NullMarked | ||
| final class ResumableUploadFutureImpl<ResponseT> implements ResumableUploadFuture<ResponseT> { | ||
|
|
||
| private static final Duration DEFAULT_GLOBAL_TIMEOUT = Duration.ofMinutes(15); | ||
|
|
||
| private final Object lock = new Object(); | ||
|
|
||
| private final ApiFuture<ResumableUploadSession> startFuture; | ||
|
|
@@ -80,7 +85,8 @@ | |
|
|
||
| private volatile @Nullable String uploadSessionUrl; | ||
|
|
||
| // Tracks the current operation's Future (start, chunk upload) to propagate cancellation. | ||
| // Tracks the current operation's Future (start, chunk upload) to propagate cancellation, or | ||
| // null once a terminal transition (succeed, fail, cancel) has claimed completion. | ||
| @GuardedBy("lock") | ||
| private @Nullable ApiFuture<?> inFlightFuture; | ||
|
|
||
|
|
@@ -140,6 +146,10 @@ | |
| } | ||
|
|
||
| private void start() { | ||
| Duration timeout = firstNonNull(options.getGlobalTimeout(), DEFAULT_GLOBAL_TIMEOUT); | ||
| ScheduledFuture<?> timeoutFuture = | ||
| executor.schedule(this::onTimeout, timeout.toMillis(), TimeUnit.MILLISECONDS); | ||
| resultFuture.addListener(() -> timeoutFuture.cancel(false), MoreExecutors.directExecutor()); | ||
| ApiFutures.addCallback( | ||
| startFuture, | ||
| new ApiFutureCallback<ResumableUploadSession>() { | ||
|
|
@@ -158,7 +168,7 @@ | |
| executor); | ||
| ApiFuture<ResponseT> uploadFuture = coordinator.getFuture(); | ||
| synchronized (lock) { | ||
| if (resultFuture.isDone()) { | ||
| if (inFlightFuture == null) { | ||
| return; | ||
| } | ||
| uploadSessionUrl = sessionUrl; | ||
|
|
@@ -195,18 +205,38 @@ | |
| MoreExecutors.directExecutor()); | ||
| } | ||
|
|
||
| private void onTimeout() { | ||
| String sessionUrl = uploadSessionUrl; | ||
| String message; | ||
| if (sessionUrl != null) { | ||
| message = "Resumable upload timed out for session: " + sessionUrl; | ||
| } else { | ||
| message = "Resumable upload timed out before session initiation completed"; | ||
| } | ||
| fail(ApiExceptionFactory.createException(message, null, TIMEOUT_STATUS_CODE, false)); | ||
| } | ||
|
|
||
| private void succeed(@Nullable ResponseT result) { | ||
| synchronized (lock) { | ||
| if (inFlightFuture == null) { | ||
| return; | ||
| } | ||
| inFlightFuture = null; | ||
| } | ||
| closePayload(); | ||
| resultFuture.set(result); | ||
| } | ||
|
|
||
| private void fail(Throwable t) { | ||
| ApiFuture<?> inFlight; | ||
|
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. There could be a race condition per AI:
Contributor
Author
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. All terminal transitions (succeed, fail, cancel) now return early if
|
||
| synchronized (lock) { | ||
| if (inFlightFuture == null) { | ||
| return; | ||
| } | ||
| inFlight = inFlightFuture; | ||
| inFlightFuture = null; | ||
| } | ||
| inFlight.cancel(true); | ||
| closePayload(); | ||
| resultFuture.setException(t); | ||
| } | ||
|
|
@@ -234,13 +264,14 @@ | |
| boolean cancelled; | ||
| ApiFuture<?> inFlight; | ||
| synchronized (lock) { | ||
| if (inFlightFuture == null) { | ||
| return false; | ||
| } | ||
| cancelled = resultFuture.cancel(mayInterruptIfRunning); | ||
| inFlight = this.inFlightFuture; | ||
| this.inFlightFuture = null; | ||
| } | ||
| if (inFlight != null) { | ||
| inFlight.cancel(mayInterruptIfRunning); | ||
| inFlight = inFlightFuture; | ||
| inFlightFuture = null; | ||
| } | ||
| inFlight.cancel(mayInterruptIfRunning); | ||
| closePayload(); | ||
| return cancelled; | ||
| } | ||
|
|
@@ -265,4 +296,17 @@ | |
| throws InterruptedException, ExecutionException, TimeoutException { | ||
| return resultFuture.get(timeout, unit); | ||
| } | ||
|
|
||
| private static final StatusCode TIMEOUT_STATUS_CODE = | ||
| new StatusCode() { | ||
| @Override | ||
| public StatusCode.Code getCode() { | ||
| return StatusCode.Code.DEADLINE_EXCEEDED; | ||
| } | ||
|
|
||
| @Override | ||
| public @Nullable Object getTransportCode() { | ||
|
Check failure on line 308 in sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadFutureImpl.java
|
||
| return null; | ||
| } | ||
| }; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.