Conversation
`ReadPayloadImpl::Accumulate()` replaced the accumulated payload with the
next one whenever the accumulated payload had no data, ignoring whether it
carried object metadata. For a 0-byte object the first payload has empty
contents but valid metadata, and the synthetic end-of-stream `ReadPayload{}`
then overwrote it, so `ReadAll()` and `AsyncClient::ReadObjectRange()`
returned `metadata() == std::nullopt`.
Only take the replace fast path when the accumulated payload has neither
data nor metadata. Add unit tests for `Accumulate()`, `ReadAll()`, and an
end-to-end `ReadObjectRange()` test with a mocked 0-byte gRPC stream.
There was a problem hiding this comment.
Code Review
This pull request ensures that object metadata is preserved when reading 0-byte objects or when an empty payload containing metadata is followed by data. It updates ReadPayloadImpl::Accumulate to only replace the left-hand side payload if it contains neither data nor metadata, and adds comprehensive unit tests to verify this behavior. Feedback on the changes suggests extending this safety check in Accumulate to also verify other fields like headers, offset, and hashes to prevent them from being discarded if they are present in an otherwise empty payload.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #16499 +/- ##
==========================================
- Coverage 92.37% 92.37% -0.01%
==========================================
Files 2262 2262
Lines 217100 217246 +146
==========================================
+ Hits 200540 200671 +131
- Misses 16560 16575 +15 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
kalragauri
left a comment
There was a problem hiding this comment.
nit: in the PR description, consider removing the notes about the first commit vs. the review update, since they won't make sense after the squash-merge.
Summary
storage::ReadAll(), and thereforeAsyncClient::ReadObjectRange(), dropped the object metadata (ReadPayload::metadata() == std::nullopt) when reading a 0-byte object. This happened even though the service sends the fullgoogle.storage.v2.Objectmetadata in the firstReadObjectResponse.Root cause
ReadPayloadImpl::Accumulate(lhs, rhs)had a fast path: "iflhshas no data, replacelhswithrhs". It only looked at the data, not the metadata or the other fields.For a 0-byte object,
ReadAll()receives:accumulated_is replaced by this payload, so it now holds the metadata.AsyncReader::Read()synthesizes asReadPayload{}(no data, no metadata). Becauseaccumulated_still has no data, the fast path fires again and overwrites the metadata.Objects with data are not affected: after the first chunk,
lhsis no longer empty, so later chunks only append data.Fix
The fast path now only replaces
lhswhen it is fully default-constructed, meaning it has no data, no metadata, no headers, no object hashes, andoffset_ == 0:This also covers an empty first payload followed by payloads with data: the metadata, headers, offset, and object hashes from the first payload are kept.
Accumulate()has exactly one caller (ReadAll()), so no other code paths change behavior.Tests
New tests:
read_payload_impl_test.cc: direct tests forAccumulate(), which had none before:AccumulateIntoDefault,AccumulateAppendsData,AccumulateEmptyIntoEmpty(existing behavior)AccumulateEmptyWithMetadataKeepsMetadata,AccumulateEmptyWithMetadataThenData,AccumulateEmptyWithoutMetadataKeepsOtherFields(regressions)read_all_test.cc:ZeroBytePayloadPreservesMetadata,EmptyPayloadWithMetadataThenDataconnection_impl_read_test.cc:ReadObjectRangeZeroByteObjectKeepsMetadata, an end-to-end test through the realReadObjectRange()-> resume -> reader ->ReadAll()stack with a mocked gRPC stream returning a 0-byte object.Results
read_payload_impl.hreverted tomain)//google/cloud/storageunit tests (Bazel, integration tests excluded)clang-formaton changed filesManual verification against production GCS
I ran a small program using
AsyncClientagainst a real regional bucket with hierarchical namespace enabled. For each of a 0-byte and an 11-byte object, it creates the object, reads it back withReadObjectRange()andReadAll(ReadObject()), checks the returned metadata, and deletes the object.ReadObjectRange(bucket, name, 0, 0)generation,size=0nullopt)ReadAll(ReadObject(bucket, name))generationnullopt)Output with the fix:
Output without the fix:
Internal bug: b/565852217