Skip to content

Commit 83b4b65

Browse files
committed
feat(gax): wire progress listener into coordinator and attempt lifecycle
1 parent 66b6815 commit 83b4b65

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;
@@ -183,6 +185,7 @@ public void onSuccess(ResumableUploadSession session) {
183185
}
184186
}
185187
uploadSessionUrl = session.getUploadUrl();
188+
progressTracker.onStarted(uploadSessionUrl);
186189
buffer = new RewindableStreamBuffer(payload, chunkSize, uploadSessionUrl);
187190
transmitChunk(0L);
188191
}
@@ -198,6 +201,14 @@ public void onFailure(Throwable t) {
198201
MoreExecutors.directExecutor());
199202
}
200203

204+
void addProgressListener(ResumableUploadProgressListener listener, Executor executor) {
205+
progressTracker.addListener(listener, executor);
206+
}
207+
208+
ResumableUploadStatus getStatus() {
209+
return progressTracker.getStatus();
210+
}
211+
201212
private static final StatusCode TIMEOUT_STATUS_CODE =
202213
new StatusCode() {
203214
@Override
@@ -210,7 +221,6 @@ public StatusCode.Code getCode() {
210221
return null;
211222
}
212223
};
213-
214224
private void onTimeout() {
215225
synchronized (lock) {
216226
if (done) {
@@ -261,6 +271,8 @@ void cancel(boolean mayInterruptIfRunning) {
261271
if (inFlight != null) {
262272
inFlight.cancel(mayInterruptIfRunning);
263273
}
274+
progressTracker.onFailed(
275+
new CancellationException("Upload was cancelled"), uploadSessionUrl);
264276
closePayload();
265277
}
266278

@@ -285,8 +297,15 @@ private void finish(@Nullable ResponseT response, @Nullable Throwable error) {
285297
}
286298
IOException closeError = closePayload();
287299
if (error == null) {
300+
RewindableStreamBuffer currentBuffer = buffer;
301+
long totalBytes =
302+
currentBuffer != null
303+
? currentBuffer.getBufferBaseOffset() + currentBuffer.getPayloadLength()
304+
: 0L;
305+
progressTracker.onFinalized(totalBytes);
288306
result.set(response);
289307
} else {
308+
progressTracker.onFailed(error, uploadSessionUrl);
290309
if (closeError != null) {
291310
error.addSuppressed(closeError);
292311
}
@@ -360,7 +379,8 @@ private void transmitChunk(long currentOffset) {
360379
url,
361380
chunkRequest,
362381
callContext,
363-
command);
382+
command,
383+
progressTracker);
364384

365385
RetryAlgorithm<ChunkUploadResponse<ResponseT>> retryAlgorithm =
366386
new RetryAlgorithm<>(
@@ -386,6 +406,7 @@ public void onSuccess(ChunkUploadResponse<ResponseT> response) {
386406
}
387407
}
388408
long nextOffset = streamBuffer.getBufferBaseOffset() + streamBuffer.getPayloadLength();
409+
progressTracker.onChunkUploaded(nextOffset);
389410
if (response.isComplete()) {
390411
finish(response.getResponse(), null);
391412
} 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)