fix(walletapi): run SyncHistory in goroutine to avoid blocking sync loop#26
fix(walletapi): run SyncHistory in goroutine to avoid blocking sync loop#26moralpriest wants to merge 1 commit into
Conversation
SyncHistory performs a binary-search scan of the blockchain to find transactions affecting the wallet, making sequential RPC calls that can take minutes on restored wallets. When called synchronously inside Sync_Wallet_Memory_With_Daemon_internal, it blocks the sync loop and prevents daemon_height updates until completed. Running SyncHistory as a goroutine allows the sync loop to continue polling and updating heights while history is scanned in the background.
Dirtybird99
left a comment
There was a problem hiding this comment.
Reviewed against the community-dev base. The goal is right — a minutes-long SyncHistory shouldn't freeze the sync loop — but as-is this introduces two crash-class races:
- Standalone, this is a fatal crash, not a panic.
sync_loopiterates the entries map (for k := range w.account.EntriesNative, daemon_communication.go:209) while the new goroutine writes it (w.account.EntriesNative[scid] = entries, lines 598/603). Concurrent map iteration + write is a Go runtime fatal throw — the deferredrecover()insideSyncHistorycannot catch it; the whole wallet process dies. PR #30 removes that loop, so this PR hard-depends on #30 landing first — worth stating in the description and enforcing in merge order. - Even with #30 merged,
save_if_disk()→Save_Wallet()→json.Marshal(w.account)iteratesEntriesNativewith no lock — and it runs both periodically insync_loopand on the line right aftergo w.SyncHistory(scid).SyncHistoryholds onlysync_multilock; the save path holds neither. Same fatal class — or a torn account snapshot encrypted to disk. The map/slice mutations inSyncHistoryand the marshal inSave_Walletneed a common mutex (e.g. wrap SyncHistory'sEntriesNativewrites inw.Lock()and take the same lock around the account marshal). - Minor: every balance change spawns a goroutine that queues on
sync_multilock; during a long restore these pile up unboundedly. A coalescing guard fixes that and shrinks the race window (see inline comment).
| w.account.Balance[scid] = b | ||
| w.Unlock() | ||
| w.SyncHistory(scid) // also update statement | ||
| go w.SyncHistory(scid) // also update statement (run in background to avoid blocking sync loop) |
There was a problem hiding this comment.
Suggest coalescing instead of unconditional spawn, so repeated balance changes during a long scan don't queue goroutines on sync_multilock (needs a synchistory_pending int32 field and sync/atomic import):
if atomic.CompareAndSwapInt32(&w.synchistory_pending, 0, 1) {
go func() {
defer atomic.StoreInt32(&w.synchistory_pending, 0)
w.SyncHistory(scid)
}()
}|
Follow-up to my review above — this is the concrete locking defect that makes SummaryCommit 6f9bd2c converted the transaction-history readers ( Writer (holds only derohe/walletapi/daemon_communication.go Lines 596 to 604 in 6f9bd2c Readers (RLock only — no shared lock with the writer): Lines 266 to 268 in 6f9bd2c Additionally, derohe/walletapi/wallet_memory.go Line 283 in 6f9bd2c Impact
FixHave |
SyncHistory performs a binary-search scan of the blockchain to find transactions affecting the wallet, making sequential RPC calls that can take minutes on restored wallets. When called synchronously inside Sync_Wallet_Memory_With_Daemon_internal, it blocks the sync loop and prevents daemon_height updates until completed.
Problem
After wallet restoration, SyncHistory takes minutes to complete. During this time the sync loop is frozen — no height updates, no balance updates, sync indicator appears stuck.
Changes
w.SyncHistory(scid)togo w.SyncHistory(scid)Notes