feat(rpc): stop flooding a busy node's RPC queue; fix the reconnect storm
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>
This commit is contained in:
+11
-2
@@ -360,6 +360,8 @@ json RPCClient::makePayload(const std::string& method, const json& params)
|
||||
|
||||
std::string RPCClient::performCall(const std::string& method, const json& params, long& httpCodeOut)
|
||||
{
|
||||
if (busy_gate_ && !busy_gate_->admit(method))
|
||||
throw RpcTransportError(RpcFailureKind::Deferred, "RPC deferred: the node is busy", CURLE_OK, 0, false);
|
||||
emitRpcTrace(method);
|
||||
|
||||
json payload = makePayload(method, params);
|
||||
@@ -380,13 +382,20 @@ std::string RPCClient::performCall(const std::string& method, const json& params
|
||||
long requestSize = 0;
|
||||
curl_easy_getinfo(impl_->curl, CURLINFO_REQUEST_SIZE, &requestSize);
|
||||
const bool sent = requestSize > 0;
|
||||
throw RpcTransportError(classifyRpcFailure(res, sent, 0, 0, std::string()),
|
||||
"RPC request failed: " + std::string(curl_easy_strerror(res)), res, 0, sent);
|
||||
const RpcFailureKind kind = classifyRpcFailure(res, sent, 0, 0, std::string());
|
||||
if (busy_gate_) busy_gate_->record(method, kind);
|
||||
throw RpcTransportError(kind, "RPC request failed: " + std::string(curl_easy_strerror(res)), res, 0, sent);
|
||||
}
|
||||
|
||||
// Check HTTP response code
|
||||
httpCodeOut = 0;
|
||||
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &httpCodeOut);
|
||||
if (busy_gate_) {
|
||||
// Any HTTP answer means a daemon worker ran our request, except the queue-full refusal.
|
||||
const bool queueFull = httpCodeOut == 500 &&
|
||||
response_data.find("Work queue depth exceeded") != std::string::npos;
|
||||
busy_gate_->record(method, queueFull ? RpcFailureKind::QueueFull : RpcFailureKind::None);
|
||||
}
|
||||
|
||||
return response_data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user