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:
@@ -0,0 +1,113 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
// Long wallet RPCs (C61). A rescan or a key import holds the daemon's cs_main for as long as the wallet scan
|
||||
// takes: minutes to hours on a big wallet. On the shared clients they used to hit the 30 s curl timeout, report
|
||||
// a failure that wasn't one and flip the app into "node busy" (a reply timeout is recorded by the busy gate).
|
||||
// They now run one at a time on a dedicated, ungated client with no client timeout, abortable from any thread.
|
||||
|
||||
#include "connection.h" // ConnectionConfig
|
||||
#include "rpc_client.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace dragonx {
|
||||
namespace rpc {
|
||||
|
||||
// The wallet RPCs that scan the chain inside the call: they run on the long-call lane, never on the shared
|
||||
// clients (rpc_ / the console's fast lane), whose 30 s timeout they outlast.
|
||||
inline bool isLongWalletMethod(const std::string& method)
|
||||
{
|
||||
return method == "rescan" || method == "importprivkey" || method == "importaddress" ||
|
||||
method == "importwallet" || method == "z_importkey" || method == "z_importviewingkey" ||
|
||||
method == "z_importwallet";
|
||||
}
|
||||
|
||||
// How a long wallet call that threw an RpcTransportError ended, which picks the wording shown to the user.
|
||||
enum class LongCallCutOff {
|
||||
None, // the request never reached the node: its error is the whole story
|
||||
Cancelled, // our own abort cut the transfer; the node may still be scanning
|
||||
Interrupted, // the request was sent but no answer came back (a dropped connection, or a proxy's 502/504
|
||||
// while the node still runs it): the key may already be in the wallet
|
||||
Refused, // the node's HTTP layer turned it away before the method ran (401, 503, queue full,
|
||||
// unhandled), so nothing was imported
|
||||
};
|
||||
|
||||
inline LongCallCutOff classifyLongCallCutOff(RpcFailureKind kind, bool requestSent)
|
||||
{
|
||||
switch (kind) {
|
||||
case RpcFailureKind::Aborted:
|
||||
return LongCallCutOff::Cancelled;
|
||||
case RpcFailureKind::Auth:
|
||||
case RpcFailureKind::ShuttingDown:
|
||||
case RpcFailureKind::QueueFull:
|
||||
case RpcFailureKind::Unhandled:
|
||||
return LongCallCutOff::Refused;
|
||||
default:
|
||||
// Any other HTTP status (a proxy's 502/504) or a lost reply proves nothing about the node's side.
|
||||
return requestSent ? LongCallCutOff::Interrupted : LongCallCutOff::None;
|
||||
}
|
||||
}
|
||||
|
||||
// One long wallet RPC at a time on its own ungated client, with no client timeout, abortable from any thread.
|
||||
class LongCallLane {
|
||||
public:
|
||||
// Main thread: claim the lane. false when a long call is already running. A new call starts un-aborted.
|
||||
bool tryBegin()
|
||||
{
|
||||
bool idle = false;
|
||||
if (!active_.compare_exchange_strong(idle, true)) return false;
|
||||
abort_ = false;
|
||||
return true;
|
||||
}
|
||||
// The call and its continuation are done: the lane is free again.
|
||||
void end() { active_ = false; }
|
||||
bool active() const { return active_.load(); }
|
||||
bool aborted() const { return abort_.load(); }
|
||||
|
||||
// Any thread. Sticky until the next tryBegin(): a call that has not opened its client yet is aborted as soon
|
||||
// as it does, and an in-flight transfer is cut through the client's progress callback (about a second).
|
||||
void abort()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mx_);
|
||||
abort_ = true;
|
||||
if (client_) client_->requestAbort();
|
||||
}
|
||||
|
||||
// Lane thread. open() FIRST (it clears the client's abort flag), THEN publish the client, THEN re-check the
|
||||
// sticky flag under the same mutex, so an abort that landed before or during open() is never erased.
|
||||
// An open() failure is not handled here: the body's first call then throws "Not connected".
|
||||
template <class Body>
|
||||
void run(RPCClient& c, const ConnectionConfig& cfg, Body&& body)
|
||||
{
|
||||
c.open(cfg.host, cfg.port, cfg.rpcuser, cfg.rpcpassword, cfg.use_tls);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mx_);
|
||||
client_ = &c;
|
||||
if (abort_) c.requestAbort();
|
||||
}
|
||||
struct Unpublish {
|
||||
LongCallLane& lane;
|
||||
~Unpublish()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(lane.mx_);
|
||||
lane.client_ = nullptr;
|
||||
}
|
||||
} unpublish{*this};
|
||||
body(c);
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mx_;
|
||||
RPCClient* client_ = nullptr; // guarded by mx_; set only while run() executes the body
|
||||
std::atomic<bool> abort_{false};
|
||||
std::atomic<bool> active_{false};
|
||||
};
|
||||
|
||||
} // namespace rpc
|
||||
} // namespace dragonx
|
||||
@@ -585,6 +585,28 @@ std::string RPCClient::callRaw(const std::string& method, const json& params)
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string RPCClient::callRaw(const std::string& method, const json& params, long timeoutSec)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||
if (!impl_->curl) {
|
||||
throw std::runtime_error("Not connected");
|
||||
}
|
||||
|
||||
// Temporarily override the timeout, as call(method, params, timeoutSec) does
|
||||
long prevTimeout = 30L;
|
||||
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec);
|
||||
|
||||
try {
|
||||
std::string out = callRaw(method, params);
|
||||
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
||||
return out;
|
||||
} catch (...) {
|
||||
// Ensure timeout is always restored
|
||||
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void RPCClient::doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err)
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -184,6 +184,12 @@ public:
|
||||
*/
|
||||
std::string callRaw(const std::string& method, const json& params = json::array());
|
||||
|
||||
/**
|
||||
* @brief callRaw() with a custom timeout, restored to the default afterwards (like the 3-arg call())
|
||||
* @param timeoutSec Timeout in seconds (0 = no timeout)
|
||||
*/
|
||||
std::string callRaw(const std::string& method, const json& params, long timeoutSec);
|
||||
|
||||
// High-level API methods - mirror the Qt version
|
||||
|
||||
// Info methods
|
||||
|
||||
Reference in New Issue
Block a user