fix(sync): reserve one slot for registry-miss retry instead of stalling dispatch#384
fix(sync): reserve one slot for registry-miss retry instead of stalling dispatch#384p0mvn wants to merge 4 commits into
Conversation
…ng dispatch A required block that is unavailable from all ready peers is parked on a registry-miss backoff and retried later. Previously, any pending retry set a head-of-line gate (`head_of_line_starved = !registry_miss_retry.is_empty()`) that paused *all* reserve block dispatch until the retry cleared. A peer could advertise a required hash it would not serve and delay unrelated block downloads for roughly the retry budget (~2 minutes) — a targeted node-level sync/catch-up DoS. Replace the all-or-nothing gate with a capacity reservation: `sync_round` now withholds a single download slot (`REGISTRY_MISS_RESERVED_DOWNLOAD_SLOTS`) for the pending retry and keeps dispatching unrelated reserve hashes while spare lookahead capacity remains. `request_blocks` takes an explicit dispatch budget so the reserved slot is honored; `obtain_tips` passes the full lookahead limit, preserving its existing behavior. Add a `sync.registry_miss.pending` gauge for observability, and tests covering the reservation math (inert with no miss, capped at one slot with several misses) and a regression test that unrelated reserve hashes still dispatch while a miss is pending.
|
Note Complete: Audit complete. No review-worthy issues remain after automatic triage. One finding was auto-invalidated. Open the full results here. Analyzed one file, diff |
… path Add an in-process test that installs a local metrics recorder and drives a genuine `NotFoundRegistry` download failure through the real handler and `update_metrics`, asserting `sync.registry_miss.pending` goes from 0 to 1 while unrelated reserve hashes still dispatch and the retry stays scheduled. This exercises the reserved-slot path end-to-end and observes the same gauge exposed on a live node's /metrics endpoint. Adds `metrics-util` as a dev-dependency for the recorder.
Replace the metrics-util-based gauge capture with a tiny test-local `Recorder` implementation, so the registry-miss gauge test pulls in no new supply-chain dependencies (metrics-util's debugging feature added unvetted transitive crates that failed cargo-vet). The test still reads the real `sync.registry_miss.pending` value emitted by `update_metrics` through the metrics facade.
Testing summaryThis documents the testing strategies completed for the registry-miss dispatch fix, from unit level up to a real running node driven by a wire-level misbehaving peer. 1. Unit / property tests (
|
Motivation
A required block that is unavailable from every ready peer is parked on a
registry-miss backoff and retried later. In the current code, any pending
retry sets a global head-of-line gate:
While that gate is set, all reserve block dispatch is paused until the retry
clears. A peer can advertise a required block hash it will not actually serve;
once that hash becomes a registry miss, unrelated (fully available) reserve
downloads are stalled for roughly the retry budget —
MISSING_BLOCK_REGISTRY_RETRY_LIMIT (60) × REGISTRY_MISS_RETRY_BACKOFF (2s) ≈ 2 minutes. Impact is a targeted, node-level sync / catch-up delay (DoS).Solution
Replace the all-or-nothing gate with a capacity reservation:
reserve_dispatch_limit()returnslookahead_limit − in_flight − reserved, wherereserved = REGISTRY_MISS_RESERVED_DOWNLOAD_SLOTS.min(registry_miss_retry.len())(0 or 1).sync_rounddispatches reserve hashes wheneverreserve_dispatch_limit > 0,so a single pending retry withholds just one slot instead of the whole window.
Unrelated reserve hashes keep flowing while spare lookahead capacity remains.
request_blocksnow takes an explicitdispatch_limit(the reserve looppasses the reserved budget;
obtain_tipspasses the full lookahead limit,preserving its prior behavior).
select!timer arm, sothe parked block still gets prompt head-of-line service — the reservation just
keeps in-flight one below the limit so the retry has room without overshoot.
The change is inert in healthy sync (no miss ⇒
reserved = 0), andsaturating_subprevents underflow.Behavior note (steady state)
In the reserve loop, dispatch is now capped so total in-flight fills up to
lookahead_limit(accounting forin_flight), whereas previouslyrequest_blockssplit at the rawlookahead_limitand could briefly overshootto ~2×. This tightens in-flight concurrency to the documented limit.
obtain_tipsis unchanged. See test evidence below for the throughput check.
Observability
sync.registry_miss.pendinggauge: number of required blocks parked on aregistry-miss backoff. A sustained non-zero value indicates required hashes are
unavailable from all ready peers (and holding the reserved slot); a spike can
indicate a peer feeding unavailable required hashes.
"requesting more blocks"debug line now logsreserve_dispatch_limitalongside
in_flight/reserve/lookahead_limit.Test evidence
New unit/regression tests in
zebrad/src/components/sync/tests/vectors.rs:reserve_dispatch_limit_is_inert_without_registry_misses— no miss ⇒ fulllookahead window is dispatchable (pins the healthy path unchanged).
reserve_dispatch_limit_caps_reservation_at_one_slot— several pending missesstill reserve only one slot.
registry_miss_does_not_block_unrelated_reserve_dispatch— regression test:with a miss parked on backoff, unrelated available reserve hashes still
dispatch to peers, and the retry stays scheduled (not dropped).
request_blocks_dispatches_only_the_budgeted_hashes—request_blocksdispatches only the caller's budget and returns the rest to the reserve.
Real-node check: syncing a fresh node from genesis through 100k heights to
confirm no throughput regression from the in-flight cap change (results added as
a comment).
Related
Adjusts the head-of-line behavior introduced in #105
(
fix: prioritize head-of-line block during registry-miss backoff).AI disclosure
Used Claude Code (Opus 4.8) to review the fix, and to author the added tests,
the
sync.registry_miss.pendingmetric, the changelog entry, and this PRdescription. The contributor is the responsible author; all changes were
reviewed and verified locally.