fix(lite): non-blocking, non-hanging sync (Finding B)
The backend `sync` command is a blocking, uninterruptible full chain scan (do_sync(true); does not honor the shutdown flag), and balance/list block until synced. Previously startSync() ran on the main thread (would freeze wallet creation) and the worker could block, making the destructor join() hang at shutdown. Redesign: - bridge is now std::shared_ptr<LiteClientBridge>, shared with a detached sync thread so detaching is safe and litelib_shutdown isn't called while a running sync still holds the bridge; the controller's own ref prevents premature shutdown during normal operation. - startSync() launches the blocking `sync` on a detached thread (non-blocking; never joined). - refreshModel() gates on syncDone_: while syncing it publishes syncstatus progress only; once synced it does the full balance/addresses/list refresh (now fast). - destructor joins only the fast poll worker and detaches the sync thread -> no hang. - syncComplete() accessor added. Tests (deterministic, via a blocking-sync fake; counters made atomic for the detached thread): testLiteWalletControllerShutdownDoesNotHangDuringSync (destructor returns <1.5s with sync blocked); refresh/worker tests wait for syncComplete()/a balance-bearing model. Stable across repeated runs; lite+backend and full-node apps build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4634,6 +4634,12 @@ void testLiteWalletControllerRefreshPopulatesState()
|
||||
EXPECT_TRUE(controller.walletOpen());
|
||||
EXPECT_TRUE(controller.syncStarted()); // auto-started when the wallet became ready
|
||||
|
||||
// Sync runs on a detached thread; the full refresh (balance/addresses) only runs once it
|
||||
// completes. Wait for it (instant with the fake) so the refresh is deterministic.
|
||||
for (int i = 0; i < 500 && !controller.syncComplete(); ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
EXPECT_TRUE(controller.syncComplete());
|
||||
|
||||
dragonx::WalletState state;
|
||||
EXPECT_TRUE(controller.refreshWalletState(state));
|
||||
EXPECT_NEAR(state.privateBalance, 2.0, 1e-9);
|
||||
@@ -4692,14 +4698,20 @@ void testLiteWalletControllerWorkerProducesModel()
|
||||
LiteWalletController controller(caps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||
EXPECT_TRUE(controller.createWallet(LiteWalletCreateRequest{}).walletReady); // auto-starts the worker
|
||||
|
||||
// The worker refreshes immediately on start; poll briefly (<=2s) for the produced model.
|
||||
// The worker publishes progress-only models while syncing, then full models once synced.
|
||||
// Poll until a full (balance-bearing) model arrives (sync is instant with the fake).
|
||||
LiteWalletAppRefreshModel model;
|
||||
bool got = false;
|
||||
for (int i = 0; i < 200 && !got; ++i) {
|
||||
got = controller.takeRefreshedModel(model);
|
||||
if (!got) std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
bool gotFull = false;
|
||||
for (int i = 0; i < 500 && !gotFull; ++i) {
|
||||
LiteWalletAppRefreshModel m;
|
||||
if (controller.takeRefreshedModel(m) && m.hasBalance) {
|
||||
model = m;
|
||||
gotFull = true;
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
EXPECT_TRUE(got);
|
||||
EXPECT_TRUE(gotFull);
|
||||
EXPECT_TRUE(model.hasBalance);
|
||||
EXPECT_TRUE(model.hasAddresses);
|
||||
|
||||
@@ -4713,6 +4725,31 @@ void testLiteWalletControllerWorkerProducesModel()
|
||||
EXPECT_FALSE(idle.takeRefreshedModel(none));
|
||||
}
|
||||
|
||||
// M2b-3 hardening: the backend `sync` is a blocking, uninterruptible full scan. Destroying the
|
||||
// controller while a sync is in flight must NOT hang (the sync thread is detached, not joined).
|
||||
void testLiteWalletControllerShutdownDoesNotHangDuringSync()
|
||||
{
|
||||
using namespace dragonx::wallet;
|
||||
const auto caps = makeWalletCapabilities(WalletBuildKind::Lite, /*embeddedDaemon*/ false, /*liteBackendLinked*/ true);
|
||||
const auto conn = defaultLiteConnectionSettings();
|
||||
|
||||
dragonx::test::g_liteFakeSyncBlock.store(true); // make the backend "sync" block indefinitely
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
{
|
||||
LiteWalletController controller(caps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||
controller.createWallet(LiteWalletCreateRequest{}); // launches the (now-blocked) sync thread
|
||||
EXPECT_TRUE(controller.syncStarted());
|
||||
EXPECT_FALSE(controller.syncComplete());
|
||||
// controller destructs here with the sync thread still blocked -> must return promptly.
|
||||
}
|
||||
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
EXPECT_TRUE(elapsedMs < 1500); // did not wait for the (blocked) sync to finish
|
||||
|
||||
dragonx::test::g_liteFakeSyncBlock.store(false); // release the detached sync thread
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50)); // let it unwind cleanly
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -4752,6 +4789,7 @@ int main()
|
||||
testLiteSyncStatusParserRealShapes();
|
||||
testLiteWalletControllerRefreshPopulatesState();
|
||||
testLiteWalletControllerWorkerProducesModel();
|
||||
testLiteWalletControllerShutdownDoesNotHangDuringSync();
|
||||
testLiteBridgeRuntimeShutdownIsIdempotent();
|
||||
testLiteBridgeRuntimeDestructorCallsShutdownOnce();
|
||||
testLiteBridgeRuntimeShutdownWaitsForOwnedStringRelease();
|
||||
|
||||
Reference in New Issue
Block a user