feat(storage): object-level checksum in gRPC bidi read - #6131
Conversation
There was a problem hiding this comment.
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.
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
- Mark reader as exhausted on error to prevent misleading checksum mismatches - Refactor compute_crc32c for early return to follow style guide
…ct-checksum-branch
joshuatants
left a comment
There was a problem hiding this comment.
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:
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)) { |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Let's take this opportunity to improve the readability of the code. Let's add:
- a docstring to the function
- 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<()> { |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
Doesn't need to be async
| } | ||
| None => { | ||
| self.exhausted = true; | ||
| if let Some(expected) = &self.object.checksums { |
There was a problem hiding this comment.
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
| got_err = true; | ||
| let fmt = format!("{e:?}"); | ||
| assert!(fmt.contains("ChecksumMismatch"), "{fmt}"); |
There was a problem hiding this comment.
Instead of checking the error string, can the error itself be checked like what is done in object_with_checksum_mismatch?
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:
compute_crc32c(bool)andcompute_md5()configuration toggles toopen_object. CRC32C is enabled by default.content-encoding: gzipobjects successfully validate their CRC32C hashes over gRPC without false mismatches.