Skip to content

Commit e8cbd42

Browse files
authored
feat(gax): add ApiCallContext and request-level settings overloads to ResumableUploadCallable (#14251)
This is needed to meet the resumable upload requirement to allow custom headers and other settings to be applied on a per-call basis.
1 parent be3d80d commit e8cbd42

3 files changed

Lines changed: 112 additions & 6 deletions

File tree

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

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,41 @@ public abstract class ResumableUploadCallable<RequestT, ResponseT> {
4848
protected ResumableUploadCallable() {}
4949

5050
/**
51-
* Performs a new resumable upload asynchronously.
51+
* Performs a new resumable upload asynchronously with settings overrides and default call
52+
* context.
5253
*
5354
* <p>The provided {@code payload} stream is consumed asynchronously by the returned {@link
5455
* ResumableUploadFuture} and will be closed automatically upon completion, failure, or
5556
* cancellation.
5657
*
5758
* @param request the request message
5859
* @param payload the data payload input stream to upload and close
59-
* @param settings call settings overrides; may be {@code null}
60+
* @param settings request-level call settings overrides; may be {@code null}
61+
* @return future for tracking and controlling the upload
62+
*/
63+
public ResumableUploadFuture<ResponseT> futureCall(
64+
RequestT request, InputStream payload, @Nullable ResumableUploadCallSettings settings) {
65+
return futureCall(request, payload, null, settings);
66+
}
67+
68+
/**
69+
* Performs a new resumable upload asynchronously with call context and settings overrides.
70+
*
71+
* <p>The provided {@code payload} stream is consumed asynchronously by the returned {@link
72+
* ResumableUploadFuture} and will be closed automatically upon completion, failure, or
73+
* cancellation.
74+
*
75+
* @param request the request message
76+
* @param payload the data payload input stream to upload and close
77+
* @param context call context overrides; may be {@code null}
78+
* @param settings request-level call settings overrides; may be {@code null}
6079
* @return future for tracking and controlling the upload
6180
*/
6281
public abstract ResumableUploadFuture<ResponseT> futureCall(
63-
RequestT request, InputStream payload, @Nullable ResumableUploadCallSettings settings);
82+
RequestT request,
83+
InputStream payload,
84+
@Nullable ApiCallContext context,
85+
@Nullable ResumableUploadCallSettings settings);
6486

6587
/**
6688
* Resumes an existing resumable upload session asynchronously using a saved session URL.

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,18 @@ public ResumableUploadCallableImpl(
7070

7171
@Override
7272
public ResumableUploadFuture<ResponseT> futureCall(
73-
RequestT request, InputStream payload, @Nullable ResumableUploadCallSettings settings) {
73+
RequestT request,
74+
InputStream payload,
75+
@Nullable ApiCallContext context,
76+
@Nullable ResumableUploadCallSettings settings) {
7477
checkNotNull(request, "request must not be null");
7578
checkNotNull(payload, "payload must not be null");
7679
ResumableUploadCallSettings effectiveSettings = defaultCallSettings.merge(settings);
80+
ApiCallContext effectiveCallContext = clientContext.getDefaultCallContext().merge(context);
7781

7882
ApiFuture<ResumableUploadSession> startFuture;
7983
try {
80-
startFuture =
81-
client.startUploadCallable().futureCall(request, clientContext.getDefaultCallContext());
84+
startFuture = client.startUploadCallable().futureCall(request, effectiveCallContext);
8285
} catch (Throwable t) {
8386
startFuture = ApiFutures.immediateFailedFuture(t);
8487
}

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,87 @@ void testUploadCallable_closesPayloadOnStartSyncFailure() {
313313
assertThat(stream.closed).isTrue();
314314
}
315315

316+
@Test
317+
void testUploadCallable_withApiCallContext_mergesAndPassesContext() throws Exception {
318+
stubStartSession("https://upload.url/context");
319+
when(mockChunkCallable.futureCall(any(ChunkUploadRequest.class), any()))
320+
.thenReturn(ApiFutures.immediateFuture(ChunkUploadResponse.create(true, "done-ctx")));
321+
322+
ApiCallContext customContext =
323+
FakeCallContext.createDefault()
324+
.withExtraHeaders(
325+
java.util.Collections.singletonMap(
326+
"X-Custom", java.util.Collections.singletonList("val")));
327+
ResumableUploadFuture<String> future =
328+
callable.futureCall("resource-path", streamOf("data"), customContext, null);
329+
assertThat(future.get()).isEqualTo("done-ctx");
330+
331+
ArgumentCaptor<ApiCallContext> startContextCaptor =
332+
ArgumentCaptor.forClass(ApiCallContext.class);
333+
verify(mockStartCallable).futureCall(any(), startContextCaptor.capture());
334+
assertThat(startContextCaptor.getValue()).isNotNull();
335+
assertThat(((FakeCallContext) startContextCaptor.getValue()).getExtraHeaders())
336+
.containsKey("X-Custom");
337+
338+
ArgumentCaptor<ApiCallContext> chunkContextCaptor =
339+
ArgumentCaptor.forClass(ApiCallContext.class);
340+
verify(mockChunkCallable).futureCall(any(), chunkContextCaptor.capture());
341+
assertThat(chunkContextCaptor.getValue()).isNotNull();
342+
assertThat(((FakeCallContext) chunkContextCaptor.getValue()).getExtraHeaders())
343+
.doesNotContainKey("X-Custom");
344+
}
345+
346+
@Test
347+
void testUploadCallable_withSettings_mergesAndAppliesSettings() throws Exception {
348+
stubStartSession("https://upload.url/settings");
349+
when(mockChunkCallable.futureCall(any(ChunkUploadRequest.class), any()))
350+
.thenReturn(ApiFutures.immediateFuture(ChunkUploadResponse.create(true, "done-settings")));
351+
352+
ResumableUploadCallSettings customSettings =
353+
ResumableUploadCallSettings.newBuilder().setChunkSize(16).build();
354+
355+
ResumableUploadFuture<String> future =
356+
callable.futureCall("resource-path", streamOf("data"), null, customSettings);
357+
assertThat(future.get()).isEqualTo("done-settings");
358+
}
359+
360+
@Test
361+
void testUploadCallable_withSettings_delegatesWithNullContext() throws Exception {
362+
stubStartSession("https://upload.url/settings-convenience");
363+
when(mockChunkCallable.futureCall(any(ChunkUploadRequest.class), any()))
364+
.thenReturn(
365+
ApiFutures.immediateFuture(ChunkUploadResponse.create(true, "done-settings-conv")));
366+
367+
ResumableUploadCallSettings customSettings =
368+
ResumableUploadCallSettings.newBuilder().setChunkSize(16).build();
369+
370+
ResumableUploadFuture<String> future =
371+
callable.futureCall("resource-path", streamOf("data"), customSettings);
372+
assertThat(future.get()).isEqualTo("done-settings-conv");
373+
}
374+
375+
@Test
376+
void testUploadCallable_withContextAndSettings_appliesBoth() throws Exception {
377+
stubStartSession("https://upload.url/ctx-settings");
378+
when(mockChunkCallable.futureCall(any(ChunkUploadRequest.class), any()))
379+
.thenReturn(ApiFutures.immediateFuture(ChunkUploadResponse.create(true, "done-both")));
380+
381+
FakeCallContext customContext = FakeCallContext.createDefault();
382+
ResumableUploadCallSettings customSettings =
383+
ResumableUploadCallSettings.newBuilder().setChunkSize(16).build();
384+
385+
ResumableUploadFuture<String> future =
386+
callable.futureCall("resource-path", streamOf("data"), customContext, customSettings);
387+
assertThat(future.get()).isEqualTo("done-both");
388+
}
389+
390+
@Test
391+
void testResumeCall_throwsUnsupportedOperationException() {
392+
assertThrows(
393+
UnsupportedOperationException.class,
394+
() -> callable.resumeCall("https://upload.url/session", streamOf("data"), null));
395+
}
396+
316397
private void stubStartSession(String uploadUrl) {
317398
when(mockStartCallable.futureCall(any(), any()))
318399
.thenReturn(

0 commit comments

Comments
 (0)