test(tools): match dragonxd's lock-free getrescaninfo; count requests per method

- getrescaninfo takes no lock in dragonxd (it only reads wallet fields),
  so the stub no longer holds it behind cs_main. Holding it there made
  the GUI's rescan-progress poll look like an abandoned request every
  30 s during a simulated rebuild.
- a `methods` event lists the 10 most-requested JSON-RPC methods with
  counts (alongside `stats`, and at exit): what the GUI is polling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 09:49:43 -05:00
co-authored by Claude Opus 5.5
parent 6b14098bb2
commit 98fa2154b4
2 changed files with 34 additions and 5 deletions
+2 -1
View File
@@ -14,7 +14,7 @@ What it copies from the real daemon (DragonX v1.3.0):
command line or `DRAGONX.conf`). When the queue is full the request gets an immediate plain-text
`HTTP 500 "Work queue depth exceeded"`. A request whose client timed out **keeps its slot** until
a worker reaches it.
- **cs_main.** One global lock. Every method except `stop` and `help` takes it. A "witness rebuild"
- **cs_main.** One global lock. Every method except `stop`, `help` and `getrescaninfo` (lock-free in dragonxd too) takes it. A "witness rebuild"
holds it for `FAKE_REBUILD_SECS` and logs the real markers:
- `Reading blocks for witness rebuild: <off> / <total>` (Phase 1, abortable);
- nothing during Phase 2, which ignores shutdown;
@@ -53,6 +53,7 @@ Everything that matters for a test is appended to `<datadir>/fake_dragonxd.event
| `sigterm` | SIGTERM/SIGINT received (POSIX) |
| `http-interrupted`, `shutdown-begin` | shutdown progress |
| `stats ...` | counters, every 30 s when they change |
| `methods ...` | the 10 most-requested JSON-RPC methods with counts (with `stats`, and at exit): what the GUI is polling |
| `clean-exit ...` | **graceful exit**, with final counters. If this is missing, the process was killed. |
## Build
+32 -4
View File
@@ -7,7 +7,7 @@
// exceeded" before any handler runs, and a request whose client gave up keeps its slot until a
// worker reaches it (libevent has no close callback);
// * a global cs_main that a "witness rebuild" holds for minutes, logging the real markers. Every
// method except the lock-free `stop` / `help` takes it;
// method except the lock-free `stop` / `help` / `getrescaninfo` takes it;
// * dragonxd's shutdown: `stop` (or SIGTERM/SIGINT on POSIX; Windows has no signal channel, as in
// v1.3.0) unbinds the listener at once, answers new requests on open connections with 503, then
// waits for the rebuild (Phase 2 is not abortable) before "Shutdown: In progress..." -> flush ->
@@ -330,6 +330,23 @@ struct Stats {
Stats g_stats;
// Requests per JSON-RPC method, for "what is the GUI polling" (the `methods` event).
std::mutex g_methodsMx;
std::map<std::string, long> g_methodCounts;
std::string topMethodsLine(size_t n)
{
std::vector<std::pair<long, std::string>> v;
{
std::lock_guard<std::mutex> lk(g_methodsMx);
for (const auto& kv : g_methodCounts) v.push_back({kv.second, kv.first});
}
std::sort(v.rbegin(), v.rend());
std::string out;
for (size_t i = 0; i < v.size() && i < n; ++i) out += (i ? " " : "") + v[i].second + "=" + std::to_string(v[i].first);
return out;
}
bool raiseMax(std::atomic<int>& max, int value)
{
int cur = max.load();
@@ -541,11 +558,16 @@ json execOne(const json& req, int& status)
if (!req.is_object() || !req.contains("method") || !req["method"].is_string())
return fail(-32600, "Missing method");
const std::string m = req["method"].get<std::string>();
{
std::lock_guard<std::mutex> lk(g_methodsMx);
++g_methodCounts[m];
}
json params = req.contains("params") ? req["params"] : json::array();
if (!params.is_array()) params = json::array();
status = 200;
const bool lockFree = (m == "stop" || m == "help");
// Like dragonxd: stop and help take no lock, nor does getrescaninfo (it only reads wallet fields).
const bool lockFree = (m == "stop" || m == "help" || m == "getrescaninfo");
if (!lockFree && !cannedReply(m, params).known) return fail(-32601, "Method not found");
if (g_warmup.load() && !allowedInWarmup(m)) return fail(-28, warmupStatus());
@@ -561,7 +583,9 @@ json execOne(const json& req, int& status)
}
Canned c;
{
if (lockFree) {
c = cannedReply(m, params);
} else {
std::lock_guard<std::mutex> lk(g_csMain);
c = cannedReply(m, params);
if (m == "setgenerate") g_minerOn = !params.empty() && params[0].is_boolean() && params[0].get<bool>();
@@ -1264,7 +1288,10 @@ int main(int argc, char** argv)
if (Clock::now() - lastStats >= 30s) {
lastStats = Clock::now();
const std::string s = statsLine();
if (s != lastStatsLine) event("stats", s);
if (s != lastStatsLine) {
event("stats", s);
event("methods", topMethodsLine(10));
}
lastStatsLine = s;
}
}
@@ -1304,6 +1331,7 @@ int main(int argc, char** argv)
fs::remove(cookieFile, ec2);
}
std::this_thread::sleep_for(200ms); // let connection threads flush their last replies
event("methods", topMethodsLine(10));
event("clean-exit", statsLine());
exitNow(0);
}