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:
@@ -95,12 +95,12 @@ LiteWalletController::LiteWalletController(WalletCapabilities capabilities,
|
||||
LiteConnectionSettings connectionSettings,
|
||||
LiteClientBridge bridge,
|
||||
LiteWalletControllerOptions options)
|
||||
: bridge_(std::move(bridge)),
|
||||
lifecycle_(capabilities, connectionSettings, &bridge_,
|
||||
: bridge_(std::make_shared<LiteClientBridge>(std::move(bridge))),
|
||||
lifecycle_(capabilities, connectionSettings, bridge_.get(),
|
||||
LiteWalletLifecycleOptions{options.allowBridgeCalls}),
|
||||
gateway_(capabilities, connectionSettings, &bridge_,
|
||||
gateway_(capabilities, connectionSettings, bridge_.get(),
|
||||
LiteWalletGatewayOptions{options.allowBridgeCalls}),
|
||||
sync_(capabilities, connectionSettings, &bridge_,
|
||||
sync_(capabilities, connectionSettings, bridge_.get(),
|
||||
LiteSyncServiceOptions{options.allowBridgeCalls})
|
||||
{
|
||||
status_ = lifecycle_.status();
|
||||
@@ -108,7 +108,11 @@ LiteWalletController::LiteWalletController(WalletCapabilities capabilities,
|
||||
|
||||
LiteWalletController::~LiteWalletController()
|
||||
{
|
||||
stopWorker();
|
||||
stopWorker(); // joins the fast poll worker (short iterations)
|
||||
// The sync thread may be blocked in an uninterruptible full scan; detach it. It holds
|
||||
// shared refs (bridge_ + syncDone_), so it stays safe and the bridge survives until it
|
||||
// finishes — the process is exiting, so a late litelib_shutdown is harmless.
|
||||
if (syncThread_.joinable()) syncThread_.detach();
|
||||
}
|
||||
|
||||
std::unique_ptr<LiteWalletController> LiteWalletController::createLinked(
|
||||
@@ -133,30 +137,51 @@ void LiteWalletController::onLifecycleResult(const LiteWalletLifecycleResult& re
|
||||
}
|
||||
}
|
||||
|
||||
LiteSyncStartResult LiteWalletController::startSync()
|
||||
void LiteWalletController::startSync()
|
||||
{
|
||||
auto result = sync_.startSync(LiteSyncStartRequest{});
|
||||
if (result.syncStarted) syncStarted_ = true;
|
||||
return result;
|
||||
if (syncLaunched_) return;
|
||||
syncLaunched_ = true;
|
||||
syncStarted_ = true;
|
||||
// The backend `sync` command is a blocking, uninterruptible full chain scan, so run it on
|
||||
// a detached thread. Capture shared refs (not the controller) so it is safe to outlive us.
|
||||
auto bridge = bridge_;
|
||||
auto done = syncDone_;
|
||||
syncThread_ = std::thread([bridge, done] {
|
||||
if (bridge) bridge->execute("sync", ""); // blocks until synced (or errors out)
|
||||
done->store(true);
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<LiteWalletAppRefreshModel> LiteWalletController::refreshModel()
|
||||
{
|
||||
if (!walletOpen_.load()) return std::nullopt;
|
||||
|
||||
// Poll sync status first so the refresh bundle (and the mapped sync model) carries it.
|
||||
LiteWalletRefreshRequest request;
|
||||
// syncstatus is fast (reads shared state the sync thread updates). Poll it every time.
|
||||
const auto syncResult = sync_.pollSyncStatus(LiteSyncStatusRequest{});
|
||||
|
||||
if (!syncDone_->load()) {
|
||||
// Sync still running: publish progress only. Data queries (balance/list) would block
|
||||
// until the chain is synced, so don't issue them yet.
|
||||
if (!syncResult.ok) return std::nullopt;
|
||||
LiteWalletAppRefreshModel model;
|
||||
model.hasSyncStatus = true;
|
||||
model.sync.walletHeight = syncResult.syncStatus.syncedBlocks;
|
||||
model.sync.chainHeight = syncResult.syncStatus.totalBlocks;
|
||||
model.sync.progress = syncResult.syncStatus.progress;
|
||||
model.sync.complete = syncResult.syncStatus.complete;
|
||||
return model;
|
||||
}
|
||||
|
||||
// Synced: full refresh (balance/addresses/transactions are fast now).
|
||||
LiteWalletRefreshRequest request;
|
||||
if (syncResult.ok) {
|
||||
request.haveSyncStatus = true;
|
||||
request.syncStatus = syncResult.syncStatus;
|
||||
}
|
||||
|
||||
const auto refreshResult = gateway_.refresh(request);
|
||||
if (refreshResult.bundle.successfulCommandCount == 0 && !request.haveSyncStatus) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto mapped = mapLiteWalletRefreshResult(refreshResult);
|
||||
if (!mapped.ok) return std::nullopt;
|
||||
return mapped.model;
|
||||
|
||||
Reference in New Issue
Block a user