Skip to content

feat(storage): object-level checksum in gRPC bidi read - #6131

Open
xlai20 wants to merge 13 commits into
googleapis:mainfrom
xlai20:gRPC-bidi-read-object-checksum-branch
Open

feat(storage): object-level checksum in gRPC bidi read#6131
xlai20 wants to merge 13 commits into
googleapis:mainfrom
xlai20:gRPC-bidi-read-object-checksum-branch

Conversation

@xlai20

@xlai20 xlai20 commented Jul 22, 2026

Copy link
Copy Markdown
Member

Description

This PR introduces object-level checksum validation (CRC32C and MD5) for gRPC bidirectional streaming reads (open_object).

go/rust-sdk-grpc-bidi-object-checksum, Issue #6056

Key Changes:

  • Checksum Validation Options: Added compute_crc32c(bool) and compute_md5() configuration toggles to open_object. CRC32C is enabled by default.
  • Resilient Error Handling: Validating checksums on the foreground reader prevents a mismatch from tearing down the underlying gRPC connection for other multiplexed streams. Additionally, streams now gracefully exhaust themselves on error to prevent infinite polling loops.
  • Testing: Added comprehensive unit and integration tests, including verification that content-encoding: gzip objects successfully validate their CRC32C hashes over gRPC without false mismatches.

@product-auto-label product-auto-label Bot added the api: storage Issues related to the Cloud Storage API. label Jul 22, 2026

@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 implements checksum verification (CRC32C and MD5) for the gRPC bidirectional read stream in Google Cloud Storage. It updates RangeReader to track offsets, update checksums, and validate them upon stream completion, while bypassing validation for ranged reads. It also adds builder methods compute_md5 and compute_crc32c to OpenObject to control checksum behavior, along with comprehensive unit and integration tests. Feedback from the review suggests marking the reader as exhausted when an error is encountered to prevent misleading checksum mismatches on subsequent calls, and refactoring compute_crc32c to return early and avoid an unnecessary else block in accordance with the repository style guide.

Comment thread src/storage/src/storage/bidi/range_reader.rs Outdated
Comment thread src/storage/src/storage/open_object.rs
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.58%. Comparing base (e50b169) to head (e01bbb2).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6131      +/-   ##
==========================================
+ Coverage   96.57%   96.58%   +0.01%     
==========================================
  Files         264      264              
  Lines       66496    66689     +193     
==========================================
+ Hits        64216    64410     +194     
+ Misses       2280     2279       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

xlai20 added 4 commits July 24, 2026 04:32
- Mark reader as exhausted on error to prevent misleading checksum mismatches

- Refactor compute_crc32c for early return to follow style guide
@xlai20
xlai20 marked this pull request as ready for review July 24, 2026 09:15
@xlai20
xlai20 requested review from a team as code owners July 24, 2026 09:15

@joshuatants joshuatants 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.

Can this PR be split s.t. that are at most 200 new LOC per PR?

Comment thread src/storage/src/storage/open_object.rs Outdated
Comment thread src/storage/src/storage/open_object.rs
Comment thread src/storage/src/storage/bidi/transport.rs Outdated
Comment thread src/storage/src/storage/bidi/transport.rs Outdated
@xlai20

xlai20 commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

Can this PR be split s.t. that are at most 200 new LOC per PR?

It is not a good idea to to split this PR because the PR is doing 1 single thing (i.e., adding object-level checksum for gRPC read), and also, most of the changes are tests and comments.

Below is the stats:

  1. Production Code (~130 lines total)
    • Executable Code: 70 lines
    • Comments & RustDocs: 54 lines
    • Blank Lines: 6 lines
  2. Test Code (~361 lines total)

As you can see, the actual logic added to open_object.rs, range_reader.rs, etc. is merely 70 lines of code.

@xlai20
xlai20 requested a review from joshuatants July 27, 2026 07:22
@joshuatants

joshuatants commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Can this PR be split s.t. that are at most 200 new LOC per PR?

It is not a good idea to to split this PR because the PR is doing 1 single thing (i.e., adding object-level checksum for gRPC read), and also, most of the changes are tests and comments.

Below is the stats:

  1. Production Code (~130 lines total)

    • Executable Code: 70 lines
    • Comments & RustDocs: 54 lines
    • Blank Lines: 6 lines
  2. Test Code (~361 lines total)

As you can see, the actual logic added to open_object.rs, range_reader.rs, etc. is merely 70 lines of code.

I review tests too. Perhaps for PRs like these we can have the logic mainly in 1 PR and tests in another.

Let's do that for future PRs. I reviewed the ones in this one already.

// Note: We do not skip checksums for gzip-encoded objects (decompressive transcoding)
// because gRPC automatic object decompression is not supported by the server.
// The client receives the raw compressed bytes which match the stored checksum.
let checksum = if matches!(range, RequestedRange::Offset(0)) {

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.

If the user makes a range call that results in RequestedRange::Segment { 0, 0 }, this will still return the whole object but the checksum won't be calculated.

A more robust detection method is to either convert the RequestadRange into the actual offset and length used in the RPC call (I think this function already exists somewhere in the code), or handle all the possible RequestedRange cases.


/// Enables computation of MD5 checksums.
///
/// By default, MD5 checksums are disabled, as they are not supported by the GCS gRPC API.

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.

If they're not supported by gRPC, and open-object is gRPC, then why offer this setter?

async fn next(&mut self) -> Option<crate::Result<bytes::Bytes>> {
let msg = self.inner.recv().await?;
Some(msg.map_err(Error::io))
if self.exhausted {

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.

Let's take this opportunity to improve the readability of the code. Let's add:

  1. a docstring to the function
  2. comments at suitable locations, e.g. logical blocks, to explain what the code does. For example, what would be helpful here is a comment explaining what each match case handles

}

#[tokio::test]
async fn object_with_checksum_bypassed_due_to_ranged_read() -> anyhow::Result<()> {

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.

Is it possible to have the test enable a checksum and then perform a ranged read to verify that no checksum is calculated?

Right now, what the test verifies is that if Checksum::default is passed in, then no validation is performed. Which is a valuable test, but not what the name suggests.

Ok(())
}

#[tokio::test]

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.

Doesn't need to be async

}
None => {
self.exhausted = true;
if let Some(expected) = &self.object.checksums {

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.

To double check: if it's possible for object.checksums to be None, then I think that in that case we should skip calling self.checksum.update in order to save computation

Comment on lines +844 to +846
got_err = true;
let fmt = format!("{e:?}");
assert!(fmt.contains("ChecksumMismatch"), "{fmt}");

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.

Instead of checking the error string, can the error itself be checked like what is done in object_with_checksum_mismatch?

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

Labels

api: storage Issues related to the Cloud Storage API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants