Skip to content

Commit ca9311c

Browse files
committed
feat(gax): wire progress listener into coordinator and attempt lifecycle
1 parent 18db19d commit ca9311c

6 files changed

Lines changed: 354 additions & 13 deletions

File tree

‎sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ChunkAttemptCallable.java‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
* session. Used with {@link com.google.api.gax.retrying.ScheduledRetryingExecutor}.
5252
*
5353
* <p>Execution follows the standard attempt template with pre-attempt recovery handling. When the
54-
* previous attempt failed with a Category 2 (recoverable) error or missing status header, {@code
54+
* previous attempt failed with a recoverable error or missing status header, {@code
5555
* prepareAttempt} queries session status, realigns the buffer window, tops up from the stream, and
5656
* dispatches the chunk upload request. The callable never blocks on {@code .get()}; results and
5757
* cancellations propagate asynchronously.
@@ -68,6 +68,7 @@ class ChunkAttemptCallable<ResponseT> implements Callable<ChunkUploadResponse<Re
6868
private final RewindableStreamBuffer buffer;
6969
private final String uploadUrl;
7070
private final ApiCallContext originalCallContext;
71+
private final @Nullable UploadProgressTracker progressTracker;
7172

7273
private volatile ChunkUploadRequest currentRequest;
7374
private volatile UploadCommand currentCommand;
@@ -84,7 +85,8 @@ class ChunkAttemptCallable<ResponseT> implements Callable<ChunkUploadResponse<Re
8485
String uploadUrl,
8586
ChunkUploadRequest request,
8687
ApiCallContext callContext,
87-
UploadCommand command) {
88+
UploadCommand command,
89+
@Nullable UploadProgressTracker progressTracker) {
8890
this.uploadChunkCallable =
8991
checkNotNull(uploadChunkCallable, "uploadChunkCallable must not be null");
9092
this.queryStatusCallable =
@@ -94,6 +96,7 @@ class ChunkAttemptCallable<ResponseT> implements Callable<ChunkUploadResponse<Re
9496
this.currentRequest = checkNotNull(request, "request must not be null");
9597
this.originalCallContext = checkNotNull(callContext, "callContext must not be null");
9698
this.currentCommand = checkNotNull(command, "command must not be null");
99+
this.progressTracker = progressTracker;
97100
}
98101

99102
void setRetryingFuture(RetryingFuture<ChunkUploadResponse<ResponseT>> retryingFuture) {
@@ -128,6 +131,9 @@ private void prepareAttempt(
128131
SettableApiFuture<ChunkUploadResponse<ResponseT>> attemptFuture,
129132
ApiCallContext attemptContext,
130133
RetryingFuture<ChunkUploadResponse<ResponseT>> currentRetryingFuture) {
134+
if (progressTracker != null) {
135+
progressTracker.onRecovering(lastFailure);
136+
}
131137
QueryStatusRequest queryRequest = QueryStatusRequest.create(uploadUrl);
132138
ApiFuture<QueryStatusResponse<ResponseT>> queryFuture =
133139
queryStatusCallable.futureCall(queryRequest, attemptContext);
@@ -190,14 +196,18 @@ private void handleQuerySuccess(
190196
failAttempt(
191197
attemptFuture,
192198
RewindableStreamBuffer.protocolViolation(
193-
"Incomplete query status response did not include a committed offset for upload URL: "
199+
"Incomplete query status response did not include a committed offset for upload"
200+
+ " URL: "
194201
+ uploadUrl));
195202
return;
196203
}
197204

198205
// Normal path: realign buffer to committedOffset, compact and top up.
199206
try {
200207
buffer.realignTo(committedOffset);
208+
if (progressTracker != null) {
209+
progressTracker.onOffsetReceived(committedOffset);
210+
}
201211
} catch (Throwable e) {
202212
failAttempt(attemptFuture, e);
203213
return;
@@ -207,7 +217,8 @@ private void handleQuerySuccess(
207217
// Preserve upload,finalize for a trailing partial after realignment.
208218
UploadCommand realignedCommand;
209219
if (buffer.isFinal()) {
210-
realignedCommand = buffer.isEmpty() ? UploadCommand.FINALIZE : UploadCommand.UPLOAD_FINALIZE;
220+
realignedCommand =
221+
buffer.isEmpty() ? UploadCommand.FINALIZE : UploadCommand.UPLOAD_FINALIZE;
211222
} else {
212223
realignedCommand = UploadCommand.UPLOAD;
213224
}

‎sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadChunkCoordinator.java‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import java.io.InputStream;
5353
import java.time.Duration;
5454
import java.util.concurrent.CancellationException;
55+
import java.util.concurrent.Executor;
5556
import java.util.concurrent.ScheduledExecutorService;
5657
import java.util.concurrent.ScheduledFuture;
5758
import java.util.concurrent.TimeUnit;
@@ -92,6 +93,7 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
9293
private final ApiCallContext callContext;
9394
private final ClientContext clientContext;
9495
private final RetrySettings chunkRetrySettings;
96+
private final UploadProgressTracker progressTracker = new UploadProgressTracker();
9597

9698
private volatile @Nullable String uploadSessionUrl;
9799
private volatile @Nullable RewindableStreamBuffer buffer;
@@ -160,6 +162,7 @@ public void onSuccess(ResumableUploadSession session) {
160162
}
161163
}
162164
uploadSessionUrl = session.getUploadUrl();
165+
progressTracker.onStarted(uploadSessionUrl);
163166
buffer = new RewindableStreamBuffer(payload, chunkSize, uploadSessionUrl);
164167
transmitChunk(0L);
165168
}
@@ -175,6 +178,14 @@ public void onFailure(Throwable t) {
175178
MoreExecutors.directExecutor());
176179
}
177180

181+
void addProgressListener(ResumableUploadProgressListener listener, Executor executor) {
182+
progressTracker.addListener(listener, executor);
183+
}
184+
185+
ResumableUploadStatus getStatus() {
186+
return progressTracker.getStatus();
187+
}
188+
178189
private static final StatusCode TIMEOUT_STATUS_CODE =
179190
new StatusCode() {
180191
@Override
@@ -187,7 +198,6 @@ public StatusCode.Code getCode() {
187198
return null;
188199
}
189200
};
190-
191201
private void onTimeout() {
192202
synchronized (lock) {
193203
if (done) {
@@ -238,6 +248,8 @@ void cancel(boolean mayInterruptIfRunning) {
238248
if (inFlight != null) {
239249
inFlight.cancel(mayInterruptIfRunning);
240250
}
251+
progressTracker.onFailed(
252+
new CancellationException("Upload was cancelled"), uploadSessionUrl);
241253
closePayload();
242254
}
243255

@@ -262,8 +274,15 @@ private void finish(@Nullable ResponseT response, @Nullable Throwable error) {
262274
}
263275
IOException closeError = closePayload();
264276
if (error == null) {
277+
RewindableStreamBuffer currentBuffer = buffer;
278+
long totalBytes =
279+
currentBuffer != null
280+
? currentBuffer.getBufferBaseOffset() + currentBuffer.getPayloadLength()
281+
: 0L;
282+
progressTracker.onFinalized(totalBytes);
265283
result.set(response);
266284
} else {
285+
progressTracker.onFailed(error, uploadSessionUrl);
267286
if (closeError != null) {
268287
error.addSuppressed(closeError);
269288
}
@@ -337,7 +356,8 @@ private void transmitChunk(long currentOffset) {
337356
url,
338357
chunkRequest,
339358
callContext,
340-
command);
359+
command,
360+
progressTracker);
341361

342362
RetryAlgorithm<ChunkUploadResponse<ResponseT>> retryAlgorithm =
343363
new RetryAlgorithm<>(
@@ -363,6 +383,7 @@ public void onSuccess(ChunkUploadResponse<ResponseT> response) {
363383
}
364384
}
365385
long nextOffset = streamBuffer.getBufferBaseOffset() + streamBuffer.getPayloadLength();
386+
progressTracker.onChunkUploaded(nextOffset);
366387
if (response.isComplete()) {
367388
finish(response.getResponse(), null);
368389
} else if (streamBuffer.isFinal()) {

‎sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadFuture.java‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import com.google.api.core.ApiFuture;
3333
import com.google.api.core.BetaApi;
34+
import java.util.concurrent.Executor;
3435
import org.jspecify.annotations.NullMarked;
3536
import org.jspecify.annotations.Nullable;
3637

@@ -48,4 +49,18 @@ public interface ResumableUploadFuture<ResponseT> extends ApiFuture<ResponseT> {
4849

4950
/** Returns the upload session URL, or {@code null} if session initiation is in progress. */
5051
@Nullable String getUploadSessionUrl();
52+
53+
/**
54+
* Registers a listener to receive progress and state transition notifications for this upload.
55+
*
56+
* <p>A snapshot of the current upload status is dispatched to the listener immediately upon
57+
* subscription on the provided executor. Subsequent status updates are delivered in order.
58+
*
59+
* @param listener callback listener to receive progress notifications
60+
* @param executor executor on which the listener callbacks are dispatched
61+
*/
62+
void addProgressListener(ResumableUploadProgressListener listener, Executor executor);
63+
64+
/** Returns the current status snapshot of the upload session. */
65+
ResumableUploadStatus getStatus();
5166
}

‎sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadFutureImpl.java‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ void setInFlightFuture(ApiFuture<?> future) {
9898
return coordinator.getUploadSessionUrl();
9999
}
100100

101+
@Override
102+
public void addProgressListener(ResumableUploadProgressListener listener, Executor executor) {
103+
coordinator.addProgressListener(listener, executor);
104+
}
105+
106+
@Override
107+
public ResumableUploadStatus getStatus() {
108+
return coordinator.getStatus();
109+
}
110+
101111
@Override
102112
public void addListener(Runnable listener, Executor executor) {
103113
result.addListener(listener, executor);
@@ -120,12 +130,12 @@ public boolean isDone() {
120130
}
121131

122132
@Override
123-
public ResponseT get() throws InterruptedException, ExecutionException {
133+
public @Nullable ResponseT get() throws InterruptedException, ExecutionException {
124134
return result.get();
125135
}
126136

127137
@Override
128-
public ResponseT get(long timeout, TimeUnit unit)
138+
public @Nullable ResponseT get(long timeout, TimeUnit unit)
129139
throws InterruptedException, ExecutionException, TimeoutException {
130140
return result.get(timeout, unit);
131141
}

‎sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ChunkAttemptCallableTest.java‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ void call_successfulChunk_setsAttemptFuture() throws Exception {
129129
"https://upload.url/test",
130130
request,
131131
callContext,
132-
UploadCommand.UPLOAD);
132+
UploadCommand.UPLOAD,
133+
/* progressTracker= */ null);
133134

134135
callable.setRetryingFuture(mockExternalFuture);
135136
ChunkUploadResponse<String> callResult = callable.call();
@@ -177,7 +178,8 @@ void call_returnsWithoutBlocking_andPropagatesCancellation() throws Exception {
177178
"https://upload.url/test",
178179
request,
179180
callContext,
180-
UploadCommand.UPLOAD);
181+
UploadCommand.UPLOAD,
182+
/* progressTracker= */ null);
181183

182184
List<Runnable> listeners = new ArrayList<>();
183185
doAnswer(
@@ -250,7 +252,8 @@ void call_perAttemptDeadline_appliesRpcTimeoutToCallContext() throws Exception {
250252
"https://upload.url/test",
251253
request,
252254
callContext,
253-
UploadCommand.UPLOAD);
255+
UploadCommand.UPLOAD,
256+
/* progressTracker= */ null);
254257

255258
callable.setRetryingFuture(mockExternalFuture);
256259
callable.call();
@@ -292,7 +295,8 @@ void call_nonBlockingExecution_callingThreadMakesImmediateProgress() throws Exce
292295
"https://upload.url/test",
293296
request,
294297
callContext,
295-
UploadCommand.UPLOAD);
298+
UploadCommand.UPLOAD,
299+
/* progressTracker= */ null);
296300

297301
callable.setRetryingFuture(mockExternalFuture);
298302

0 commit comments

Comments
 (0)