fix(sync): bound keep-alive ping and resume a sync stuck on lock/unlock
Three issues surfaced by testing the previous lock/unlock reconnect fix on Android with a large wallet: - The keep-alive ping had no timeout, so a "half-open" TCP connection (remote end gone with no FIN/RST ever delivered, the common outcome of Doze/mobile-radio suspend after a longer lock) left it awaiting a response that never arrives — the failure path that tears the dead client down was never reached. Bound the ping to 8s and added a re-entrancy guard so overlapping 20s ticks can't pile up concurrent reconnect attempts while one is stuck. - CheckConnectionOnResumeAsync bailed out whenever a sync was already in progress, so a lock/unlock during an active sync (which has no per-request timeout of its own) left it hung indefinitely instead of recovering. It now cancels the stuck sync and tears down the dead client, marking the interruption as self-inflicted (_resumeRecovering) so the UI shows "reconnecting" and restarts immediately instead of a transient error message. - WalletSynchronizer.ExportCaches persisted raw transaction bytes only for already-verified transactions, discarding anything downloaded but not yet through Merkle-proof verification when a sync was interrupted mid-way — forcing a full re-download of a large wallet's transactions on every resume instead of resuming straight into proof verification. Track confirmed txids at download time (_confirmedTxids), independent of verification status, and export/preload against that instead. Also unified the three sync progress messages onto one template reporting against the sync's total transaction/proof counts rather than this session's download count, so a resumed sync immediately shows "transactions n/n" instead of a misleading "0/0" before jumping into proof verification.
This commit is contained in:
1 parent
94a474fe41
commit
4cd5fab736
3 files changed
+125
-35
No files matched your search
@@ -77,6 +77,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
// ---- keep-alive ----
|
||||
private bool _autoReconnect;
|
||||
private bool _syncFailed;
|
||||
private bool _resumeRecovering;
|
||||
private readonly DispatcherTimer _keepAliveTimer;
|
||||
|
||||
// ---- server UI sync ----
|
||||
@@ -161,41 +162,60 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
_ = CheckForUpdatesAsync();
|
||||
}
|
||||
|
||||
private bool _keepAliveRunning;
|
||||
|
||||
private async System.Threading.Tasks.Task KeepAliveTickAsync()
|
||||
{
|
||||
if (IsSyncing)
|
||||
// Re-entrancy guard: without it, a ping stuck on a half-open socket (see
|
||||
// below) would let every subsequent 20s tick pile up another concurrent
|
||||
// ping/reconnect attempt on top of it.
|
||||
if (IsSyncing || _keepAliveRunning)
|
||||
return;
|
||||
if (_client is { IsConnected: true } client)
|
||||
_keepAliveRunning = true;
|
||||
try
|
||||
{
|
||||
// If the wallet is open and the last sync failed, retry automatically.
|
||||
if (_syncFailed && _account is not null)
|
||||
if (_client is { IsConnected: true } client)
|
||||
{
|
||||
await ConnectAndSync();
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
await client.PingAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// TcpClient.Connected only reflects the last known socket state, so a
|
||||
// connection killed silently while the app was suspended (e.g. Android
|
||||
// Doze after screen lock) still reports IsConnected == true. A failed
|
||||
// ping is the only reliable signal here: tear the dead client down so
|
||||
// the next tick reconnects instead of retrying forever on a dead socket.
|
||||
await DisconnectAsync();
|
||||
if (_autoReconnect)
|
||||
// If the wallet is open and the last sync failed, retry automatically.
|
||||
if (_syncFailed && _account is not null)
|
||||
{
|
||||
ConnectionStatus = Loc.Tr("conn.reconnecting");
|
||||
await ConnectAndSync();
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
// A "half-open" TCP connection (remote end gone with no FIN/RST
|
||||
// ever delivered — the common outcome of Android Doze/mobile-radio
|
||||
// suspend killing the route silently) never fails the write, so
|
||||
// PingAsync would otherwise await a response that never arrives.
|
||||
// Bound it explicitly instead of relying on it to throw.
|
||||
using var timeoutCts = new System.Threading.CancellationTokenSource(System.TimeSpan.FromSeconds(8));
|
||||
await client.PingAsync(timeoutCts.Token);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// TcpClient.Connected only reflects the last known socket state, so a
|
||||
// connection killed silently while the app was suspended still reports
|
||||
// IsConnected == true. A failed/timed-out ping is the only reliable
|
||||
// signal here: tear the dead client down so the next tick reconnects
|
||||
// instead of retrying forever on a dead socket.
|
||||
await DisconnectAsync();
|
||||
if (_autoReconnect)
|
||||
{
|
||||
ConnectionStatus = Loc.Tr("conn.reconnecting");
|
||||
await ConnectAndSync();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_autoReconnect)
|
||||
{
|
||||
ConnectionStatus = Loc.Tr("conn.reconnecting");
|
||||
await ConnectAndSync();
|
||||
}
|
||||
}
|
||||
else if (_autoReconnect)
|
||||
finally
|
||||
{
|
||||
ConnectionStatus = Loc.Tr("conn.reconnecting");
|
||||
await ConnectAndSync();
|
||||
_keepAliveRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +226,35 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
/// during Doze).</summary>
|
||||
public async System.Threading.Tasks.Task CheckConnectionOnResumeAsync()
|
||||
{
|
||||
if (IsSyncing) return;
|
||||
if (IsSyncing)
|
||||
{
|
||||
// A sync in progress when the phone locked may be stuck awaiting a
|
||||
// response on a socket that died silently while suspended (same
|
||||
// half-open-TCP scenario as the keep-alive ping, but sync requests have
|
||||
// no timeout of their own). Cancel it and tear the client down instead
|
||||
// of leaving it hung forever. _resumeRecovering tells ConnectAndSync's
|
||||
// error handling that this interruption is self-inflicted and expected,
|
||||
// so it shows "reconnecting" and restarts immediately instead of
|
||||
// flashing a scary error message and waiting for the next keep-alive tick.
|
||||
_resumeRecovering = true;
|
||||
ConnectionStatus = Loc.Tr("conn.reconnecting");
|
||||
ConnectionStatusShort = Loc.Tr("conn.reconnecting");
|
||||
_syncCts.Cancel();
|
||||
// Deliberately not DisconnectAsync() here: it also nulls _synchronizer,
|
||||
// which still holds whatever this sync already downloaded/verified (e.g.
|
||||
// thousands of Merkle proofs on a large wallet). Nulling it before the
|
||||
// cancelled ConnectAndSync unwinds would discard that progress, forcing a
|
||||
// full restart instead of a resume. Only tear down the dead socket here;
|
||||
// ConnectAndSync's own cancellation handling persists the partial cache
|
||||
// (PersistPartialTxCache) before it recreates the synchronizer — same
|
||||
// sequencing already used for the "server changed mid-sync" case.
|
||||
if (_client is { } deadClient)
|
||||
{
|
||||
_client = null;
|
||||
try { await deadClient.DisposeAsync(); } catch { }
|
||||
}
|
||||
return;
|
||||
}
|
||||
await KeepAliveTickAsync();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user