feat(storage): extend read hedging to mid-stream chunk reads - #16446
Conversation
There was a problem hiding this comment.
Code Review
This pull request extends the HedgedObjectReadSource to support hedging of mid-stream reads when a stall is detected, rather than only hedging the initial stream open. It introduces tracking of the current offset, direction, and generation to allow new hedge attempts to resume reading from the correct position. The feedback focuses on style guide compliance regarding type deduction, specifically requesting the replacement of auto with explicit types where it obscures StatusOr return types or primitive values.
7ee849a to
c4f9b42
Compare
c4f9b42 to
5e62f8f
Compare
5e62f8f to
d204b1d
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #16446 +/- ##
==========================================
+ Coverage 92.32% 92.34% +0.02%
==========================================
Files 2246 2246
Lines 213921 214568 +647
==========================================
+ Hits 197504 198149 +645
- Misses 16417 16419 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
d204b1d to
b250178
Compare
|
/gcbrun |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request enhances HedgedObjectReadSource to support hedging subsequent reads mid-stream when a read stalls, rather than only hedging the initial stream open. It introduces tracking for the current offset, direction, end offset, and generation, and updates the child factory to allow opening new streams at specific positions. Additionally, it adds comprehensive unit tests to verify the mid-stream hedging behavior, stalling detection, and offset tracking. A review comment correctly identifies a style guide violation where auto is used to deduce a primitive type (std::int64_t) in hedged_object_read_source.cc.
b250178 to
8b6e16e
Compare
8b6e16e to
cb65764
Compare
|
/gcbrun |
cb65764 to
8bbc230
Compare
8bbc230 to
24eacfa
Compare
24eacfa to
23cc230
Compare
23cc230 to
8d91ec8
Compare
8d91ec8 to
5ab04a3
Compare
Hedging previously covered only the stream open (googleapis#16344), so a connection that stalled mid-download could not be raced. Once an attempt won the open race, every subsequent Read() was a plain passthrough to that child for the life of the download. Make HedgedObjectReadSource stateful: track the stream position (offset, direction, generation, size, transcoding) the same way RetryObjectReadSource does when it resumes after a failure, so a hedge can be opened at the stream's current offset rather than at the original request offset. Time each read and re-race a read whose predecessor exceeded ReadHedgeDelayOption, with the active child as the primary attempt. A hedge that wins replaces the active child; once a read completes within the delay the stream returns to direct reads. ChildFactory now takes the position to open at, and StorageConnectionImpl::ReadObject() applies the same request rewrite RetryObjectReadSource uses on resume, so hedged and retried reads resume through identical logic. Racing is skipped where it cannot be correct or cannot help: under decompressive transcoding, for reads above MaximumHedgeBufferOption, and once the stream has reached the end of the requested data. Also harden the race bookkeeping: resolve the race when every attempt fails, prefer the primary's error over a hedge's, and fail fast on a permanent primary error. No new public options, and no behavior change when hedging is disabled. TAG=agy CONV=3c1752ee-1a2a-4f41-b047-1cc070877764
7386fb8 to
0c38ad2
Compare
Motivation
#16344 added speculative hedging to
ReadObject(), but only for the stream open. Once an attempt won the race, every subsequentRead()was a plain passthrough to that child for the life of the download.That covers TTFB, which is where most tail latency lives, but it leaves a real gap: a connection that is healthy at open can stall later. For a large object read in chunks, a single stalled connection mid-download costs as much as a slow open, and the client had no way to recover short of the retry policy firing on an actual error — a stall that never errors just blocks.
This PR extends hedging to any read in the stream.
What changed
HedgedObjectReadSourcebecomes stateful. It tracks the stream's position the same wayRetryObjectReadSourcealready does when it resumes after a failure — current offset, offset direction, pinned generation, object size, and whether decompressive transcoding is in play — so it can open a new child at the stream's current position rather than at the original request offset.Each read is timed. A read that takes longer than
ReadHedgeDelayOptionmarks the stream as stalled, and the next read is raced: the existing active child becomes the primary attempt, and hedges are opened at the current offset, pinned to the generation observed so far. A hedge that wins replaces the active child and serves the rest of the stream. Once a read completes within the delay, the stream returns to direct reads on the caller's thread with no thread hops or copies.To support this,
ChildFactorygained the position it should open at:StorageConnectionImpl::ReadObject()implements it by applying the same request rewriteRetryObjectReadSource::Read()applies on resume (ReadLast/ReadFromOffset, plusGenerationwhen known), so hedged and retried reads resume through identical logic.Where racing is deliberately skipped
Mid-stream racing is not always safe or useful, so it is bypassed:
gunzipped)MaximumHedgeBufferOptionThe stream open is always raced, as before.
Error handling changes
Important
These are behavioral changes beyond mid-stream hedging and deserve their own look.
RaceStatepreviously resolved only on the primary's open error; a hedge that failed was silently ignored, and if every attempt failed the caller could be left waiting. It now:kNotFoundorkPermissionDenied, so the caller is not held until every in-flight hedge has exhausted its own retry budget.Compatibility
EnableReadHedgingOption,ReadHedgeDelayOption,MaxReadHedgesOption, andMaximumHedgeBufferOption.retry_source_factory().HedgedObjectReadSourceandChildFactorylive instorage::internal.Testing
google/cloud/storage:internal_hedged_object_read_source_testgrows from 14 to 26 cases, covering generation pinning, gunzip bypass,ReadLastoffset tracking and clamping, the three end-of-stream variants, stall detection and the return to direct reads, hedge-pool exhaustion, and the all-attempts-fail paths.Areas worth reviewer attention
RetryObjectReadSource.UpdateState()duplicates the offset/generation/gunzip logic inRetryObjectReadSource::HandleResult(). Is the duplication acceptable, or should the two share a small helper? They must not drift.ReadLastclamping.ReadLast(N)withNlarger than the object returns the whole object, so the remaining count has to be bounded by the object size before a hedge is opened with it — otherwise the hedge covers the whole object again. Covered byReadLastLargerThanObjectClampsOffset.