feat(wallet): pause routine wallet scans during a user-run consolidation

A manual consolidation's merges compete with the GUI's routine wallet scans for
the daemon's cs_main: the run won't fire the next merge while any scan is in
flight, and because each merge mutates the wallet it re-triggers the balance /
history scans, which then delay the following merge. The WalletScanGovernor
already serializes all of these one-at-a-time under a cs_main budget, so the fix
is to pause the heavy routine streams — not every RPC — while a user run owns the
budget.

WalletScanGovernor gains per-stream routine suppression (tryAdmit returns empty
for a paused stream, which keeps waiting and resumes the instant it is unpaused;
explicit needs via beginExplicit are never paused), plus a convenience that
pauses the balance / addresses / history / recent-tx group together. App::update
pauses that group while consolidation_.active() AND the run is user-started —
an auto (background) run is left gentle and untouched, and the cheap sync / peer
/ mempool / operation-status polls keep running so sync and disconnects stay
visible. The end-of-run forced balance refresh repaints once the run finishes.

Note: a manual run is bounded (≤ maxBatches per click, ~minutes), so the balance
and recent-tx views are only briefly stale — an incoming payment shows once the
run finishes or is stopped. Unit-tested (testWalletScanGovernor): a paused stream
is not admitted but keeps waiting, explicit bypasses it, unpausing re-admits, and
chat/Adhoc are never in the paused group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 14:22:53 -05:00
co-authored by Claude Opus 4.8
parent 9335e2bffb
commit d83169c963
4 changed files with 41 additions and 0 deletions
+17
View File
@@ -18030,6 +18030,23 @@ void testWalletScanGovernor()
EXPECT_TRUE(static_cast<bool>(gov.tryAdmit(ScanStream::Balance, true, sig, 372.0)));
EXPECT_FALSE(gov.waiting(ScanStream::Balance, 373.0)); // admitted: no longer waiting
}
{ // Suppression: a paused routine stream is never admitted (but keeps waiting); explicit needs bypass
// it; unpausing admits it again. Used to give a user consolidation the cs_main budget.
G gov;
gov.suppressRoutineWalletScans(true);
EXPECT_TRUE(gov.streamSuppressed(ScanStream::Balance));
EXPECT_FALSE(static_cast<bool>(gov.tryAdmit(ScanStream::Balance, true, sig, 0.0))); // paused: not admitted
EXPECT_TRUE(gov.waiting(ScanStream::Balance, 0.1)); // ...but still waiting
auto ex = gov.beginExplicit(ScanStream::Balance, sig, 0.2); // explicit bypasses it
EXPECT_TRUE(static_cast<bool>(ex));
gov.complete(ex, 0.0, ScanOutcome::Skipped, 0.3);
gov.suppressRoutineWalletScans(false);
EXPECT_FALSE(gov.streamSuppressed(ScanStream::Balance));
EXPECT_TRUE(static_cast<bool>(gov.tryAdmit(ScanStream::Balance, true, sig, 0.4))); // resumes once unpaused
// Chat scans and Adhoc (the merges themselves) are never in the suppressed group.
EXPECT_FALSE(gov.streamSuppressed(ScanStream::ChatNotes));
EXPECT_FALSE(gov.streamSuppressed(ScanStream::Adhoc));
}
}
// The between-blocks probe's rotation: every z-address once (oldest-scanned first) before it is done.