perf(history): weekly safety-net full re-sweep for sent history
Persisting the sent-sweep watermark means a relaunch runs a windowed sweep, which can't see a change made outside the GUI (a Console z_importviewingkey, an external same-seed restore) that adds an old send below the window. This adds a periodic full re-sweep so that case is still caught within ~7 days. - sentSweepSafetyNetDue(now, lastFull) = lastFull > 0 && now - lastFull > 7d (a wallet that has never full-swept doesn't trigger one; the seeding sweep records the time). lastFullTime / lastFullHeight ride in the sent_sweep snapshot; any successful full sweep (seed, verify-mismatch or safety-net) resets the clock. On load, a valid watermark without a clock starts it now, so even watermarks seeded before this field eventually re-sweep. - When due, the snapshot forces a full sweep for that cycle. Gated on: a watermark exists, a routine (non-syncing) cycle, not the Mining page, and not in full-sweep backoff -- so a full sweep that can't finish on a slow daemon backs off instead of retrying every cycle. Degrades gracefully: ~0.4 s on a note-cache daemon; callLong(120 s) with backoff on an old one (the deferred banding would keep each cs_main hold short so it fits there too). Helper + persistence covered by tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+22
-3
@@ -2023,6 +2023,12 @@ void App::loadTransactionHistoryCacheIfAvailable()
|
||||
history_full_sweep_failures_ = loaded.sentSweep.fullFailures;
|
||||
history_full_sweep_retry_at_ = loaded.sentSweep.fullRetryDelaySec > 0
|
||||
? std::time(nullptr) + loaded.sentSweep.fullRetryDelaySec : 0;
|
||||
history_sent_last_full_time_ = loaded.sentSweep.lastFullTime;
|
||||
history_sent_last_full_height_ = loaded.sentSweep.lastFullHeight;
|
||||
// Start the safety-net clock if a watermark came back without one (an older cache, or one seeded before
|
||||
// this field existed), so a windowed-only wallet still gets a full re-sweep ~7 days from now.
|
||||
if (history_sent_last_full_time_ == 0 && loaded.sentSweep.valid())
|
||||
history_sent_last_full_time_ = std::time(nullptr);
|
||||
++history_sent_epoch_; // the loaded watermark replaces any in-flight sweep's result
|
||||
transaction_history_cache_loaded_ = true;
|
||||
// What is on disk is exactly what was just loaded: the first identical refresh must not rewrite it.
|
||||
@@ -2109,6 +2115,8 @@ void App::storeTransactionHistoryCacheIfAvailable(data::HistoryStoreMode mode)
|
||||
job->sentSweep.fullRetryDelaySec = history_full_sweep_retry_at_ > nowSec
|
||||
? static_cast<long>(history_full_sweep_retry_at_ - nowSec) : 0;
|
||||
}
|
||||
job->sentSweep.lastFullTime = history_sent_last_full_time_;
|
||||
job->sentSweep.lastFullHeight = history_sent_last_full_height_;
|
||||
|
||||
auto* cache = &transaction_history_cache_; // App member: outlives every job (see app.h)
|
||||
history_writer_.submit("tx-history", [cache, job]() { return cache->writeSnapshot(std::move(*job)); });
|
||||
@@ -2811,8 +2819,15 @@ void App::startTransactionCycle(bool full, bool routineAllowed, services::ScanTi
|
||||
if (!chatZ.empty() && listed(chatZ)) transactionSnapshot.priorityScanFloors[chatZ] = currentBlocks;
|
||||
}
|
||||
}
|
||||
transactionSnapshot.sentScanFromHeight = history_sent_scan_height_;
|
||||
transactionSnapshot.sentVerifyPending = history_sent_verify_pending_;
|
||||
// Weekly safety net: on a watermarked wallet, force a full re-sweep now and then so a change made
|
||||
// outside the GUI (a Console import, an external same-seed restore) that the per-event guards can't
|
||||
// see is still caught within ~7 days. Not on the Mining page; needs a routine (non-syncing) cycle.
|
||||
const bool sentSafetyNetDue = routineAllowed && history_sent_scan_height_ >= 0 &&
|
||||
current_page_ != ui::NavPage::Mining &&
|
||||
std::time(nullptr) >= history_full_sweep_retry_at_ && // a failed full sweep backs off, not retries
|
||||
services::sentSweepSafetyNetDue(std::time(nullptr), history_sent_last_full_time_);
|
||||
transactionSnapshot.sentScanFromHeight = sentSafetyNetDue ? -1 : history_sent_scan_height_;
|
||||
transactionSnapshot.sentVerifyPending = history_sent_verify_pending_ && !sentSafetyNetDue;
|
||||
transactionSnapshot.sentVerifyAnchorHeight = history_sent_anchor_height_;
|
||||
transactionSnapshot.sentVerifyAnchorHash = history_sent_anchor_hash_;
|
||||
// A windowed sweep mid-pass too, now and then, so an untracked send (the Console, another wallet on
|
||||
@@ -2826,7 +2841,7 @@ void App::startTransactionCycle(bool full, bool routineAllowed, services::ScanTi
|
||||
// and backs off after whole-wallet sweeps that failed.
|
||||
transactionSnapshot.skipSentSweep =
|
||||
(!routineAllowed && initial_history_scan_complete_) ||
|
||||
(history_sent_scan_height_ >= 0 && history_sent_scan_height_ == currentBlocks) ||
|
||||
(!sentSafetyNetDue && history_sent_scan_height_ >= 0 && history_sent_scan_height_ == currentBlocks) ||
|
||||
(history_sent_scan_height_ < 0 && std::time(nullptr) < history_full_sweep_retry_at_);
|
||||
} else {
|
||||
// Light: rows now (a send, a mutation), no wallet-wide z-scans.
|
||||
@@ -2887,6 +2902,10 @@ void App::startTransactionCycle(bool full, bool routineAllowed, services::ScanTi
|
||||
history_sent_verify_pending_ = false; // this sweep covered the window; nothing owed
|
||||
history_full_sweep_failures_ = 0;
|
||||
history_full_sweep_retry_at_ = 0;
|
||||
if (result.sentSweepFull) { // a full sweep resets the safety-net clock
|
||||
history_sent_last_full_time_ = now;
|
||||
history_sent_last_full_height_ = result.sentSweepHeight;
|
||||
}
|
||||
} else if (result.sentSweepRan && result.sentSweepFull &&
|
||||
services::fullSentSweepFailureCounts(result.sentSweepErrKind)) {
|
||||
++history_full_sweep_failures_;
|
||||
|
||||
Reference in New Issue
Block a user