upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/rpc.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-01 14:41:09 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-01 14:41:09 +0000
commit8e0d97f7957e570c2e26d79e66efe0cc064cee4b (patch)
tree9a1dee0f994171bb217de852d9d659408df384f4 /src/rpc.cpp
parenta1d867ecbcfc9e218408d0a11863a450243a01f9 (diff)
pool: log a connection census, not just the broadcast count
"broadcast to N miner(s)" counts the authorized sockets that were sent a job. It says nothing about the rest of the connection table, so a pool holding a pile of half-open sessions and one real miner reads the same as a pool with N miners. #19 was opened on exactly that confusion: ~55 connections served per job while ~2 outside miners actually submitted shares. Log the breakdown every 60s instead: connected, subscribed (completed mining.subscribe), authorized, active (authorized and seen within one payout-rotate cycle), and never-auth-idle (past the grace period without authorizing). The pool category is on by default, so this reaches a production log without /debug, and the burst-collapse window is 3s, so a 60s cadence is never the duplicate this tree has swallowed before. The census also reports how close the single select() loop is to running out of room, which is not the same question on both platforms. On POSIX FD_SETSIZE bounds the value of any descriptor handed to FD_SET, so the highest fd we hold is what matters; on Windows an fd_set is a counted array of SOCKET handles whose values are arbitrary, so the count is what matters. FD_SETSIZE comes from the platform headers either way -- this file must not define it, because redefining it after <sys/select.h> or winsock2.h changes nothing at all.
Diffstat (limited to 'src/rpc.cpp')
-rw-r--r--src/rpc.cpp84
1 files changed, 84 insertions, 0 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 7a6b6a4..4ff09b9 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -2474,6 +2474,7 @@ static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by thi
2474struct Miner { 2474struct Miner {
2475 btf_socket_t fd; 2475 btf_socket_t fd;
2476 bool authorised = false; 2476 bool authorised = false;
2477 bool subscribed = false; // completed mining.subscribe (#19 census)
2477 std::string address; 2478 std::string address;
2478 std::string worker; 2479 std::string worker;
2479 std::string readBuf; 2480 std::string readBuf;
@@ -2582,6 +2583,20 @@ static bool SendLine(btf_socket_t fd, const json& j)
2582static const size_t MAX_STRATUM_LINE = 64 * 1024; 2583static const size_t MAX_STRATUM_LINE = 64 * 1024;
2583static const size_t MAX_POOL_MINERS = 1000; 2584static const size_t MAX_POOL_MINERS = 1000;
2584 2585
2586// #19 instrumentation: the "broadcast to N miner(s)" line counts connected
2587// sockets, which says nothing about how many are actually mining. These split
2588// the connection census into connected / subscribed / authorized / recently
2589// active, so an operator can tell a real workforce from a pile of half-open
2590// sessions at a glance.
2591static const int64 POOL_CENSUS_SECS = 60; // how often to log the breakdown
2592static const int64 POOL_ACTIVE_WINDOW = 300; // a miner is "active" if it
2593 // authorized and was seen within
2594 // this window (one payout-rotate
2595 // cycle)
2596static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not
2597 // authorized after this long is
2598 // idle, not merely starting up
2599
2585// Non-blocking recv: pull whatever bytes are available now into buf, then 2600// Non-blocking recv: pull whatever bytes are available now into buf, then
2586// return one complete line if one is ready. Three outcomes: 2601// return one complete line if one is ready. Three outcomes:
2587// true -- line is filled; buf may still contain more lines 2602// true -- line is filled; buf may still contain more lines
@@ -2690,6 +2705,65 @@ static void BroadcastJob(bool clean)
2690} 2705}
2691 2706
2692// --------------------------------------------------------------------------- 2707// ---------------------------------------------------------------------------
2708// #19: connection census. "broadcast to N miner(s)" only counts authorized
2709// sockets that got a job; it says nothing about the rest of the connection
2710// table. This logs the full breakdown so an operator can tell a real workforce
2711// apart from half-open / never-authorized / idle sessions, and flags when the
2712// single select() loop is running out of room (issue #26).
2713//
2714// "Out of room" is not the same question on both platforms, and FD_SETSIZE
2715// answers a different one on each. On POSIX it bounds the *value* of any
2716// descriptor handed to FD_SET, so what matters is the highest fd we hold. On
2717// Windows an fd_set is a counted array of SOCKET handles whose numeric values
2718// are arbitrary, so what matters is how many we hold, not how large they are.
2719// FD_SETSIZE comes from <sys/select.h> or winsock2.h; this file must not define
2720// it, because redefining it after those headers changes nothing.
2721// ---------------------------------------------------------------------------
2722static void LogPoolConnectionCensus()
2723{
2724 int connected = 0, subscribed = 0, authorized = 0, active = 0, neverAuthIdle = 0;
2725 int64 now = GetTime();
2726#ifndef _WIN32
2727 int maxfd = 0;
2728#endif
2729 for (Miner* m : gMiners) {
2730 connected++;
2731#ifndef _WIN32
2732 if ((int)m->fd > maxfd) maxfd = (int)m->fd;
2733#endif
2734 if (m->subscribed) subscribed++;
2735 if (m->authorised) {
2736 authorized++;
2737 if (m->lastSeen > 0 && now - m->lastSeen <= POOL_ACTIVE_WINDOW)
2738 active++;
2739 } else if (now - m->connectTime >= POOL_UNAUTH_GRACE_SECS) {
2740 neverAuthIdle++; // past the grace period and still not authorized
2741 }
2742 }
2743
2744#ifndef _WIN32
2745 int headroom = (int)FD_SETSIZE - maxfd;
2746 LogPrint("pool", "[pool] census: connected=%d subscribed=%d authorized=%d"
2747 " active(last %llds)=%d never-auth-idle=%d"
2748 " (cap=%zu, maxfd=%d, FD_SETSIZE=%d)\n",
2749 connected, subscribed, authorized, (long long)POOL_ACTIVE_WINDOW,
2750 active, neverAuthIdle, MAX_POOL_MINERS, maxfd, (int)FD_SETSIZE);
2751#else
2752 int headroom = (int)FD_SETSIZE - connected;
2753 LogPrint("pool", "[pool] census: connected=%d subscribed=%d authorized=%d"
2754 " active(last %llds)=%d never-auth-idle=%d"
2755 " (cap=%zu, FD_SETSIZE=%d sockets)\n",
2756 connected, subscribed, authorized, (long long)POOL_ACTIVE_WINDOW,
2757 active, neverAuthIdle, MAX_POOL_MINERS, (int)FD_SETSIZE);
2758#endif
2759 if (headroom <= 16)
2760 LogPrint("pool", "[pool] WARNING: %d slots left before FD_SETSIZE=%d --"
2761 " the select() loop is about to walk into undefined behaviour"
2762 " (#26). Refuse new miners or rebuild with poll/epoll.\n",
2763 headroom, (int)FD_SETSIZE);
2764}
2765
2766// ---------------------------------------------------------------------------
2693// Stratum message handler -- called for each complete line from a miner 2767// Stratum message handler -- called for each complete line from a miner
2694// Returns false if the miner should be disconnected immediately. 2768// Returns false if the miner should be disconnected immediately.
2695// --------------------------------------------------------------------------- 2769// ---------------------------------------------------------------------------
@@ -2757,6 +2831,7 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
2757 // ------------------------------------------------------------------ 2831 // ------------------------------------------------------------------
2758 if (method == "mining.subscribe") { 2832 if (method == "mining.subscribe") {
2759 std::string sid = ToHex(&m->fd, 4); 2833 std::string sid = ToHex(&m->fd, 4);
2834 m->subscribed = true;
2760 LogPrint("worker", "[worker->pool] subscribe from fd=%d sid=%s\n", 2835 LogPrint("worker", "[worker->pool] subscribe from fd=%d sid=%s\n",
2761 (int)m->fd, sid.c_str()); 2836 (int)m->fd, sid.c_str());
2762 reply(json::array({json::array({json::array({"mining.notify",sid})}),sid,4})); 2837 reply(json::array({json::array({json::array({"mining.notify",sid})}),sid,4}));
@@ -4593,6 +4668,7 @@ static void PoolEventLoop()
4593 int lastHeight = -1; 4668 int lastHeight = -1;
4594 int64 lastRefresh = GetTime(); 4669 int64 lastRefresh = GetTime();
4595 int64 lastStatsRefresh = 0; 4670 int64 lastStatsRefresh = 0;
4671 int64 lastCensus = 0; // #19 connection census throttle
4596 int blocksFoundThisSession = 0; 4672 int blocksFoundThisSession = 0;
4597 int64 noJobSince = 0; // 0 = have a job 4673 int64 noJobSince = 0; // 0 = have a job
4598 bool warnedNoJob = false; 4674 bool warnedNoJob = false;
@@ -4810,6 +4886,14 @@ static void PoolEventLoop()
4810 RefreshStats(blocksFoundThisSession, roundShareTotal); 4886 RefreshStats(blocksFoundThisSession, roundShareTotal);
4811 lastStatsRefresh = GetTime(); 4887 lastStatsRefresh = GetTime();
4812 } 4888 }
4889
4890 // 6. #19: log the connection census periodically. Cheap (one pass over
4891 // gMiners) and it is the single number an operator reads to judge pool
4892 // health, so it should not be an overstatement.
4893 if (GetTime() - lastCensus >= POOL_CENSUS_SECS) {
4894 LogPoolConnectionCensus();
4895 lastCensus = GetTime();
4896 }
4813 } 4897 }
4814 4898
4815 for (Miner* m : gMiners) { sock_close(m->fd); delete m; } 4899 for (Miner* m : gMiners) { sock_close(m->fd); delete m; }