Skip to content

fix(dash-spv): requeue in-flight downloads when a single peer disconnects - #941

Merged
ZocoLini merged 2 commits into
devfrom
fix/requeue-inflight-on-peer-disconnect
Aug 11, 2026
Merged

fix(dash-spv): requeue in-flight downloads when a single peer disconnects#941
ZocoLini merged 2 commits into
devfrom
fix/requeue-inflight-on-peer-disconnect

Conversation

@xdustinface

@xdustinface xdustinface commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Problem

requeue_in_flight was only reachable through the total-peer-loss path. When one of several peers disconnected, the items in flight to that peer stayed stranded until the 15-30s stall timeout picked them up, delaying sync on every ordinary peer drop.

Fix

A new on_peer_disconnect hook requeues the disconnected peer's in-flight items immediately while the remaining peers keep working. It is deliberately separate from on_disconnect, which resets manager state and would be destructive for the filter header and InstantSend managers on a single-peer drop.

Covered by a new test for the single-peer disconnect requeue path.

Summary by CodeRabbit

  • Bug Fixes
    • Synchronization requests are automatically requeued when a peer disconnects, allowing downloads to continue with remaining peers.
    • In-flight block, header, filter, and masternode data requests are resent without losing progress.
    • Reconnecting requests no longer incorrectly increase retry counts.
    • Synchronization state is preserved until all available peers are disconnected.

…ects

`DownloadCoordinator::requeue_in_flight` was only ever reached through `SyncManager::stop_sync`, which fires exclusively when the last peer goes away. Losing one peer out of several therefore stranded everything that peer owed us until the per-item timeout expired, 15s for `getmnlistd`, 20s for `getcfheaders`, 30s for headers and blocks. On mainnet that showed up as an evicted peer's batches trickling back in over 20 seconds while the filter header out-of-order buffer opened a six-figure height gap behind them.

Add `SyncManager::on_peer_disconnect`, a no-op by default, dispatched from `NetworkEvent::PeerDisconnected`. That event already exists, already carries the dropped address, and already precedes `PeersUpdated` on every removal path, so a drop that leaves other peers connected becomes observable without touching the network layer. It stays separate from `on_disconnect` on purpose: several `on_disconnect` implementations are written for total peer loss and are destructive, `FilterHeadersManager` rebuilds its whole pipeline and `InstantSendManager` drops its unvalidated cache, so reusing that hook for routine peer churn would repeatedly discard real progress.

In-flight items are not attributed to a peer, so a requeue reissues everything outstanding, including requests a surviving peer will still answer. That trade is acceptable rather than merely tolerable: `requeue_in_flight` preserves retry counts, so nothing escapes or prematurely exhausts its budget, and every receive path treats a response it no longer tracks as unrequested, `FiltersPipeline` even cancels the pending entry when a late reply completes a requeued batch. The cost is redundant traffic, bounded by the concurrency limit, in exchange for dropping the worst-case recovery from 30s to the next 100ms tick. Per-item peer attribution stays deferred.

Per manager:

- `BlocksManager` and `FiltersManager` requeue their coordinator, matching what they already do on total loss.
- `FilterHeadersManager` requeues in-flight batches only, leaving `batch_starts`, `next_expected` and the out-of-order buffer intact rather than resetting the pipeline the way total loss does.
- `MasternodesManager` requeues the `getmnlistd` pipeline. The QRInfo request is tracked outside the coordinator with its own escalating timeout and attempt budget, so it is left to that path.
- `BlockHeadersManager` clears per-segment in-flight bookkeeping and reissues synchronously, but only while `Syncing`. A segment rejects headers whose request it no longer tracks, so deferring the resend to the next tick would open a window where a surviving peer's reply looks unsolicited. Outside `Syncing` nothing reissues header requests, so clearing there would strand the request instead of retrying it.
- `ChainLockManager`, `InstantSendManager` and `MempoolManager` keep the default no-op. None of them drive a `DownloadCoordinator`, so nothing is stranded behind a timeout, and their disconnect state is either a dedup set that would cause redundant `getdata` if cleared or, for the mempool, already tracked per peer.
Pins the two properties the fix depends on: a drop that leaves peers connected reaches the requeue hook and not the destructive total-loss one, and a requeue reissues each request with the identity it was originally paired with, block hash for `getdata`, start height for `getcfheaders`, base hash for `getmnlistd`.

Also pins that a requeue is not an attempt against an item's retry budget, since a peer disconnect is not the item's fault and repeated churn would otherwise exhaust the budget and drop work.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c9892852-5642-46b8-af1c-fe46eb3f9dcf

📥 Commits

Reviewing files that changed from the base of the PR and between d91ad05 and 165e1c6.

📒 Files selected for processing (10)
  • dash-spv/src/sync/block_headers/sync_manager.rs
  • dash-spv/src/sync/blocks/manager.rs
  • dash-spv/src/sync/blocks/sync_manager.rs
  • dash-spv/src/sync/download_coordinator.rs
  • dash-spv/src/sync/filter_headers/pipeline.rs
  • dash-spv/src/sync/filter_headers/sync_manager.rs
  • dash-spv/src/sync/filters/sync_manager.rs
  • dash-spv/src/sync/masternodes/pipeline.rs
  • dash-spv/src/sync/masternodes/sync_manager.rs
  • dash-spv/src/sync/sync_manager.rs

📝 Walkthrough

Walkthrough

The synchronization framework now requeues in-flight requests when one peer disconnects. It preserves progress and retry metadata, resends pending requests immediately where required, and keeps total-peer-loss cleanup separate.

Changes

Peer disconnect recovery

Layer / File(s) Summary
Disconnect event dispatch
dash-spv/src/sync/sync_manager.rs
The public hook handles individual peer disconnects separately from total-peer-loss cleanup. Tests verify that progress remains intact until all peers are lost.
Pipeline request requeueing
dash-spv/src/sync/download_coordinator.rs, dash-spv/src/sync/filter_headers/pipeline.rs, dash-spv/src/sync/masternodes/pipeline.rs
In-flight filter-header and masternode requests return to pending state without losing progress, hash mappings, buffered responses, or retry-count semantics.
Sync manager integrations
dash-spv/src/sync/block_headers/sync_manager.rs, dash-spv/src/sync/blocks/sync_manager.rs, dash-spv/src/sync/blocks/manager.rs, dash-spv/src/sync/filter_headers/sync_manager.rs, dash-spv/src/sync/filters/sync_manager.rs, dash-spv/src/sync/masternodes/sync_manager.rs
Block, block-header, filter, filter-header, and masternode managers requeue disconnected-peer requests. Regression tests verify request retransmission and preserved request data.
Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Peer
  participant SyncManager
  participant BlocksManager
  participant DownloadCoordinator
  Peer->>SyncManager: PeerDisconnected
  SyncManager->>BlocksManager: on_peer_disconnect()
  BlocksManager->>DownloadCoordinator: requeue in-flight requests
  DownloadCoordinator-->>BlocksManager: pending requests
  BlocksManager-->>Peer: resend GetData requests
Loading

Possibly related PRs

Suggested labels: ready-for-review

Suggested reviewers: zocolini

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes requeueing in-flight dash-spv downloads after a single peer disconnects.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/requeue-inflight-on-peer-disconnect

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.74468% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.24%. Comparing base (d91ad05) to head (165e1c6).

Files with missing lines Patch % Lines
dash-spv/src/sync/masternodes/sync_manager.rs 0.00% 3 Missing ⚠️
dash-spv/src/sync/blocks/manager.rs 95.12% 2 Missing ⚠️
dash-spv/src/sync/filter_headers/pipeline.rs 97.36% 1 Missing ⚠️
dash-spv/src/sync/masternodes/pipeline.rs 96.87% 1 Missing ⚠️
dash-spv/src/sync/sync_manager.rs 97.95% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #941      +/-   ##
==========================================
+ Coverage   75.17%   75.24%   +0.06%     
==========================================
  Files         328      328              
  Lines       78194    78370     +176     
==========================================
+ Hits        58783    58967     +184     
+ Misses      19411    19403       -8     
Flag Coverage Δ
core 77.29% <ø> (ø)
ffi 48.59% <ø> (+<0.01%) ⬆️
rpc 20.00% <ø> (ø)
spv 91.43% <95.74%> (+0.11%) ⬆️
wallet 76.88% <ø> (ø)
Files with missing lines Coverage Δ
dash-spv/src/sync/block_headers/sync_manager.rs 86.66% <100.00%> (+2.05%) ⬆️
dash-spv/src/sync/blocks/sync_manager.rs 83.78% <100.00%> (+1.43%) ⬆️
dash-spv/src/sync/download_coordinator.rs 100.00% <100.00%> (ø)
dash-spv/src/sync/filter_headers/sync_manager.rs 100.00% <100.00%> (ø)
dash-spv/src/sync/filters/sync_manager.rs 100.00% <100.00%> (ø)
dash-spv/src/sync/filter_headers/pipeline.rs 97.21% <97.36%> (+0.02%) ⬆️
dash-spv/src/sync/masternodes/pipeline.rs 97.89% <96.87%> (-0.16%) ⬇️
dash-spv/src/sync/sync_manager.rs 83.89% <97.95%> (+10.67%) ⬆️
dash-spv/src/sync/blocks/manager.rs 96.72% <95.12%> (-0.33%) ⬇️
dash-spv/src/sync/masternodes/sync_manager.rs 81.25% <0.00%> (-0.81%) ⬇️

... and 3 files with indirect coverage changes

@github-actions github-actions Bot added the ready-for-review CodeRabbit has approved this PR label Aug 10, 2026
@xdustinface
xdustinface requested a review from ZocoLini August 10, 2026 13:33
@ZocoLini
ZocoLini merged commit 47166b5 into dev Aug 11, 2026
38 checks passed
@ZocoLini
ZocoLini deleted the fix/requeue-inflight-on-peer-disconnect branch August 11, 2026 09:53
ZocoLini added a commit that referenced this pull request Aug 11, 2026
The old network module gave each sync manager an `on_peer_disconnect`
hook that requeued its own in-flight work, and three separate fixes
landed against it (#941, #943, #953) — one for the
block pipeline, one for progress being discarded along with the requeue,
one for requeued work never being reissued. The broker owns a request
from send to response, so it replaced all three hooks with a single
central requeue, and their regression tests went with the hooks: the
replacement path had no coverage at all, in the area with the worst
track record.

Both callers — the timeout monitor kicking a stalled peer and the pump
seeing a socket close — did this inline and identically, buried in
spawned tasks where nothing could reach them. Lift it into
`requeue_requests_from` and pin the three properties the old tests
guarded:

- a departed peer's requests come back, a healthy peer's do not
- the key stays registered as `Queued`, so a pipeline re-declaring the
  request cannot queue a duplicate on top of the retry
- only the response retires the key, so a requeued request stays owned
  by someone

Checked against injected regressions: dropping the key instead of
requeuing it fails two of the three, and requeuing nothing fails all
three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ToH2xGXqVcxiwMkNYaWkh7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review CodeRabbit has approved this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants