fix(lite): flush the wallet save on quit before _Exit

On quit the lite controller's destructor never runs, because main()
ends the process with _Exit(). Any state the background refresh had
scanned since the last save was lost: the destructor is where that
final save would normally happen, and it was being skipped on the
one path that always runs.

- shutdown() now calls a new flushOnExit() that persists once on the
  way out, bounded to 4 s so the existing 15 s shutdown watchdog
  budget still holds. On timeout it abandons the save thread and lets
  _Exit follow.
- It is a plain save through the same backend path, which writes a
  temp file and renames it into place. That cannot corrupt the wallet
  if it is cut off or abandoned, and it does not change the wallet
  file format - it only persists sooner on the quit path.
- It reuses the destructor's guards and also skips when a wallet op
  is already in progress: shutdown() has already waited for ops to go
  idle, and a still-pending op holds the sync lock and issues its own
  save, so flushOnExit would only block the budget on a save that
  cannot take the lock.

Tests cover a save on the open, synced, idle path (exactly once), a
blocked save returning within the budget, and the retired and
never-opened guards skipping the save.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 07:51:34 -05:00
co-authored by Claude Opus 4.8
parent e2210cf5d3
commit d9dae45c39
4 changed files with 92 additions and 1 deletions
+4
View File
@@ -175,6 +175,10 @@ public:
~LiteWalletController(); // stops + joins the background refresh worker
// Best-effort persist on the quit path (the destructor's save is skipped because main() _Exit()s).
// Bounded: abandons the save thread on timeout; the process is about to _Exit. Same guards as ~dtor.
void flushOnExit(std::chrono::milliseconds budget);
LiteWalletController(const LiteWalletController&) = delete;
LiteWalletController& operator=(const LiteWalletController&) = delete;