Skip to content

Commit 43ec12a

Browse files
committed
feat(gax): align resumable upload chunk size to server granularity and surface effective chunk size
1 parent e83c768 commit 43ec12a

12 files changed

Lines changed: 159 additions & 40 deletions

File tree

‎sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ResumableUploadStartCallable.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ static <RequestT> UnaryCallable<RequestT, ResumableUploadSession> create(
108108
/** A listener that parses HTTP response headers to produce a {@link ResumableUploadSession}. */
109109
private static class StartUploadResponseListener extends HttpJsonClientCall.Listener<String> {
110110
private final ResumableUploadHttpJsonFuture<ResumableUploadSession> future;
111-
private long chunkGranularity = 1L;
111+
private int chunkGranularity = 1;
112112
@Nullable private String uploadUrl;
113113
@Nullable private Throwable headerParsingException;
114114

@@ -132,7 +132,7 @@ public void onHeaders(HttpJsonMetadata responseHeaders) {
132132
}
133133

134134
try {
135-
long parsed = Long.parseLong(granularityStr);
135+
int parsed = Integer.parseInt(granularityStr);
136136
if (parsed <= 0) {
137137
this.headerParsingException =
138138
ApiExceptionFactory.createException(

‎sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonResumableUploadClientTest.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ void startUpload_validHeaders_returnsSession() {
105105
ResumableUploadSession session = client.startUploadCallable().call(request);
106106

107107
assertThat(session.getUploadUrl()).isEqualTo(TEST_UPLOAD_URL);
108-
assertThat(session.getChunkGranularity()).isEqualTo(262144L);
108+
assertThat(session.getChunkGranularity()).isEqualTo(262144);
109109
}
110110

111111
@Test
@@ -123,7 +123,7 @@ void startUpload_caseInsensitiveHeaders_returnsSession() {
123123

124124
assertThat(session.getUploadUrl())
125125
.isEqualTo("https://test.googleapis.com/upload/session/case-insensitive");
126-
assertThat(session.getChunkGranularity()).isEqualTo(524288L);
126+
assertThat(session.getChunkGranularity()).isEqualTo(524288);
127127
}
128128

129129
@Test

‎sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/resumable/ResumableUploadSession.java‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
@AutoValue
4242
public abstract class ResumableUploadSession {
4343

44-
private static final long DEFAULT_CHUNK_GRANULARITY = 1L;
44+
private static final int DEFAULT_CHUNK_GRANULARITY = 1;
4545

4646
/** Returns the server-provided URL to which data uploads are directed. */
4747
public abstract String getUploadUrl();
@@ -56,7 +56,7 @@ public abstract class ResumableUploadSession {
5656
*
5757
* @return the chunk granularity in bytes
5858
*/
59-
public abstract long getChunkGranularity();
59+
public abstract int getChunkGranularity();
6060

6161
public abstract Builder toBuilder();
6262

@@ -69,7 +69,7 @@ public static Builder newBuilder() {
6969
public abstract static class Builder {
7070
public abstract Builder setUploadUrl(String uploadUrl);
7171

72-
public abstract Builder setChunkGranularity(long chunkGranularity);
72+
public abstract Builder setChunkGranularity(int chunkGranularity);
7373

7474
public abstract ResumableUploadSession build();
7575
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
6868
private final UnaryCallable<QueryStatusRequest, QueryStatusResponse<ResponseT>>
6969
queryStatusCallable;
7070
private final String uploadUrl;
71+
private final int chunkSize;
7172
private final RewindableStreamBuffer buffer;
7273
private final ApiCallContext callContext;
7374
private final ExponentialRetryAlgorithm recoveryAlgorithm;
@@ -93,6 +94,7 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
9394
this.queryStatusCallable =
9495
checkNotNull(queryStatusCallable, "queryStatusCallable must not be null");
9596
this.uploadUrl = checkNotNull(uploadUrl, "uploadUrl must not be null");
97+
this.chunkSize = chunkSize;
9698
checkNotNull(payload, "payload must not be null");
9799
this.callContext = checkNotNull(callContext, "callContext must not be null");
98100
this.recoveryAlgorithm = checkNotNull(recoveryAlgorithm, "recoveryAlgorithm must not be null");
@@ -107,7 +109,7 @@ ApiFuture<ResponseT> getFuture() {
107109
}
108110

109111
void start() {
110-
progressTracker.onStarted(uploadUrl);
112+
progressTracker.onStarted(uploadUrl, chunkSize);
111113
uploadResultFuture.addListener(
112114
() -> {
113115
Future<?> inFlight = inFlightFuture;

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

Lines changed: 12 additions & 1 deletion
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 com.google.api.core.InternalExtensionOnly;
3435
import org.jspecify.annotations.NullMarked;
3536
import org.jspecify.annotations.Nullable;
3637

@@ -43,9 +44,19 @@
4344
* @param <ResponseT> the type of the final response message returned once the upload completes
4445
*/
4546
@BetaApi
47+
@InternalExtensionOnly
4648
@NullMarked
4749
public interface ResumableUploadFuture<ResponseT> extends ApiFuture<ResponseT> {
4850

49-
/** Returns the upload session URL, or {@code null} if session initiation is in progress. */
51+
/**
52+
* Returns the upload session URL, or {@code null} if session initiation is in progress or did not
53+
* complete successfully.
54+
*/
5055
@Nullable String getUploadSessionUrl();
56+
57+
/**
58+
* Returns the effective chunk size in bytes aligned to the server's chunk granularity, or {@code
59+
* null} if session initiation is in progress or did not complete successfully.
60+
*/
61+
@Nullable Integer getEffectiveChunkSize();
5162
}

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

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,12 @@ final class ResumableUploadFutureImpl<ResponseT> implements ResumableUploadFutur
8585
private final SettableApiFuture<ResponseT> resultFuture = SettableApiFuture.create();
8686

8787
private volatile @Nullable InputStream payload;
88-
private volatile @Nullable String uploadSessionUrl;
88+
89+
@GuardedBy("lock")
90+
private @Nullable String uploadSessionUrl;
91+
92+
@GuardedBy("lock")
93+
private @Nullable Integer effectiveChunkSize;
8994

9095
// Tracks the current operation's Future (start, chunk upload) to propagate cancellation, or
9196
// null once a terminal transition (succeed, fail, cancel) has claimed completion.
@@ -161,13 +166,20 @@ private void start(InputStreamSupplier payloadSupplier) throws IOException {
161166
@Override
162167
public void onSuccess(ResumableUploadSession session) {
163168
String sessionUrl = session.getUploadUrl();
169+
int chunkSize;
170+
try {
171+
chunkSize = alignChunkSize(options.getChunkSize(), session.getChunkGranularity());
172+
} catch (Throwable t) {
173+
fail(t);
174+
return;
175+
}
164176
ResumableUploadChunkCoordinator<ResponseT> coordinator =
165177
new ResumableUploadChunkCoordinator<>(
166178
uploadChunkCallable,
167179
queryStatusCallable,
168180
sessionUrl,
169181
payload,
170-
options.getChunkSize(),
182+
chunkSize,
171183
callContext,
172184
recoveryAlgorithm,
173185
executor,
@@ -177,6 +189,7 @@ public void onSuccess(ResumableUploadSession session) {
177189
if (inFlightFuture == null) {
178190
return;
179191
}
192+
effectiveChunkSize = chunkSize;
180193
uploadSessionUrl = sessionUrl;
181194
inFlightFuture = uploadFuture;
182195
}
@@ -213,7 +226,7 @@ public void onFailure(Throwable t) {
213226

214227
private void onTimeout() {
215228
String message;
216-
if (uploadSessionUrl != null) {
229+
if (getUploadSessionUrl() != null) {
217230
message = "Resumable upload timed out";
218231
} else {
219232
message = "Resumable upload timed out before session initiation completed";
@@ -251,14 +264,22 @@ private void fail(Throwable t) {
251264
}
252265

253266
private ApiException toTerminalException(Throwable t) {
254-
if (t instanceof ApiException && uploadSessionUrl == null) {
267+
String sessionUrl;
268+
Integer chunkSize;
269+
synchronized (lock) {
270+
sessionUrl = uploadSessionUrl;
271+
chunkSize = effectiveChunkSize;
272+
}
273+
if (t instanceof ApiException && sessionUrl == null) {
255274
return (ApiException) t;
256275
}
257276
String message = firstNonNull(t.getMessage(), t.getClass().getSimpleName());
258-
if (uploadSessionUrl != null) {
259-
message = message + " (upload URL: " + uploadSessionUrl + ")";
277+
if (sessionUrl != null) {
278+
message =
279+
String.format(
280+
"%s (upload URL: %s, effective chunk size: %d)", message, sessionUrl, chunkSize);
260281
}
261-
// Rethrow with URL included in message.
282+
// Rethrow with URL and effective chunk size included in message.
262283
if (t instanceof ApiException) {
263284
ApiException apiException = (ApiException) t;
264285
return ApiExceptionFactory.createException(
@@ -272,6 +293,16 @@ private ApiException toTerminalException(Throwable t) {
272293
message, t, statusCodeOf(StatusCode.Code.UNKNOWN), false);
273294
}
274295

296+
static int alignChunkSize(int configuredChunkSize, int chunkGranularity) {
297+
checkArgument(configuredChunkSize > 0, "configuredChunkSize must be > 0");
298+
checkArgument(chunkGranularity > 0, "chunkGranularity must be > 0");
299+
if (configuredChunkSize <= chunkGranularity) {
300+
return chunkGranularity;
301+
}
302+
int unalignedRemainder = configuredChunkSize % chunkGranularity;
303+
return configuredChunkSize - unalignedRemainder;
304+
}
305+
275306
private void closePayload(@Nullable Throwable primaryException) {
276307
if (payload == null) {
277308
return;
@@ -287,7 +318,16 @@ private void closePayload(@Nullable Throwable primaryException) {
287318

288319
@Override
289320
public @Nullable String getUploadSessionUrl() {
290-
return uploadSessionUrl;
321+
synchronized (lock) {
322+
return uploadSessionUrl;
323+
}
324+
}
325+
326+
@Override
327+
public @Nullable Integer getEffectiveChunkSize() {
328+
synchronized (lock) {
329+
return effectiveChunkSize;
330+
}
291331
}
292332

293333
@Override

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,9 @@ public static Builder newBuilder() {
9797
public abstract static class Builder {
9898
/**
9999
* Sets the chunk size in bytes. The payload is sent in chunks of this size, and each chunk is
100-
* buffered in memory so it can be resent if the upload needs to recover. Must be positive;
101-
* defaults to 8 MB.
100+
* buffered in memory so it can be resent if the upload needs to recover. If the server
101+
* specifies a chunk granularity, the effective chunk size is rounded down to a multiple of that
102+
* granularity (or raised to the granularity if smaller). Must be positive; defaults to 8 MB.
102103
*/
103104
public abstract Builder setChunkSize(int chunkSize);
104105

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ public abstract class ResumableUploadProgress {
5555
*/
5656
public abstract @Nullable String getUploadUrl();
5757

58+
/**
59+
* Returns the effective chunk size in bytes aligned to the server's chunk granularity, or {@code
60+
* null} if session initiation is pending.
61+
*/
62+
public abstract @Nullable Integer getEffectiveChunkSize();
63+
5864
/** Returns the number of bytes confirmed as uploaded to the server so far. */
5965
public abstract long getBytesUploaded();
6066

@@ -87,6 +93,8 @@ static Builder newBuilder() {
8793
abstract static class Builder {
8894
abstract Builder setUploadUrl(@Nullable String uploadUrl);
8995

96+
abstract Builder setEffectiveChunkSize(@Nullable Integer effectiveChunkSize);
97+
9098
abstract Builder setBytesUploaded(long bytesUploaded);
9199

92100
abstract Builder setState(String state);

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,14 @@ ResumableUploadProgress getProgress() {
116116
}
117117
}
118118

119-
void onStarted(String uploadUrl) {
119+
void onStarted(String uploadUrl, int effectiveChunkSize) {
120120
checkNotNull(uploadUrl, "uploadUrl must not be null");
121121
transition(
122-
builder -> builder.setState(ResumableUploadProgress.STATE_STARTED).setUploadUrl(uploadUrl));
122+
builder ->
123+
builder
124+
.setState(ResumableUploadProgress.STATE_STARTED)
125+
.setUploadUrl(uploadUrl)
126+
.setEffectiveChunkSize(effectiveChunkSize));
123127
}
124128

125129
void onChunkUploaded(long bytesUploaded) {

‎sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/resumable/ResumableUploadSessionTest.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ void build_withUploadUrl_setsDefaultChunkGranularity() {
4343
ResumableUploadSession.newBuilder().setUploadUrl(UPLOAD_URL).build();
4444

4545
assertThat(session.getUploadUrl()).isEqualTo(UPLOAD_URL);
46-
assertThat(session.getChunkGranularity()).isEqualTo(1L);
46+
assertThat(session.getChunkGranularity()).isEqualTo(1);
4747
}
4848

4949
@Test
5050
void build_withExplicitChunkGranularity_preservesGranularity() {
51-
long customChunkGranularity = 256 * 1024L;
51+
int customChunkGranularity = 256 * 1024;
5252

5353
ResumableUploadSession session =
5454
ResumableUploadSession.newBuilder()

0 commit comments

Comments
 (0)