fix(lite): surface sync failures instead of a frozen "Syncing 99.9%"; don't report a lost server as "Synced"
Lite-parity audit, sync-progress semantics (state/presentation only — no send/sync backend call changed): - A scan failure set scan->failed + lite_error, but no UI read lite_error, so the user saw "Syncing 99.9%" forever. The status bar now shows the error text on a lite scan/server failure. - After the initial catch-up, a server loss on the cheap probe path (ProbeFailed) left the last "Synced" model on screen. The worker now re-publishes that model demoted (not complete, reason "server unreachable"). - refreshModelImpl's height-read "synced" override no longer fires while syncstatus says the backend is still scanning or is indeterminate, and a server-bound read failure in a full cycle demotes to not-synced. - The syncstatus parser no longer treats a missing/unrecognised "syncing" field as fully synced: it reports an explicit "indeterminate" (not complete). Explicit "false" stays caught-up; the fake backend's syncing:true 1000/1000 behaviour is unchanged. Tests: parser cases for missing/odd/idle "syncing" (testLiteSyncStatus*). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1070,7 +1070,12 @@ std::optional<LiteWalletAppRefreshModel> LiteWalletController::refreshModelImpl(
|
||||
// returns just {"syncing":"false"}, so the mapped walletHeight is 0 and the status bar showed
|
||||
// "blocks: 0" when fully synced. Query the wallet's last-scanned height (a fast local read) and
|
||||
// surface it as the synced height/tip so the block count is correct at rest.
|
||||
if (bridge_ && (!honorStop || running_.load())) {
|
||||
// Do not force "synced" from the height read while syncstatus says the backend is still scanning (work remaining)
|
||||
// or could not be interpreted: the height only proves the last scanned block, not that we reached the tip.
|
||||
const bool backendBusy = syncResult.ok && !syncResult.syncStatus.complete &&
|
||||
(syncResult.syncStatus.indeterminate ||
|
||||
syncResult.syncStatus.syncedBlocks < syncResult.syncStatus.totalBlocks);
|
||||
if (bridge_ && !backendBusy && (!honorStop || running_.load())) {
|
||||
const auto h = bridge_->execute("height", "");
|
||||
if (h.ok) {
|
||||
const auto parsed = parseLiteHeightResponse(h.value);
|
||||
@@ -1094,7 +1099,14 @@ std::optional<LiteWalletAppRefreshModel> LiteWalletController::refreshModelImpl(
|
||||
model.sync.progress = std::min(model.sync.progress, 0.999);
|
||||
}
|
||||
}
|
||||
// A server-bound read failed this cycle (server lost after catch-up): not synced, and say why. Without this the
|
||||
// height override above kept reporting Synced because only a failed *scan* (scan_->failed) demoted it.
|
||||
if (refreshResult.syncReadFailed && model.hasSyncStatus) {
|
||||
model.sync.complete = false;
|
||||
model.sync.progress = std::min(model.sync.progress, 0.999);
|
||||
}
|
||||
model.syncError = scan_->errorCopy();
|
||||
if (model.syncError.empty() && refreshResult.syncReadFailed) model.syncError = "server unreachable";
|
||||
return model;
|
||||
}
|
||||
|
||||
@@ -1633,6 +1645,7 @@ void LiteWalletController::workerLoop()
|
||||
{
|
||||
int failStreak = 0; // consecutive cycles whose syncing reads hit a backend error
|
||||
RefreshGate gate; // worker-local; haveFull=false => the FIRST cycle is always a full refresh
|
||||
std::optional<LiteWalletAppRefreshModel> lastModel; // last published full model (to demote on a failed probe)
|
||||
while (running_.load()) {
|
||||
if (walletOpen_.load()) {
|
||||
const GateDecision decision = evaluateRefreshGate(gate, failStreak);
|
||||
@@ -1644,6 +1657,19 @@ void LiteWalletController::workerLoop()
|
||||
// run a second do_sync this cycle; treat it as a failed cycle so the existing back-off
|
||||
// applies and we don't double the dead-server stall. Next cycle takes the full path.
|
||||
failStreak += 1;
|
||||
// The probe path never re-derived sync state, so the last 'Synced' model stayed on screen while the
|
||||
// server was gone. Re-publish it demoted, with the reason.
|
||||
if (lastModel && running_.load()) {
|
||||
auto demoted = *lastModel;
|
||||
if (demoted.hasSyncStatus) {
|
||||
demoted.sync.complete = false;
|
||||
demoted.sync.progress = std::min(demoted.sync.progress, 0.999);
|
||||
}
|
||||
if (demoted.syncError.empty()) demoted.syncError = "server unreachable";
|
||||
lastModel = demoted;
|
||||
std::lock_guard<std::mutex> lock(modelMutex_);
|
||||
pendingModel_ = std::move(demoted);
|
||||
}
|
||||
} else { // DoFull
|
||||
// Sample the change baselines BEFORE the data commands (race rule).
|
||||
const std::uint64_t epoch0 = bridge_ ? bridge_->mutationEpoch() : 0;
|
||||
@@ -1661,6 +1687,7 @@ void LiteWalletController::workerLoop()
|
||||
gate.encrypted = m.encrypted;
|
||||
gate.locked = m.locked;
|
||||
gate.fullAt = std::chrono::steady_clock::now();
|
||||
lastModel = m;
|
||||
std::lock_guard<std::mutex> lock(modelMutex_);
|
||||
pendingModel_ = std::move(model);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user