Skip to content

Commit 1078304

Browse files
committed
feat(gax): wire progress listener into coordinator and attempt lifecycle
1 parent e65339b commit 1078304

8 files changed

Lines changed: 373 additions & 10 deletions

File tree

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class ChunkAttemptCallable<ResponseT> implements Callable<ChunkUploadResponse<Re
7272
private final RewindableStreamBuffer buffer;
7373
private final String uploadUrl;
7474
private final ApiCallContext originalCallContext;
75+
private final UploadProgressTracker progressTracker;
7576
private final long deadlineNanos;
7677
private final ApiClock clock;
7778

@@ -90,7 +91,8 @@ class ChunkAttemptCallable<ResponseT> implements Callable<ChunkUploadResponse<Re
9091
String uploadUrl,
9192
ChunkUploadRequest request,
9293
ApiCallContext callContext,
93-
ResumableUploadCommand command) {
94+
ResumableUploadCommand command,
95+
UploadProgressTracker progressTracker) {
9496
this(
9597
uploadChunkCallable,
9698
queryStatusCallable,
@@ -99,6 +101,7 @@ class ChunkAttemptCallable<ResponseT> implements Callable<ChunkUploadResponse<Re
99101
request,
100102
callContext,
101103
command,
104+
progressTracker,
102105
Long.MAX_VALUE,
103106
NanoClock.getDefaultClock());
104107
}
@@ -111,6 +114,7 @@ class ChunkAttemptCallable<ResponseT> implements Callable<ChunkUploadResponse<Re
111114
ChunkUploadRequest request,
112115
ApiCallContext callContext,
113116
ResumableUploadCommand command,
117+
UploadProgressTracker progressTracker,
114118
long deadlineNanos,
115119
ApiClock clock) {
116120
this.uploadChunkCallable =
@@ -122,6 +126,7 @@ class ChunkAttemptCallable<ResponseT> implements Callable<ChunkUploadResponse<Re
122126
this.currentRequest = checkNotNull(request, "request must not be null");
123127
this.originalCallContext = checkNotNull(callContext, "callContext must not be null");
124128
this.currentCommand = checkNotNull(command, "command must not be null");
129+
this.progressTracker = checkNotNull(progressTracker, "progressTracker must not be null");
125130
this.deadlineNanos = deadlineNanos;
126131
this.clock = checkNotNull(clock, "clock must not be null");
127132
}
@@ -164,6 +169,8 @@ private void prepareAttempt(
164169
ApiCallContext attemptContext,
165170
RetryingFuture<ChunkUploadResponse<ResponseT>> currentRetryingFuture) {
166171
this.currentCommand = ResumableUploadCommand.QUERY;
172+
progressTracker.onRecovering(lastFailure);
173+
167174
// Per GAX-R7: query uses sensible unary defaults trimmed to the remaining global deadline.
168175
long remainingNanos =
169176
deadlineNanos == Long.MAX_VALUE
@@ -259,6 +266,7 @@ private void handleQuerySuccess(
259266
// Normal path: realign buffer to committedOffset, compact and top up.
260267
try {
261268
buffer.realignTo(committedOffset);
269+
progressTracker.onOffsetReceived(committedOffset);
262270
} catch (Throwable e) {
263271
failAttempt(attemptFuture, e);
264272
return;

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
8585
private final long deadlineNanos;
8686
private final ApiCallContext callContext;
8787
private final ClientContext clientContext;
88+
private final UploadProgressTracker progressTracker;
8889
private final SettableApiFuture<ResponseT> result = SettableApiFuture.create();
8990
private volatile @Nullable ApiFuture<?> currentChunkFuture;
9091

@@ -96,7 +97,8 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
9697
ResumableUploadCallSettings settings,
9798
long deadlineNanos,
9899
ApiCallContext callContext,
99-
ClientContext clientContext) {
100+
ClientContext clientContext,
101+
UploadProgressTracker progressTracker) {
100102
this.uploadChunkCallable =
101103
checkNotNull(uploadChunkCallable, "uploadChunkCallable must not be null");
102104
this.queryStatusCallable =
@@ -107,6 +109,7 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
107109
this.deadlineNanos = deadlineNanos;
108110
this.callContext = checkNotNull(callContext, "callContext must not be null");
109111
this.clientContext = checkNotNull(clientContext, "clientContext must not be null");
112+
this.progressTracker = checkNotNull(progressTracker, "progressTracker must not be null");
110113
this.buffer = new RewindableStreamBuffer(payload, settings.getChunkSize(), uploadUrl);
111114
}
112115

@@ -198,6 +201,7 @@ private void transmitSingleChunk(long currentOffset) {
198201
chunkRequest,
199202
chunkCallContext,
200203
command,
204+
progressTracker,
201205
deadlineNanos,
202206
clientContext.getClock());
203207

@@ -222,6 +226,7 @@ public void onSuccess(ChunkUploadResponse<ResponseT> response) {
222226
return;
223227
}
224228
long nextOffset = buffer.getBufferBaseOffset() + buffer.getPayloadLength();
229+
progressTracker.onChunkUploaded(nextOffset);
225230
if (response.getUploadStatus() == ResumableUploadStatus.FINAL) {
226231
result.set(response.getResponse());
227232
} else if (buffer.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 progress snapshot of the upload session. */
65+
ResumableUploadProgress getStatus();
5166
}

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ final class ResumableUploadFutureImpl<ResponseT> implements ResumableUploadFutur
7777
private final ApiCallContext callContext;
7878
private final ClientContext clientContext;
7979
private final ScheduledExecutorService executor;
80+
private final UploadProgressTracker progressTracker = new UploadProgressTracker();
8081
private final SettableApiFuture<ResponseT> resultFuture = SettableApiFuture.create();
8182

8283
private volatile @Nullable String uploadSessionUrl;
@@ -160,6 +161,7 @@ public void onSuccess(ResumableUploadSession session) {
160161
return;
161162
}
162163
uploadSessionUrl = session.getUploadUrl();
164+
progressTracker.onStarted(uploadSessionUrl);
163165
ResumableUploadChunkCoordinator<ResponseT> coordinator =
164166
new ResumableUploadChunkCoordinator<>(
165167
uploadChunkCallable,
@@ -169,7 +171,8 @@ public void onSuccess(ResumableUploadSession session) {
169171
settings,
170172
deadlineNanos,
171173
callContext,
172-
clientContext);
174+
clientContext,
175+
progressTracker);
173176
uploadFuture = coordinator.start();
174177
inFlightFuture = uploadFuture;
175178
}
@@ -241,11 +244,13 @@ private void finish(@Nullable ResponseT response, @Nullable Throwable error) {
241244
}
242245
IOException closeError = closePayload();
243246
if (error == null) {
247+
progressTracker.onFinalized(progressTracker.getStatus().getBytesUploaded());
244248
resultFuture.set(response);
245249
} else {
246250
if (closeError != null) {
247251
error.addSuppressed(closeError);
248252
}
253+
progressTracker.onFailed(error, uploadSessionUrl);
249254
resultFuture.setException(error);
250255
}
251256
}
@@ -270,6 +275,16 @@ private void finish(@Nullable ResponseT response, @Nullable Throwable error) {
270275
return uploadSessionUrl;
271276
}
272277

278+
@Override
279+
public void addProgressListener(ResumableUploadProgressListener listener, Executor executor) {
280+
progressTracker.addListener(listener, executor);
281+
}
282+
283+
@Override
284+
public ResumableUploadProgress getStatus() {
285+
return progressTracker.getStatus();
286+
}
287+
273288
@Override
274289
public void addListener(Runnable listener, Executor executor) {
275290
resultFuture.addListener(listener, executor);
@@ -295,6 +310,7 @@ public boolean cancel(boolean mayInterruptIfRunning) {
295310
if (inFlight != null) {
296311
inFlight.cancel(mayInterruptIfRunning);
297312
}
313+
progressTracker.onFailed(new CancellationException("Upload was cancelled"), uploadSessionUrl);
298314
closePayload();
299315
return resultFuture.cancel(mayInterruptIfRunning);
300316
}

‎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
@@ -131,7 +131,8 @@ void call_successfulChunk_setsAttemptFuture() throws Exception {
131131
"https://upload.url/test",
132132
request,
133133
callContext,
134-
ResumableUploadCommand.UPLOAD);
134+
ResumableUploadCommand.UPLOAD,
135+
new UploadProgressTracker());
135136

136137
callable.setRetryingFuture(mockExternalFuture);
137138
ChunkUploadResponse<String> callResult = callable.call();
@@ -179,7 +180,8 @@ void call_returnsWithoutBlocking_andPropagatesCancellation() throws Exception {
179180
"https://upload.url/test",
180181
request,
181182
callContext,
182-
ResumableUploadCommand.UPLOAD);
183+
ResumableUploadCommand.UPLOAD,
184+
new UploadProgressTracker());
183185

184186
List<Runnable> listeners = new ArrayList<>();
185187
doAnswer(
@@ -253,7 +255,8 @@ void call_perAttemptDeadline_appliesRpcTimeoutToCallContext() throws Exception {
253255
"https://upload.url/test",
254256
request,
255257
callContext,
256-
ResumableUploadCommand.UPLOAD);
258+
ResumableUploadCommand.UPLOAD,
259+
new UploadProgressTracker());
257260

258261
callable.setRetryingFuture(mockExternalFuture);
259262
callable.call();
@@ -294,7 +297,8 @@ void call_nonBlockingExecution_callingThreadMakesImmediateProgress() throws Exce
294297
"https://upload.url/test",
295298
request,
296299
callContext,
297-
ResumableUploadCommand.UPLOAD);
300+
ResumableUploadCommand.UPLOAD,
301+
new UploadProgressTracker());
298302

299303
callable.setRetryingFuture(mockExternalFuture);
300304

0 commit comments

Comments
 (0)