fix(lite): fast retry when a server is only warming up (-28)

When the preferred lightwalletd server is reachable but warming up (JSON-RPC -28
/ "Activating best chain"), the failover treated it like a dead server and fell
through to the others, so the wallet didn't open until the next 20s retry — even
though the healthy server was ready within seconds.

Detect the warmup error during failover, flag it on the open outcome
(lastOpenWasWarmup()), and have the App retry on a short ~4s interval in that case
instead of 20s, so the wallet opens promptly once warmup clears. A unit test
covers a warming-preferred + dead-fallback open setting the flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 21:26:14 -05:00
parent dc07491abb
commit 3d4b013b0c
5 changed files with 61 additions and 8 deletions

View File

@@ -22,6 +22,16 @@ namespace wallet {
namespace {
constexpr double kZatoshisPerCoin = 100000000.0; // DRGX has 1e8 zatoshis per coin
// A lightwalletd open error that means "server is up but still warming up" (JSON-RPC -28 /
// "Activating best chain", "Loading"/"Verifying"/"Rescanning" phases) rather than a dead server.
// Such a server will be ready shortly, so the caller should retry it soon.
bool liteOpenErrorIsWarmup(const std::string& error)
{
const auto has = [&error](const char* s) { return error.find(s) != std::string::npos; };
return has("-28") || has("Activating best chain") || has("warming up") ||
has("Loading block") || has("Verifying blocks") || has("Rescanning");
}
// Extract a backend {"error":..} message (string or arbitrary JSON) into a plain string.
std::string extractJsonError(const nlohmann::json& value)
{
@@ -361,7 +371,10 @@ bool LiteWalletController::beginOpenExisting()
}
const std::string why = call.error.empty() ? "unreachable" : call.error;
liteLog(" " + url + ": " + why);
if (!call.error.empty()) outcome.error = call.error;
if (!call.error.empty()) {
outcome.error = call.error;
if (liteOpenErrorIsWarmup(call.error)) outcome.warming = true; // healthy, just starting
}
}
{
std::lock_guard<std::mutex> lk(*resultMutex);
@@ -386,6 +399,7 @@ void LiteWalletController::pumpAsyncOpen()
if (outcome.ok) {
walletOpen_ = true;
lastOpenError_.clear();
lastOpenWarming_ = false;
status_ = WalletBackendStatus{WalletBackendState::Ready, "wallet open", {}, {}, 0.0};
liteLog("Wallet opened via " + outcome.serverUrl);
if (persist_) persist_();
@@ -393,8 +407,10 @@ void LiteWalletController::pumpAsyncOpen()
startWorker(); // begin periodic refresh -> WalletState
} else {
lastOpenError_ = outcome.error;
lastOpenWarming_ = outcome.warming; // a healthy server was warming up -> retry sooner
status_ = WalletBackendStatus{WalletBackendState::Error, outcome.error, {}, {}, 0.0};
liteLog("Open failed: " + outcome.error);
liteLog("Open failed: " + outcome.error +
(outcome.warming ? " (a server is warming up — will retry shortly)" : ""));
}
}

View File

@@ -173,6 +173,10 @@ public:
void pumpAsyncOpen();
bool openInProgress() const { return openRunning_ && openRunning_->load(); }
const std::string& lastOpenError() const { return lastOpenError_; }
// True if the last failed open hit a server that was merely warming up (JSON-RPC -28 /
// "Activating best chain"): the server is healthy and will be ready shortly, so the caller
// should retry sooner rather than waiting out the normal interval.
bool lastOpenWasWarmup() const { return lastOpenWarming_; }
bool syncStarted() const { return syncStarted_; }
bool syncComplete() const { return syncDone_ && syncDone_->load(); }
@@ -290,13 +294,14 @@ private:
// the detached thread captures only shared_ptrs + value copies, never `this`, so it can
// safely outlive the controller). pumpAsyncOpen() finalizes the result on the main thread.
std::vector<std::string> failoverServerUrls() const;
struct OpenOutcome { bool ok = false; std::string serverUrl; std::string error; };
struct OpenOutcome { bool ok = false; bool warming = false; std::string serverUrl; std::string error; };
std::thread openThread_;
std::shared_ptr<std::atomic<bool>> openRunning_ = std::make_shared<std::atomic<bool>>(false);
std::shared_ptr<std::mutex> openResultMutex_ = std::make_shared<std::mutex>();
std::shared_ptr<std::optional<OpenOutcome>> openResult_ =
std::make_shared<std::optional<OpenOutcome>>();
std::string lastOpenError_; // main-thread only
bool lastOpenWarming_ = false; // last failed open hit a warming-up (-28) server
// Joinable background refresh worker (fast iterations: syncstatus, plus data once synced).
std::thread worker_;