dragonxd serves RPC with 8 workers and a 16-deep queue. While a witness rebuild holds cs_main, every request needing it waits, and a request the client gave up on keeps its slot. The GUI kept polling and re-probing until all 24 slots were its own abandoned requests (even stop was then refused), treated the busy node as lost (tearing down the wallet state), and retried connect at frame rate on some pages. Busy gate (src/rpc/rpc_busy_gate.h), enforced in RPCClient::performCall for the shared rpc_ / fast_rpc_: - the first reply timeout or queue-full refusal marks the node busy; from then on nothing needing cs_main is sent (calls fail at once with the new RpcFailureKind::Deferred). stop, help and getrescaninfo are lock-free in dragonxd and always go through - one canary getinfo with no client timeout waits on its own client and thread; its answer clears the gate and triggers an immediate refresh (queue-full: retried after 2..30 s; no answer: back to normal handling) - the timer dispatch skips Core/Tx/Addresses/Peers/chat/mining/opid and connect attempts while busy - the 3-strike teardown counts only failures that put the connection in doubt, never busy ones; no balance scans follow a busy chain-info call Reconnect storm: retries were armed with setTimer(Core, kCoreDefault - 1), i.e. "4 s elapsed", which against Overview's 2 s interval meant every frame. RefreshScheduler::deferFor() is an absolute hold, and ConnectRetryBackoff picks the delay by failure kind (not listening 1/2/5 s, queue full 2..30 s, busy 30 s, else 4 s). Measured against the fake daemon: abandoned requests during a rebuild went from ~24 (every slot) to at most 2, with no teardown and recovery when the rebuild ends; a saturated queue sees about one canary per back-off step instead of ~190 requests/s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
157 lines
7.2 KiB
C++
157 lines
7.2 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#pragma once
|
|
|
|
// Classifying a failed RPC call from structured data (curl code, request-sent bit, HTTP status,
|
|
// JSON-RPC error code, non-JSON body) instead of substrings of what(). Header-only and pure, so the
|
|
// test target can use it without compiling rpc_client.cpp.
|
|
|
|
#include <curl/curl.h>
|
|
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
namespace dragonx {
|
|
namespace rpc {
|
|
|
|
enum class RpcFailureKind {
|
|
None, // the call succeeded
|
|
NotListening, // connection refused: nothing bound to the RPC port (not started, or already unbound)
|
|
ConnectTimeout, // TCP connect didn't complete in time (Windows after the daemon unbinds; filtered port)
|
|
ReplyTimeout, // request sent, no reply in time: the daemon holds it (busy, e.g. blocked on cs_main)
|
|
QueueFull, // HTTP 500 "Work queue depth exceeded": rejected before any handler ran; holds no slot
|
|
ShuttingDown, // HTTP 503: the daemon's HTTP server was interrupted for shutdown
|
|
Unhandled, // HTTP 500 "Unhandled request": queued, then dropped by a shutting-down daemon
|
|
Auth, // HTTP 401: wrong rpcuser/rpcpassword (or a stale .cookie)
|
|
Warmup, // JSON-RPC -28 (RPC_IN_WARMUP): the node is still loading
|
|
AppError, // the daemon ran the method and answered with a JSON-RPC error
|
|
Aborted, // our own requestAbort() cut the transfer
|
|
Deferred, // not sent: the node is busy (RpcBusyGate), so we held back rather than queue more
|
|
Other,
|
|
};
|
|
|
|
inline const char* rpcFailureKindName(RpcFailureKind kind)
|
|
{
|
|
switch (kind) {
|
|
case RpcFailureKind::None: return "None";
|
|
case RpcFailureKind::NotListening: return "NotListening";
|
|
case RpcFailureKind::ConnectTimeout: return "ConnectTimeout";
|
|
case RpcFailureKind::ReplyTimeout: return "ReplyTimeout";
|
|
case RpcFailureKind::QueueFull: return "QueueFull";
|
|
case RpcFailureKind::ShuttingDown: return "ShuttingDown";
|
|
case RpcFailureKind::Unhandled: return "Unhandled";
|
|
case RpcFailureKind::Auth: return "Auth";
|
|
case RpcFailureKind::Warmup: return "Warmup";
|
|
case RpcFailureKind::AppError: return "AppError";
|
|
case RpcFailureKind::Aborted: return "Aborted";
|
|
case RpcFailureKind::Deferred: return "Deferred";
|
|
case RpcFailureKind::Other: return "Other";
|
|
}
|
|
return "Other";
|
|
}
|
|
|
|
// True for the two curl timeouts: what the old `what().find("Timeout")` checks meant.
|
|
inline bool isRpcTimeout(RpcFailureKind kind)
|
|
{
|
|
return kind == RpcFailureKind::ConnectTimeout || kind == RpcFailureKind::ReplyTimeout;
|
|
}
|
|
|
|
/**
|
|
* @param curlCode CURLcode of the transfer (CURLE_OK when a response arrived)
|
|
* @param requestSent the request reached the daemon (CURLINFO_REQUEST_SIZE > 0). libcurl reports
|
|
* connect- and reply-phase timeouts with the same code; this tells them apart.
|
|
* @param httpStatus HTTP status, 0 when there was no response
|
|
* @param rpcCode JSON-RPC error.code from a JSON error body, 0 when there was none
|
|
* @param plainBody the response body when it was NOT JSON (dragonxd's HTTP-layer replies), else ""
|
|
*/
|
|
inline RpcFailureKind classifyRpcFailure(int curlCode, bool requestSent, long httpStatus, int rpcCode,
|
|
const std::string& plainBody)
|
|
{
|
|
if (curlCode != CURLE_OK) {
|
|
switch (curlCode) {
|
|
case CURLE_ABORTED_BY_CALLBACK: return RpcFailureKind::Aborted;
|
|
case CURLE_COULDNT_CONNECT: return RpcFailureKind::NotListening;
|
|
case CURLE_OPERATION_TIMEDOUT:
|
|
return requestSent ? RpcFailureKind::ReplyTimeout : RpcFailureKind::ConnectTimeout;
|
|
default: return RpcFailureKind::Other;
|
|
}
|
|
}
|
|
if (rpcCode == -28) return RpcFailureKind::Warmup; // takes precedence over the HTTP status
|
|
if (rpcCode != 0) return RpcFailureKind::AppError; // 500, or 400/404 (invalid request / no method)
|
|
if (httpStatus == 200) return RpcFailureKind::None;
|
|
if (httpStatus == 401) return RpcFailureKind::Auth;
|
|
if (httpStatus == 503) return RpcFailureKind::ShuttingDown;
|
|
if (httpStatus == 500) {
|
|
if (plainBody.find("Work queue depth exceeded") != std::string::npos) return RpcFailureKind::QueueFull;
|
|
if (plainBody.find("Unhandled request") != std::string::npos) return RpcFailureKind::Unhandled;
|
|
}
|
|
return RpcFailureKind::Other;
|
|
}
|
|
|
|
/**
|
|
* "RPC error: HTTP <code>", plus the body when the daemon sent a plain-text one (e.g. ": Work queue
|
|
* depth exceeded"), trimmed and limited to 120 printable ASCII characters so an HTML page or binary
|
|
* junk can't flood a log line. Exactly "RPC error: HTTP <code>" when nothing printable remains.
|
|
*/
|
|
inline std::string formatHttpError(long httpStatus, const std::string& plainBody)
|
|
{
|
|
std::string text;
|
|
bool pendingSpace = false;
|
|
for (unsigned char c : plainBody) {
|
|
if (text.size() >= 120) break;
|
|
if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
|
|
pendingSpace = !text.empty();
|
|
} else if (c >= 0x21 && c <= 0x7e) {
|
|
if (pendingSpace && text.size() < 119) text.push_back(' ');
|
|
pendingSpace = false;
|
|
text.push_back(static_cast<char>(c));
|
|
}
|
|
}
|
|
std::string out = "RPC error: HTTP " + std::to_string(httpStatus);
|
|
if (!text.empty()) out += ": " + text;
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* The daemon ran the method and answered with a JSON-RPC error. what() is the daemon's message;
|
|
* `code` is its JSON-RPC error code, notably -28 (RPC_IN_WARMUP). Callers catch this type to mean
|
|
* "the daemon said no" (e.g. no mnemonic -> fall back), as opposed to a transport failure.
|
|
*/
|
|
class RpcError : public std::runtime_error {
|
|
public:
|
|
RpcError(int errorCode, const std::string& message, long status = 0)
|
|
: std::runtime_error(message), code(errorCode), httpStatus(status) {}
|
|
RpcFailureKind kind() const { return code == -28 ? RpcFailureKind::Warmup : RpcFailureKind::AppError; }
|
|
int code = 0;
|
|
long httpStatus = 0;
|
|
};
|
|
|
|
/**
|
|
* The call produced no JSON-RPC answer: curl failed (refused, timed out, aborted), or the daemon's
|
|
* HTTP layer turned the request away (401, 503, queue full, unhandled). what() keeps the historic
|
|
* text ("RPC request failed: <curl error>" / "RPC error: HTTP <code>[: body]").
|
|
*/
|
|
class RpcTransportError : public std::runtime_error {
|
|
public:
|
|
RpcTransportError(RpcFailureKind failureKind, const std::string& message, int curl = CURLE_OK,
|
|
long status = 0, bool sent = false)
|
|
: std::runtime_error(message), kind(failureKind), curlCode(curl), httpStatus(status), requestSent(sent) {}
|
|
RpcFailureKind kind = RpcFailureKind::Other;
|
|
int curlCode = CURLE_OK;
|
|
long httpStatus = 0;
|
|
bool requestSent = false;
|
|
};
|
|
|
|
// The kind of any exception an RPC call threw (Other for anything that isn't an RPC error).
|
|
inline RpcFailureKind rpcFailureKindOf(const std::exception& e)
|
|
{
|
|
if (const auto* t = dynamic_cast<const RpcTransportError*>(&e)) return t->kind;
|
|
if (const auto* r = dynamic_cast<const RpcError*>(&e)) return r->kind();
|
|
return RpcFailureKind::Other;
|
|
}
|
|
|
|
} // namespace rpc
|
|
} // namespace dragonx
|