Skip to content

Commit f9ca3dc

Browse files
committed
feat(gax): add recoverable error query and buffer realignment loop
Introduces a query-and-realign loop when encountering recoverable protocol errors during chunk uploads. Queries the server for the committed offset and adjusts the buffer window before resuming chunk transmission.
1 parent 0925229 commit f9ca3dc

5 files changed

Lines changed: 326 additions & 30 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
import com.google.api.core.InternalApi;
3838
import com.google.api.gax.resumable.ChunkUploadRequest;
3939
import com.google.api.gax.resumable.ChunkUploadResponse;
40+
import com.google.api.gax.resumable.QueryStatusRequest;
41+
import com.google.api.gax.resumable.QueryStatusResponse;
4042
import com.google.api.gax.resumable.ResumableUploadClient;
4143
import com.google.api.gax.resumable.ResumableUploadSession;
4244
import com.google.api.gax.retrying.ExponentialRetryAlgorithm;
@@ -74,6 +76,8 @@ public class ResumableUploadCallableImpl<RequestT, ResponseT>
7476
private final ClientContext clientContext;
7577
private final UnaryCallable<ChunkUploadRequest, ChunkUploadResponse<ResponseT>>
7678
retryingUploadChunkCallable;
79+
private final UnaryCallable<QueryStatusRequest, QueryStatusResponse<ResponseT>>
80+
retryingQueryCallable;
7781

7882
public ResumableUploadCallableImpl(
7983
ResumableUploadClient<RequestT, ResponseT> client,
@@ -86,6 +90,9 @@ public ResumableUploadCallableImpl(
8690
this.retryingUploadChunkCallable =
8791
createRetryingCallable(
8892
client.uploadChunkCallable(), ResumableUploadCommand.UPLOAD, clientContext);
93+
this.retryingQueryCallable =
94+
createRetryingCallable(
95+
client.queryStatusCallable(), ResumableUploadCommand.QUERY, clientContext);
8996
}
9097

9198
@Override
@@ -109,6 +116,7 @@ public ResumableUploadFuture<ResponseT> futureCall(
109116
return ResumableUploadFutureImpl.create(
110117
startFuture,
111118
retryingUploadChunkCallable,
119+
retryingQueryCallable,
112120
payload,
113121
effectiveSettings,
114122
clientContext.getDefaultCallContext());

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

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -209,24 +209,22 @@ private void onChunkUploaded(ChunkUploadResponse<ResponseT> response) {
209209
if (result.isDone()) {
210210
return;
211211
}
212-
try {
213-
if (response.getUploadStatus() == ResumableUploadStatus.FINAL) {
214-
result.set(response.getResponse());
215-
} else if (buffer.isFinal()) {
216-
throw new IllegalStateException(
217-
"Upload stream ended and final chunk was transmitted, but server returned incomplete"
218-
+ " status for upload URL: "
219-
+ uploadUrl);
220-
} else {
221-
long nextOffset = buffer.getBufferBaseOffset() + buffer.getPayloadLength();
222-
transmitChunk(nextOffset);
223-
}
224-
} catch (Throwable t) {
225-
result.setException(t);
212+
long nextOffset = buffer.getBufferBaseOffset() + buffer.getPayloadLength();
213+
if (response.getUploadStatus() == ResumableUploadStatus.FINAL) {
214+
result.set(response.getResponse());
215+
} else if (buffer.isFinal()) {
216+
result.setException(
217+
new IllegalStateException(
218+
"Upload stream ended and final chunk was transmitted, but server returned"
219+
+ " incomplete status for upload URL: "
220+
+ uploadUrl));
221+
} else {
222+
chunkExecutor.execute(() -> transmitChunk(nextOffset));
226223
}
227224
}
228225

229226
private ChunkUploadRequest buildCurrentChunkRequest() {
227+
// Determine if this is the final chunk and build the chunk request.
230228
return ChunkUploadRequest.newBuilder()
231229
.setUploadUrl(uploadUrl)
232230
.setPayload(buffer.getBuffer())
@@ -236,21 +234,20 @@ private ChunkUploadRequest buildCurrentChunkRequest() {
236234
.build();
237235
}
238236

239-
private static FailedPreconditionException protocolViolation(String message) {
240-
return new FailedPreconditionException(
241-
message,
242-
null,
243-
new StatusCode() {
244-
@Override
245-
public StatusCode.Code getCode() {
246-
return StatusCode.Code.FAILED_PRECONDITION;
247-
}
237+
private static final StatusCode FAILED_PRECONDITION_STATUS_CODE =
238+
new StatusCode() {
239+
@Override
240+
public StatusCode.Code getCode() {
241+
return StatusCode.Code.FAILED_PRECONDITION;
242+
}
248243

249-
@Override
250-
public @Nullable Object getTransportCode() {
251-
return null;
252-
}
253-
},
254-
false);
244+
@Override
245+
public @Nullable Object getTransportCode() {
246+
return null;
247+
}
248+
};
249+
250+
private static FailedPreconditionException protocolViolation(String message) {
251+
return new FailedPreconditionException(message, null, FAILED_PRECONDITION_STATUS_CODE, false);
255252
}
256253
}

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
import com.google.api.core.SettableApiFuture;
3939
import com.google.api.gax.resumable.ChunkUploadRequest;
4040
import com.google.api.gax.resumable.ChunkUploadResponse;
41+
import com.google.api.gax.resumable.QueryStatusRequest;
42+
import com.google.api.gax.resumable.QueryStatusResponse;
4143
import com.google.api.gax.resumable.ResumableUploadSession;
4244
import com.google.common.util.concurrent.MoreExecutors;
4345
import com.google.errorprone.annotations.concurrent.GuardedBy;
@@ -65,6 +67,8 @@ final class ResumableUploadFutureImpl<ResponseT> implements ResumableUploadFutur
6567
private final ApiFuture<ResumableUploadSession> startFuture;
6668
private final UnaryCallable<ChunkUploadRequest, ChunkUploadResponse<ResponseT>>
6769
uploadChunkCallable;
70+
private final UnaryCallable<QueryStatusRequest, QueryStatusResponse<ResponseT>>
71+
queryStatusCallable;
6872
private final InputStream payload;
6973
private final ResumableUploadCallSettings settings;
7074
private final ApiCallContext callContext;
@@ -85,12 +89,18 @@ final class ResumableUploadFutureImpl<ResponseT> implements ResumableUploadFutur
8589
static <ResponseT> ResumableUploadFutureImpl<ResponseT> create(
8690
ApiFuture<ResumableUploadSession> startFuture,
8791
UnaryCallable<ChunkUploadRequest, ChunkUploadResponse<ResponseT>> uploadChunkCallable,
92+
UnaryCallable<QueryStatusRequest, QueryStatusResponse<ResponseT>> queryStatusCallable,
8893
InputStream payload,
8994
ResumableUploadCallSettings settings,
9095
ApiCallContext callContext) {
9196
ResumableUploadFutureImpl<ResponseT> future =
9297
new ResumableUploadFutureImpl<>(
93-
startFuture, uploadChunkCallable, payload, settings, callContext);
98+
startFuture,
99+
uploadChunkCallable,
100+
queryStatusCallable,
101+
payload,
102+
settings,
103+
callContext);
94104
try {
95105
future.start();
96106
} catch (Throwable t) {
@@ -102,12 +112,15 @@ static <ResponseT> ResumableUploadFutureImpl<ResponseT> create(
102112
private ResumableUploadFutureImpl(
103113
ApiFuture<ResumableUploadSession> startFuture,
104114
UnaryCallable<ChunkUploadRequest, ChunkUploadResponse<ResponseT>> uploadChunkCallable,
115+
UnaryCallable<QueryStatusRequest, QueryStatusResponse<ResponseT>> queryStatusCallable,
105116
InputStream payload,
106117
ResumableUploadCallSettings settings,
107118
ApiCallContext callContext) {
108119
this.startFuture = checkNotNull(startFuture, "startFuture must not be null");
109120
this.uploadChunkCallable =
110121
checkNotNull(uploadChunkCallable, "uploadChunkCallable must not be null");
122+
this.queryStatusCallable =
123+
checkNotNull(queryStatusCallable, "queryStatusCallable must not be null");
111124
this.payload = checkNotNull(payload, "payload must not be null");
112125
this.settings = checkNotNull(settings, "settings must not be null");
113126
checkArgument(settings.getChunkSize() > 0, "chunkSize must be > 0");
@@ -128,6 +141,7 @@ public void onSuccess(ResumableUploadSession session) {
128141
ResumableUploadChunkCoordinator<ResponseT> coordinator =
129142
new ResumableUploadChunkCoordinator<>(
130143
uploadChunkCallable,
144+
queryStatusCallable,
131145
uploadSessionUrl,
132146
payload,
133147
settings.getChunkSize(),

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ void testResumableUploadCallable() {
216216
mock(ResumableUploadClient.class, Mockito.withSettings().withoutAnnotations());
217217
when(uploadClient.uploadChunkCallable())
218218
.thenReturn(mock(UnaryCallable.class, Mockito.withSettings().withoutAnnotations()));
219+
when(uploadClient.queryStatusCallable())
220+
.thenReturn(mock(UnaryCallable.class, Mockito.withSettings().withoutAnnotations()));
219221
ResumableUploadCallSettings settings =
220222
ResumableUploadCallSettings.newBuilder().setChunkSize(1024).build();
221223

0 commit comments

Comments
 (0)