fix(rpc): run rescans and key imports without the 30 s timeout
A rescan or key import holds the node's cs_main for the whole wallet scan. On the shared clients it hit the 30 s curl timeout, reported a failure that wasn't one, abandoned a daemon RPC slot and flipped the app into "node busy". - One long wallet RPC at a time (rescan, importprivkey, importaddress, importwallet, z_importkey, z_importviewingkey, z_importwallet) runs on its own ungated client with no client timeout (rpc/long_call_lane.h). It can be aborted from any thread, and an abort is never lost, even one that lands before the client opens. - The GUI rescan, key import and sweep use it. The sweep's fund-moving body is unchanged; only the client it calls differs. runtimeRescan() returns bool, and a refused post-bootstrap reconcile stays armed. - The console routes rescan and the import commands through the lane; its input stays usable meanwhile, and a refusal is printed like any other result. RPCClient gains callRaw(.., timeoutSec). - Restart, wallet switch, delete-chain, bootstrap (now also from Settings) and the other node-stopping flows are refused while it runs, with a message saying a key import or rescan is still running; quit asks for confirmation. The lane is aborted at quit. - A second import or rescan is refused before the key is copied anywhere. An import cut off after the request reached the node (a lost reply, or a proxy's 502/504 while the node still imports) says the key may already be in the wallet and to run Settings -> Rescan. Only the node's own HTTP-layer refusals (401, 503, work queue full, unhandled request) say that the key was not imported. A pure classifyLongCallCutOff() makes that choice. - New strings in all 8 languages; console results stay English. Tests: the long-method list and the console's routing; one lane at a time; an abort lands whether it comes before the client opens or during the call; a call with no client timeout outlasts a 3 s scan, where a 1 s gated client times out and marks the node busy; classifyLongCallCutOff() for every failure kind with and without a sent request, and from raw HTTP replies (502, 504, a plain 500 -> cut off; 401, 503, queue full, unhandled -> refused). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+28
-7
@@ -1513,7 +1513,7 @@ void App::update()
|
||||
std::chrono::steady_clock::now() >= post_bootstrap_probe_retry_at_ &&
|
||||
rpcConnected && state_.connected && !rpcBusy &&
|
||||
!state_.warming_up && !state_.isLocked() && !state_.sync.rescanning &&
|
||||
!runtime_rescan_active_ && !bootstrap_downloading_ &&
|
||||
!runtime_rescan_active_ && !long_lane_.active() && !bootstrap_downloading_ &&
|
||||
state_.sync.blocks > 1) { // wait until the tip is known so the probe has a real range
|
||||
if (post_bootstrap_probe_attempts_ == 0)
|
||||
ui::Notifications::instance().info(TR("appx_bootstrap_complete_reconciling"));
|
||||
@@ -1527,7 +1527,12 @@ void App::update()
|
||||
post_bootstrap_rescan_pending_ = false;
|
||||
post_bootstrap_probe_attempts_ = 0;
|
||||
if (!fullHistory) {
|
||||
runtimeRescan(lowest); // bootstrapped/pruned: rescan from the snapshot base
|
||||
// bootstrapped/pruned: rescan from the snapshot base. Refused (a key import or rescan is still
|
||||
// running, the node is busy): keep the reconcile armed and try again in 30 s.
|
||||
if (!runtimeRescan(lowest)) {
|
||||
post_bootstrap_rescan_pending_ = true;
|
||||
post_bootstrap_probe_retry_at_ = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
}
|
||||
} else {
|
||||
rescanBlockchain(); // full-history node: the -rescan restart works
|
||||
}
|
||||
@@ -6456,7 +6461,8 @@ void App::beginShutdown()
|
||||
// with shutdown_confirmed_ set (and, for "keep node running", shutdown_keep_daemon_override_).
|
||||
if (!shutdown_confirmed_ && shouldConfirmDaemonStop()) {
|
||||
DEBUG_LOGF("beginShutdown: the node is %s — asking before stopping it\n",
|
||||
node_rebuilding_ ? "rebuilding witnesses" : "too busy to answer RPC");
|
||||
node_rebuilding_ ? "rebuilding witnesses"
|
||||
: long_lane_.active() ? "running a key import or rescan" : "too busy to answer RPC");
|
||||
pending_shutdown_confirm_ = true;
|
||||
// The modal only renders in a visible frame: a quit from the taskbar while minimized (or behind
|
||||
// other windows) would otherwise wait on a prompt nobody can see.
|
||||
@@ -6490,6 +6496,7 @@ void App::beginShutdown()
|
||||
if (rpc_) rpc_->requestAbort();
|
||||
if (fast_rpc_) fast_rpc_->requestAbort();
|
||||
abortRpcBusyCanary();
|
||||
long_lane_.abort(); // a key import / rescan waiting with no client timeout; the node's own stop ends its scan
|
||||
if (worker_) {
|
||||
worker_->requestStop();
|
||||
}
|
||||
@@ -6811,12 +6818,19 @@ bool App::nodeBusyForStop() const
|
||||
// external node's busyness shows only as RPC that has stopped answering.
|
||||
daemon::DaemonBusyTracker ours;
|
||||
if (busyTrackerIsLive()) ours = daemon_busy_;
|
||||
return daemon::shouldConfirmStop(true, ours, rpcBusyWhileAlive());
|
||||
// A key import or rescan on the long-call lane (C61) counts too: stopping the node would cut it off, and
|
||||
// aborting the wallet's wait would not stop the node's scan, so the stop would wait for it anyway.
|
||||
return daemon::shouldConfirmStop(true, ours, rpcBusyWhileAlive() || long_lane_.active());
|
||||
}
|
||||
|
||||
bool App::refuseIfNodeBusy()
|
||||
{
|
||||
if (!nodeBusyForStop()) return false;
|
||||
if (long_lane_.active()) {
|
||||
DEBUG_LOGF("[App] refused: a key import or rescan is still running — stopping the node now would cut it off\n");
|
||||
ui::Notifications::instance().warning(TR("appx_wallet_op_running"), 10.0f);
|
||||
return true;
|
||||
}
|
||||
DEBUG_LOGF("[App] refused: the node is busy (%s) — stopping it now would make it start over\n",
|
||||
daemon_busy_.rebuilding() ? "rebuilding witnesses" : "not answering RPC");
|
||||
ui::Notifications::instance().warning(TR("appx_node_busy_try_later"), 10.0f);
|
||||
@@ -6847,13 +6861,18 @@ void App::renderDaemonStopConfirm()
|
||||
elapsed = formatBusyDuration(std::chrono::duration<double>(
|
||||
std::chrono::steady_clock::now() - rpc_busy_since_).count());
|
||||
const bool rebuilding = busy.rebuilding;
|
||||
const bool walletOp = !rebuilding && long_lane_.active(); // a key import or rescan (C61)
|
||||
if (Type().subtitle1()) ImGui::PushFont(Type().subtitle1());
|
||||
ImGui::TextUnformatted(TR(rebuilding ? "stopconfirm_rebuild_title" : "stopconfirm_busy_title"));
|
||||
ImGui::TextUnformatted(TR(rebuilding ? "stopconfirm_rebuild_title"
|
||||
: walletOp ? "stopconfirm_walletop_title" : "stopconfirm_busy_title"));
|
||||
if (Type().subtitle1()) ImGui::PopFont();
|
||||
ImGui::Spacing();
|
||||
char body[1024];
|
||||
snprintf(body, sizeof(body), TR(rebuilding ? "stopconfirm_rebuild_body" : "stopconfirm_busy_body"),
|
||||
elapsed.empty() ? "0:00" : elapsed.c_str());
|
||||
if (walletOp)
|
||||
snprintf(body, sizeof(body), "%s", TR("stopconfirm_walletop_body"));
|
||||
else
|
||||
snprintf(body, sizeof(body), TR(rebuilding ? "stopconfirm_rebuild_body" : "stopconfirm_busy_body"),
|
||||
elapsed.empty() ? "0:00" : elapsed.c_str());
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f);
|
||||
ImGui::TextUnformatted(body);
|
||||
ImGui::PopTextWrapPos();
|
||||
@@ -7627,6 +7646,7 @@ void App::shutdown()
|
||||
DEBUG_LOGF("Synchronous shutdown fallback...\n");
|
||||
http_cancel_.store(true, std::memory_order_relaxed); // queued / in-flight market GETs end at once
|
||||
async_tasks_.cancelAll();
|
||||
long_lane_.abort(); // else joinAll() waits out a key import / rescan (no client timeout)
|
||||
async_tasks_.joinAll();
|
||||
if (rpc_) rpc_->requestAbort(); // unblock any in-flight curl before joining
|
||||
if (fast_rpc_) fast_rpc_->requestAbort();
|
||||
@@ -7659,6 +7679,7 @@ void App::shutdown()
|
||||
if (shutdown_thread_.joinable()) {
|
||||
shutdown_thread_.join();
|
||||
}
|
||||
long_lane_.abort(); // beginShutdown() already did; harmless again, and joinAll() must never wait out a scan
|
||||
async_tasks_.joinAll();
|
||||
// Join the RPC worker thread (was signaled in beginShutdown via requestStop)
|
||||
if (worker_) {
|
||||
|
||||
Reference in New Issue
Block a user