Skip to content

feat(gax): add ResumableUploadClient and supporting classes - #14072

Draft
whowes wants to merge 1 commit into
mainfrom
whowes/resumable-upload-client
Draft

feat(gax): add ResumableUploadClient and supporting classes#14072
whowes wants to merge 1 commit into
mainfrom
whowes/resumable-upload-client

Conversation

@whowes

@whowes whowes commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a suite of low-level classes and interfaces to support resumable uploads in the com.google.api.gax.resumableupload package, including request/response models, session metadata, progress listeners, and a client interface, along with corresponding unit tests. The review feedback focuses on improving robustness and safety by adding input validation (such as ensuring non-negative offsets and valid total lengths) in the factory methods of the request, response, and status models, as well as defensively copying query parameters in StartUploadRequest to guarantee immutability.

@whowes
whowes force-pushed the whowes/resumable-upload-client branch from 8d9f27f to 24d585f Compare August 13, 2026 22:04
@whowes
whowes force-pushed the whowes/resumable-upload-client branch from 24d585f to 1ee141c Compare August 13, 2026 22:38
@whowes

whowes commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a suite of low-level classes and interfaces to support resumable uploads, including request/response models for starting uploads, transmitting chunks, querying status, and tracking progress, along with associated unit tests. The review feedback suggests enhancing data integrity by adding stricter validation checks to enforce protocol invariants in ChunkUploadRequest and ResumableUploadStatus (such as verifying that offsets and uploaded bytes do not exceed the total payload length), and updating the test suite to cover these new validation scenarios.

Comment on lines +59 to +64
public static ChunkUploadRequest create(
String uploadUrl, ByteString payload, long offset, long totalLength, boolean isFinal) {
Preconditions.checkArgument(offset >= 0, "offset must be non-negative");
Preconditions.checkArgument(totalLength >= -1, "totalLength must be >= -1");
return new AutoValue_ChunkUploadRequest(uploadUrl, payload, offset, totalLength, isFinal);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent invalid states, we should enforce that the chunk offset and payload size do not exceed the total length when the total length is known. Additionally, if this is the final chunk, the end offset must exactly equal the total length. Please ensure these validation steps are not redundant with checks already performed by upstream callers.

  public static ChunkUploadRequest create(
      String uploadUrl, ByteString payload, long offset, long totalLength, boolean isFinal) {
    Preconditions.checkArgument(offset >= 0, "offset must be non-negative");
    Preconditions.checkArgument(totalLength >= -1, "totalLength must be >= -1");
    if (totalLength != -1) {
      Preconditions.checkArgument(
          offset + payload.size() <= totalLength,
          "offset + payload size (%s) cannot exceed totalLength (%s)",
          offset + payload.size(),
          totalLength);
      if (isFinal) {
        Preconditions.checkArgument(
            offset + payload.size() == totalLength,
            "final chunk end offset (%s) must equal totalLength (%s)",
            offset + payload.size(),
            totalLength);
      }
    }
    return new AutoValue_ChunkUploadRequest(uploadUrl, payload, offset, totalLength, isFinal);
  }
References
  1. When implementing property parsing or validation logic, ensure that null checks and validation steps are not redundant with checks already performed by upstream callers or preceding logic in the same method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is more appropriately handled and tested on the caller level (the state machine)

Comment on lines +82 to +87
public static ResumableUploadStatus create(
String uploadUrl, long bytesUploaded, long totalBytes, State state) {
Preconditions.checkArgument(bytesUploaded >= 0, "bytesUploaded must be non-negative");
Preconditions.checkArgument(totalBytes >= -1, "totalBytes must be >= -1");
return new AutoValue_ResumableUploadStatus(uploadUrl, bytesUploaded, totalBytes, state);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure data integrity, we should validate that the number of bytes uploaded does not exceed the total bytes when the total bytes are known. Furthermore, when the upload state is finalized, the bytes uploaded must equal the total bytes. Please ensure these validation steps are not redundant with checks already performed by upstream callers.

  public static ResumableUploadStatus create(
      String uploadUrl, long bytesUploaded, long totalBytes, State state) {
    Preconditions.checkArgument(bytesUploaded >= 0, "bytesUploaded must be non-negative");
    Preconditions.checkArgument(totalBytes >= -1, "totalBytes must be >= -1");
    if (totalBytes != -1) {
      Preconditions.checkArgument(
          bytesUploaded <= totalBytes,
          "bytesUploaded (%s) cannot exceed totalBytes (%s)",
          bytesUploaded,
          totalBytes);
      if (state == State.FINALIZED) {
        Preconditions.checkArgument(
            bytesUploaded == totalBytes,
            "bytesUploaded (%s) must equal totalBytes (%s) when finalized",
            bytesUploaded,
            totalBytes);
      }
    }
    return new AutoValue_ResumableUploadStatus(uploadUrl, bytesUploaded, totalBytes, state);
  }
References
  1. When implementing property parsing or validation logic, ensure that null checks and validation steps are not redundant with checks already performed by upstream callers or preceding logic in the same method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See related comment

Comment on lines +59 to +74
void chunkUploadRequest_enforcesProtocolInvariants() {
// Valid: offset >= 0, totalLength == -1 (streaming/unknown) or >= 0 (known)
ChunkUploadRequest streamingRequest =
ChunkUploadRequest.create(UPLOAD_URL, ByteString.copyFromUtf8("data"), 0, -1, false);
assertThat(streamingRequest.getTotalLength()).isEqualTo(-1);

// Invalid: negative offset
assertThrows(
IllegalArgumentException.class,
() -> ChunkUploadRequest.create(UPLOAD_URL, ByteString.EMPTY, -1, 100, false));

// Invalid: totalLength < -1
assertThrows(
IllegalArgumentException.class,
() -> ChunkUploadRequest.create(UPLOAD_URL, ByteString.EMPTY, 0, -2, false));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the test to verify the new chunk upload request invariants (exceeding total length and final chunk offset matching total length). Before adding these new test cases, please verify the existing test suite to ensure these scenarios are not already covered.

  void chunkUploadRequest_enforcesProtocolInvariants() {
    // Valid: offset >= 0, totalLength == -1 (streaming/unknown) or >= 0 (known)
    ChunkUploadRequest streamingRequest =
        ChunkUploadRequest.create(UPLOAD_URL, ByteString.copyFromUtf8("data"), 0, -1, false);
    assertThat(streamingRequest.getTotalLength()).isEqualTo(-1);

    // Invalid: negative offset
    assertThrows(
        IllegalArgumentException.class,
        () -> ChunkUploadRequest.create(UPLOAD_URL, ByteString.EMPTY, -1, 100, false));

    // Invalid: totalLength < -1
    assertThrows(
        IllegalArgumentException.class,
        () -> ChunkUploadRequest.create(UPLOAD_URL, ByteString.EMPTY, 0, -2, false));

    // Invalid: offset + payload size exceeds totalLength
    assertThrows(
        IllegalArgumentException.class,
        () -> ChunkUploadRequest.create(UPLOAD_URL, ByteString.copyFromUtf8("data"), 90, 92, false));

    // Invalid: final chunk end offset does not equal totalLength
    assertThrows(
        IllegalArgumentException.class,
        () -> ChunkUploadRequest.create(UPLOAD_URL, ByteString.copyFromUtf8("data"), 0, 10, true));
  }
References
  1. Before adding new test cases for input validation (such as non-numeric values or unrecognized properties), verify the existing test suite to ensure these scenarios are not already covered.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See related comment

Comment on lines +109 to +120
@Test
void progressListener_dispatchesStatusSnapshot() {
AtomicReference<ResumableUploadStatus> capturedStatus = new AtomicReference<>();
ResumableUploadProgressListener listener = capturedStatus::set;

ResumableUploadStatus status =
ResumableUploadStatus.create(
UPLOAD_URL, 1024L, 2048L, ResumableUploadStatus.State.UPLOADING);
listener.onProgress(status);

assertThat(capturedStatus.get()).isEqualTo(status);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a test case to verify the new resumable upload status invariants (bytes uploaded exceeding total bytes, and finalized state matching total bytes). Before adding these new test cases, please verify the existing test suite to ensure these scenarios are not already covered.

  @Test
  void progressListener_dispatchesStatusSnapshot() {
    AtomicReference<ResumableUploadStatus> capturedStatus = new AtomicReference<>();
    ResumableUploadProgressListener listener = capturedStatus::set;

    ResumableUploadStatus status =
        ResumableUploadStatus.create(
            UPLOAD_URL, 1024L, 2048L, ResumableUploadStatus.State.UPLOADING);
    listener.onProgress(status);

    assertThat(capturedStatus.get()).isEqualTo(status);
  }

  @Test
  void resumableUploadStatus_enforcesInvariants() {
    // Invalid: bytesUploaded exceeds totalBytes
    assertThrows(
        IllegalArgumentException.class,
        () -> ResumableUploadStatus.create(UPLOAD_URL, 101L, 100L, ResumableUploadStatus.State.UPLOADING));

    // Invalid: finalized state but bytesUploaded does not equal totalBytes
    assertThrows(
        IllegalArgumentException.class,
        () -> ResumableUploadStatus.create(UPLOAD_URL, 50L, 100L, ResumableUploadStatus.State.FINALIZED));
  }
References
  1. Before adding new test cases for input validation (such as non-numeric values or unrecognized properties), verify the existing test suite to ensure these scenarios are not already covered.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See related comment

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed for 'gapic-generator-java-root'

Failed conditions
77.6% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed for 'gapic-generator-java-root'

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant