Skip to content

fix(dash-spv): reissue requeued masternode requests from tick - #953

Merged
xdustinface merged 1 commit into
devfrom
fix/masternode-tick-requeue-stall
Aug 11, 2026
Merged

fix(dash-spv): reissue requeued masternode requests from tick#953
xdustinface merged 1 commit into
devfrom
fix/masternode-tick-requeue-stall

Conversation

@xdustinface

@xdustinfacexdustinface commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

MasternodesManager::tick only ran the MnListDiff timeout-and-resend branch while requests were in flight. A single-peer disconnect requeues every in-flight getmnlistd as pending via on_peer_disconnect, leaving zero in flight, so the pending work was never reissued: responses arriving after the requeue are dropped as untracked and no other path calls send_pending. Masternode sync then stalled permanently with no error logged. Observed on a mainnet fresh sync where a stalled-peer eviction hit mid-pipeline, freezing at diffs_processed: 6 with 42 requests stranded until restart.

Gate the branch on the pipeline having outstanding work instead, so requeued pending requests go out on the next tick. MnListDiffPipeline::active_count becomes test-only.

Summary by CodeRabbit

  • Bug Fixes
    • Improved masternode list synchronization after peer disconnections.
    • Pending synchronization requests are now correctly requeued and resent during the next update cycle.
    • Timeout handling now continues while synchronization is incomplete, ensuring interrupted requests are processed reliably.

`MasternodesManager::tick` only ran the MnListDiff timeout-and-resend branch while requests were in flight. A single-peer disconnect requeues every in-flight `getmnlistd` as pending via `on_peer_disconnect`, leaving zero in flight, so the pending work was never reissued: responses arriving after the requeue are dropped as untracked and no other path calls `send_pending`. Masternode sync then stalled permanently with no error logged. Observed on a mainnet fresh sync where a stalled-peer eviction hit mid-pipeline, freezing at `diffs_processed: 6` with 42 requests stranded until restart.
Gate the branch on the pipeline having outstanding work instead, so requeued pending requests go out on the next tick. `MnListDiffPipeline::active_count` becomes test-only.
@coderabbitai

coderabbitaiBot commented Aug 11, 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: 745f4451-a946-4453-aee5-e1511f19d785

📥 Commits

Reviewing files that changed from the base of the PR and between 94eba8e and 877e659.

📒 Files selected for processing (3)
  • dash-spv/src/sync/masternodes/manager.rs
  • dash-spv/src/sync/masternodes/pipeline.rs
  • dash-spv/src/sync/masternodes/sync_manager.rs

📝 Walkthrough

Walkthrough

The MnListDiff timeout path now processes incomplete pipelines after peer disconnects. A Tokio integration test verifies that queued requests are resent and tracked as active. The active-count helper is limited to test builds.

Changes

MnListDiff requeue handling

Layer / File(s)Summary
Process incomplete MnListDiff pipelines
dash-spv/src/sync/masternodes/sync_manager.rs
The timeout path checks for an incomplete pipeline and processes requeued MnListDiff requests after peer disconnects.
Validate request resend and tracking
dash-spv/src/sync/masternodes/manager.rs, dash-spv/src/sync/masternodes/pipeline.rs
The integration test verifies that two disconnected requests are resent and marked active. The active_count helper is compiled only for tests.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers:zocolini

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely describes the main fix: reissuing requeued masternode requests from tick.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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/masternode-tick-requeue-stall

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

@xdustinface
xdustinface merged commit 34886d9 into devAug 11, 2026
34 of 35 checks passed
@xdustinface
xdustinface deleted the fix/masternode-tick-requeue-stall branch August 11, 2026 10:18
@codecov

codecovBot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.57%. Comparing base (94eba8e) to head (877e659).
⚠️ Report is 1 commits behind head on dev.

Additional details and impacted files
@@ Coverage Diff @@## dev #953 +/- ##
==========================================
+ Coverage 75.52% 75.57% +0.04% 
==========================================
Files 329 329 Lines 79136 79169 +33 ==========================================
+ Hits 59769 59829 +60 + Misses 19367 19340 -27 
FlagCoverage Δ
core77.29% <ø> (ø)
ffi49.03% <ø> (ø)
rpc20.00% <ø> (ø)
spv91.57% <100.00%> (+0.15%)⬆️
wallet77.46% <ø> (ø)
Files with missing linesCoverage Δ
dash-spv/src/sync/masternodes/manager.rs93.90% <100.00%> (+0.39%)⬆️
dash-spv/src/sync/masternodes/pipeline.rs97.89% <ø> (ø)
dash-spv/src/sync/masternodes/sync_manager.rs82.23% <ø> (+0.98%)⬆️

... and 4 files with indirect coverage changes

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
bfoss765 added a commit that referenced this pull request Aug 21, 2026
The branch was cut at b056d07, before #953 landed on dev; the dev merge
(08961af) surfaced two artifacts:
- pipeline.rs carried a second MnListDiffPipeline::requeue_in_flight,
identical in body to the one #953 added. The two sat in different parts
of the file so git merged cleanly, but the result failed to compile
with E0592. Drop ours, keep dev's; pipeline.rs is now byte-identical
to dev. on_peer_disconnect now routes through the
MasternodeSyncState::requeue_in_flight wrapper like on_disconnect,
instead of reaching into the pipeline directly (same behaviour: the
wrapper only delegates to the pipeline).
- tick's MnListDiff section had been reshaped by both sides for the same
reason, leaving two consecutive `if !is_complete()` blocks after the
merge, the first holding only handle_timeouts(). Restore dev's single
block; dev already provides the flush behaviour this PR's version
described, and test_tick_reissues_requeued_mnlistdiffs passes against
it.
Also correct the watchdog's "known route in" example: a peerless send
cannot strand the manager (request_qr_info pushes onto an unbounded
channel and only errors once the channel is closed, so it fails later in
the network task with the slot still armed). The real routes in are
send_qrinfo_for_tip's early Ok returns - no stored tip, or a tip at
genesis - after the caller has already cleared the slot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
xdustinface added a commit that referenced this pull request Aug 22, 2026
* fix(dash-spv): recover masternode sync from a rejected QRInfo
One QRInfo response the engine rejects permanently strands masternode
sync. `qrinfo_received()` cleared `qrinfo_in_flight` before the fallible
`feed_qr_info`, so a validation failure returned `Err` leaving the
manager in `Syncing` with nothing in flight and an empty diff pipeline.
The [10, 30, 60] retry ladder is armed solely by `qrinfo_in_flight`, so
`tick` fell through forever - a frozen `qr_infos_requested: 1` for the
life of the process, with the error surfacing only as a
`SyncEvent::ManagerError` that has no consumer.
Masternode sync gates overall SYNCED, so while stalled every InstantLock
fails verification, DAPI has no masternode list, and platform features
are dead until a restart.
Three changes:
1. Release the request slot only after the last fallible step, just
before `queue_requests`. On the `feed_qr_info` error branch keep the
slot armed and flag the attempt `rejected`, which `tick` treats as an
elapsed timeout: the retry rotates to the next peer via
`send_distributed`'s round-robin, on the existing budget, so a
deterministically-bad response still terminates after
MAX_RETRY_ATTEMPTS dispatches. `last_processed_qrinfo_tip` continues
to be set only on success, so the retry is not dropped as a duplicate.
2. Add a stall watchdog to `tick`: `Syncing` with no QRInfo in flight and
an empty diff pipeline for longer than 60s re-dispatches a QRInfo.
This covers the routes the in-flight flag cannot, notably a
`send_qrinfo_for_tip` that fails after its caller already cleared the
slot. Timed off a new `last_qrinfo_dispatch` rather than
`progress.last_activity()`, which unrelated block events keep bumping.
3. `on_disconnect` requeues in-flight `GetMnListDiff`s instead of
clearing, mirroring `BlocksManager` and `FiltersManager`, and leaves
the QRInfo slot armed. `tick` now flushes the pending queue whenever
the pipeline is non-empty, which is what actually reissues requeued
requests - every other `send_pending` call site hangs off a response
handler that cannot run while nothing is in flight.
* fix(dash-spv): record the processed QRInfo tip only after the last fallible step
Recording last_processed_qrinfo_tip before build_mnlistdiff_request_pairs
turned the dedup gate against the retry ladder: a failure in that step left
the request slot armed, but every retried response for the same tip was
rejected at the handler entry by should_process_qrinfo, so the retry budget
burned down with no way to succeed - reintroducing the permanent stall this
branch exists to fix, through a different fallible step.
Defer the record to the success path, next to qrinfo_received(). A straggler
can only arrive after the handler returns, so the dedup gate loses nothing.
Re-feeding the engine on such a retry is already accepted by design - the
on_disconnect path clears the recorded tip for the same reason.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(dash-spv): de-link a pub(super) item rustdoc cannot resolve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dash-spv): drop rebase artifacts duplicated by #953 landing on dev
The branch was cut at b056d07, before #953 landed on dev; the dev merge
(08961af) surfaced two artifacts:
- pipeline.rs carried a second MnListDiffPipeline::requeue_in_flight,
identical in body to the one #953 added. The two sat in different parts
of the file so git merged cleanly, but the result failed to compile
with E0592. Drop ours, keep dev's; pipeline.rs is now byte-identical
to dev. on_peer_disconnect now routes through the
MasternodeSyncState::requeue_in_flight wrapper like on_disconnect,
instead of reaching into the pipeline directly (same behaviour: the
wrapper only delegates to the pipeline).
- tick's MnListDiff section had been reshaped by both sides for the same
reason, leaving two consecutive `if !is_complete()` blocks after the
merge, the first holding only handle_timeouts(). Restore dev's single
block; dev already provides the flush behaviour this PR's version
described, and test_tick_reissues_requeued_mnlistdiffs passes against
it.
Also correct the watchdog's "known route in" example: a peerless send
cannot strand the manager (request_qr_info pushes onto an unbounded
channel and only errors once the channel is closed, so it fails later in
the network task with the slot still armed). The real routes in are
send_qrinfo_for_tip's early Ok returns - no stored tip, or a tip at
genesis - after the caller has already cleared the slot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Kevin Rombach <35775977+xdustinface@users.noreply.github.com>
romchornyi pushed a commit that referenced this pull request Aug 25, 2026
* fix(dash-spv): recover masternode sync from a rejected QRInfo
One QRInfo response the engine rejects permanently strands masternode
sync. `qrinfo_received()` cleared `qrinfo_in_flight` before the fallible
`feed_qr_info`, so a validation failure returned `Err` leaving the
manager in `Syncing` with nothing in flight and an empty diff pipeline.
The [10, 30, 60] retry ladder is armed solely by `qrinfo_in_flight`, so
`tick` fell through forever - a frozen `qr_infos_requested: 1` for the
life of the process, with the error surfacing only as a
`SyncEvent::ManagerError` that has no consumer.
Masternode sync gates overall SYNCED, so while stalled every InstantLock
fails verification, DAPI has no masternode list, and platform features
are dead until a restart.
Three changes:
1. Release the request slot only after the last fallible step, just
before `queue_requests`. On the `feed_qr_info` error branch keep the
slot armed and flag the attempt `rejected`, which `tick` treats as an
elapsed timeout: the retry rotates to the next peer via
`send_distributed`'s round-robin, on the existing budget, so a
deterministically-bad response still terminates after
MAX_RETRY_ATTEMPTS dispatches. `last_processed_qrinfo_tip` continues
to be set only on success, so the retry is not dropped as a duplicate.
2. Add a stall watchdog to `tick`: `Syncing` with no QRInfo in flight and
an empty diff pipeline for longer than 60s re-dispatches a QRInfo.
This covers the routes the in-flight flag cannot, notably a
`send_qrinfo_for_tip` that fails after its caller already cleared the
slot. Timed off a new `last_qrinfo_dispatch` rather than
`progress.last_activity()`, which unrelated block events keep bumping.
3. `on_disconnect` requeues in-flight `GetMnListDiff`s instead of
clearing, mirroring `BlocksManager` and `FiltersManager`, and leaves
the QRInfo slot armed. `tick` now flushes the pending queue whenever
the pipeline is non-empty, which is what actually reissues requeued
requests - every other `send_pending` call site hangs off a response
handler that cannot run while nothing is in flight.
* fix(dash-spv): record the processed QRInfo tip only after the last fallible step
Recording last_processed_qrinfo_tip before build_mnlistdiff_request_pairs
turned the dedup gate against the retry ladder: a failure in that step left
the request slot armed, but every retried response for the same tip was
rejected at the handler entry by should_process_qrinfo, so the retry budget
burned down with no way to succeed - reintroducing the permanent stall this
branch exists to fix, through a different fallible step.
Defer the record to the success path, next to qrinfo_received(). A straggler
can only arrive after the handler returns, so the dedup gate loses nothing.
Re-feeding the engine on such a retry is already accepted by design - the
on_disconnect path clears the recorded tip for the same reason.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(dash-spv): de-link a pub(super) item rustdoc cannot resolve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dash-spv): drop rebase artifacts duplicated by #953 landing on dev
The branch was cut at b056d07, before #953 landed on dev; the dev merge
(08961af) surfaced two artifacts:
- pipeline.rs carried a second MnListDiffPipeline::requeue_in_flight,
identical in body to the one #953 added. The two sat in different parts
of the file so git merged cleanly, but the result failed to compile
with E0592. Drop ours, keep dev's; pipeline.rs is now byte-identical
to dev. on_peer_disconnect now routes through the
MasternodeSyncState::requeue_in_flight wrapper like on_disconnect,
instead of reaching into the pipeline directly (same behaviour: the
wrapper only delegates to the pipeline).
- tick's MnListDiff section had been reshaped by both sides for the same
reason, leaving two consecutive `if !is_complete()` blocks after the
merge, the first holding only handle_timeouts(). Restore dev's single
block; dev already provides the flush behaviour this PR's version
described, and test_tick_reissues_requeued_mnlistdiffs passes against
it.
Also correct the watchdog's "known route in" example: a peerless send
cannot strand the manager (request_qr_info pushes onto an unbounded
channel and only errors once the channel is closed, so it fails later in
the network task with the slot still armed). The real routes in are
send_qrinfo_for_tip's early Ok returns - no stored tip, or a tip at
genesis - after the caller has already cleared the slot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Kevin Rombach <35775977+xdustinface@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@xdustinface