Hedge slow requests in SimpleSidecarRetriever - #11011
Conversation
claude reviewOverview This PR adds speculative "hedge" requests to SimpleSidecarRetriever: when a column retrieval has been in-flight for longer than hedgeDelay (default 3s), a second attempt is dispatched to an alternate custody peer, bounded by overlapFraction Correctness Issues
// 1. Claim the peer slot // 2. Fire the RPC (future created here) // 3. Register the future — happens AFTER firing If gossip delivers the column (via reqRespSucceeded) between steps 1 and 3, that method calls request.cancelActiveRequests(), which iterates activeRequests — but the new peer's future isn't there yet. The future is never cancelled and leaks until
// reqRespSucceeded (gossip): // reqRespCompleted (RPC success branch): The RPC success branch could just call reqRespSucceeded(request, maybeResult). As-is, a future change to one path (e.g., adding a metric) can silently miss the other. Design / Concurrency
roundInProgress serializes concurrent round invocations, but RPC completion callbacks and gossip completions run outside that guard. The result is that during a round:
This is acceptable for a best-effort heuristic, but it means the overlap budget accounting is approximate: a request that transitions from 2 → 1 attempts mid-round could be counted toward currentOverlap and excluded from hedging, even though one
overlapFraction > 1.0 allows more hedged requests than pending requests. overlapFraction < 0.0 is clamped by the budget <= 0 check, but still an unexpected configuration. A guard at construction time (checkArgument(overlapFraction >= 0.0 &&
void cancelActiveRequests() { If two threads call this concurrently (possible: gossip path + dispose-completed-requests path on the next round), the forEach → clear → clear sequence is not linearized. Double-cancellation is harmless (cancel on an already-done future is a Minor / Style
Other timeout parameters in P2PConfig.Builder are validated (or at least documented). The overlap fraction should be validated at build() time or in the setter — a value of 5.0 (a common misinterpretation of "5%") would schedule 5× the pending
The default 3s is well-reasoned but tied to a specific knowledge of RPC timeout internals. Operators tuning --Xp2p-sidecar-retrieval-overlap-fraction would likely also want to tune hedgeDelay. Consider adding
The PR description explicitly documents the important scenario where the primary RPC times out while a hedged attempt is still in flight, freeing the slot to re-hedge to a third peer. This is the path that enables the rotating-hedge behaviour
Before: The test name is sanityTest and checks simpleSidecarRetriever (hedging disabled). This change is fine, but the helper createSidecarAndAddToAllPeers is called throughout the test file without appearing in the diff — it must already exist in the What's Good
Summary The core mechanism is sound. The main things to address before merging: validate overlapFraction at construction (#4), document or mitigate the gossip-vs-activation window (#1), and add the re-hedge-after-timeout test (#8). Items #2 and #5 are |
|
@rolfyone addressed feedback
|
|
I've decided not trying to avoid race completely, we are ok with eventual consistency here, we'd better remove over-complication. We should be sure that requests are eventually cleared and that's all. It's verified with the new test. |
|
codex has a concern P1 SimpleSidecarRetriever.java#L330-L334: activeRequests.put(...) happens after the request is handed to reqResp. Because nextRound() is explicitly unsynchronized and production uses a 4-thread das runner, another round can flush() the buffered request before this put. On failure, reqRespCompleted removes the peer from attemptingPeers/activeRequests, then this code re-inserts a completed ActiveRequest. That stale entry is then counted by createFromCurrentPendingRequests; with a peer limit of 1 it can reserve the peer forever for a still-pending column. The existing PR comment says this clears on timeout, but in this interleaving the future has already completed, so there is no later timeout to clean it up. Still running another round from claude... |
|
claude had a run at it too, seems mostly very minor things but included here. PR #11011 Review — Hedge slow requests in SimpleSidecarRetriever Defect 1 — Spurious RPC still fires after gossip-race cancellation In activateMatchedRequest, when gossip resolves a request between attemptingPeers.add() and activeRequests.put(), the code calls activeRpcRequest.cancel(true). But this cancels the handle-wrapper future, not reqRespPromise — the future already Fix: Cancel reqRespPromise directly before cancelling activeRpcRequest, or guard flush() with !promise().isDone() before adding to the node batch. Defect 2 — "At most two concurrent attempts" Javadoc invariant is wrong addHedgeMatches documents a strict two-attempt ceiling. But concurrent nextRound() calls (scheduler thread vs. reqResp thread) can both observe attemptingPeers.size() == 1 for the same request and both succeed in activateMatchedRequest with Fix: Change the Javadoc to "typically at most two; concurrent rounds may transiently produce three." Defect 3 — hedgeCounter overcounts unactivated hedge matches hedgeCounter.incrementAndGet() fires when a hedge RequestMatch is added to matches, before activateMatchedRequest runs. If a concurrent round already added that peer to attemptingPeers, activation returns false and no RPC fires — but the counter Fix: Move the increment inside activateMatchedRequest, after the guard succeeds and the attempt is confirmed to be a hedge. Design concern — Default 5% fraction silently disables hedging below 20 pending requests final int maxOverlap = (int) Math.floor(overlapFraction * pendingRequests.size()); With overlapFraction = 0.05 (the default): Math.floor(0.05 × N) = 0 for N < 20. The primary scenario described in the PR — one column stuck on a slow peer — is exactly the case where hedging is disabled. Math.max(1, (int) Math.floor(...)) gated on Test concern — Re-hedge exclusion mechanism doesn't match production shouldReHedgeToThirdPeerWhenPrimaryFailsWhileHedgeInFlight excludes the "column-not-available" peer via currentRequestLimit(1) — after one failure, completedRequests increments and available = 0, excluding the peer from re-selection. In Test coverage gap — concurrent-round races are untestable StubAsyncRunner is single-threaded, so concurrent nextRound() invocations (Defect 2) and the gossip-race in activateMatchedRequest (Defect 1) have no test coverage. Worth noting as known uncovered territory. Overall The core hedging state machine is correct — the ConcurrentHashMap-per-peer approach, activation guard, failure cleanup, and overlap budget are sound. The three defects above are real but not catastrophic: one leaks an occasional spurious RPC, one |
|
@rolfyone |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Reviewed by Cursor Bugbot for commit 71a2e60. Configure here.
71a2e60 to
66b6576
Compare

PR Description
Adds following logic to
SimpleSidecarRetriever:Adds redundant attempts for requests that have been in-flight for too long, re-dispatching them to an alternate custody peer. Bounded by the overlap budget so at most
overlapFractionof the pending requests carry a duplicate attempt at once.A request carries at most two concurrent attempts; escalation past that is implicit, driven by the RPC first-chunk timeout
(
Eth2OutgoingRequestHandler.RESPONSE_CHUNK_ARRIVAL_TIMEOUT, ~10s) freeing a slot to re-hedge a still-stuck request to a fresh peer.Example (
hedgeDelay= 3s, RPC timeout = 10s):attemptingPeers = {A}.hedgeDelay) —size == 1→ hedge → peer B.{A, B}.size == 2, not eligible; both attempts still outstanding (nothing new).{B};firstInFlightAtnot reset (B still in flight), so immediately hedge-eligible again → hedge to fresh peer C.{B, C}.{C, D}… rotating a dead peer out ~every RPC timeout until the column arrives or all attempts drain (then primary re-dispatches).My preliminary testing shows ~2x sync speed improvement
Fixed Issue(s)
Documentation
doc-change-requiredlabel to this PR if updates are required.Changelog
Note
Medium Risk
Changes concurrent DAS retrieval and req/resp batching on the sync path; behavior is tunable and heavily tested but overlap and unsynchronized rounds add extra peer traffic under load.
Overview
Improves Fulu data column sidecar retrieval when peers are slow or columns arrive via gossip before RPC completes.
SimpleSidecarRetrievermoves from a single in-flight RPC per column to multiple concurrent attempts per pending column. After a configurable hedge delay (default 3s), it can hedge by requesting the same column from an alternate custody peer, capped bysidecarRetrievalOverlapFraction(default 5% of pending work, floored to at least one hedge when enabled).0disables hedging. Completion paths cancel redundant attempts without penalizing peers, and fix races where gossip wins or an attempt finishes before registration so peers are not locked out of request budget.DataColumnReqRespBatchingImpl.flush()now skips buffered requests whose futures are already done (e.g. caller cancelled because gossip satisfied the need), avoiding useless network RPCs and peer scoring.Configuration is exposed via
P2PConfig, hidden CLI--Xp2p-sidecar-retrieval-overlap-fraction, and passed fromBeaconChainController. Tests cover hedging budgets, gossip/cancellation races, and batching behavior.Reviewed by Cursor Bugbot for commit 66b6576. Bugbot is set up for automated code reviews on this repo. Configure here.