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

@@ -40,6 +40,8 @@ inline std::atomic<bool> g_liteFakeEncrypted{false}; // wallet-encryption state
inline std::atomic<bool> g_liteFakeLocked{false}; // spending-keys-locked state
// initialize_existing fails for any server URL containing this substring (open-failover tests).
inline std::string g_liteFakeDeadServerSubstr;
// initialize_existing returns a warming-up (-28) error for URLs containing this substring.
inline std::string g_liteFakeWarmupServerSubstr;
inline void resetLiteFakeCounters()
{
@@ -47,6 +49,7 @@ inline void resetLiteFakeCounters()
g_liteFakeFreed = 0;
g_liteFakeShutdownCalled = false;
g_liteFakeDeadServerSubstr.clear();
g_liteFakeWarmupServerSubstr.clear();
}
inline char* liteFakeDup(const char* s)
@@ -74,9 +77,18 @@ inline char* liteFakeInitFromPhrase(bool, const char*, const char*,
// g_liteFakeDeadServerSubstr, so tests can exercise the controller's open-with-failover.
inline char* liteFakeInitExisting(bool, const char* server)
{
if (!g_liteFakeDeadServerSubstr.empty() && server &&
std::string(server).find(g_liteFakeDeadServerSubstr) != std::string::npos) {
return liteFakeDup("Error: could not connect to server"); // bridge maps to ok=false
if (server) {
const std::string s(server);
if (!g_liteFakeWarmupServerSubstr.empty() &&
s.find(g_liteFakeWarmupServerSubstr) != std::string::npos) {
// Server is up but warming up — mirrors the real -28 / "Activating best chain".
return liteFakeDup("Error: grpc-message: \"error requesting block: -28: "
"Activating best chain...\"");
}
if (!g_liteFakeDeadServerSubstr.empty() &&
s.find(g_liteFakeDeadServerSubstr) != std::string::npos) {
return liteFakeDup("Error: could not connect to server"); // bridge maps to ok=false
}
}
return liteFakeDup("OK");
}

View File

@@ -3599,8 +3599,26 @@ void testLiteWalletControllerOpenFailover()
EXPECT_TRUE(!controller.lastOpenError().empty());
}
// A warming-up (-28) server is flagged so the caller can retry sooner. Preferred server is
// warming, the fallback is dead -> open fails but lastOpenWasWarmup() is set.
{
dragonx::test::resetLiteFakeCounters();
dragonx::test::g_liteFakeWalletExists = true;
dragonx::test::g_liteFakeWarmupServerSubstr = "dead.example"; // preferred (sticky) is warming
dragonx::test::g_liteFakeDeadServerSubstr = "good.example"; // fallback unreachable
LiteWalletController controller(liteCaps, conn,
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
EXPECT_TRUE(controller.beginOpenExisting());
for (int i = 0; i < 400 && controller.openInProgress(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(5));
controller.pumpAsyncOpen();
EXPECT_FALSE(controller.walletOpen());
EXPECT_TRUE(controller.lastOpenWasWarmup());
}
dragonx::test::g_liteFakeWalletExists = false;
dragonx::test::g_liteFakeDeadServerSubstr.clear();
dragonx::test::g_liteFakeWarmupServerSubstr.clear();
}
// M2: a parsed lite refresh bundle maps through to the app's WalletState (the last hop