From 8e0d97f7957e570c2e26d79e66efe0cc064cee4b Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Tue, 1 Sep 2026 14:41:09 +0000 Subject: 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 or winsock2.h changes nothing at all. --- src/rpc.cpp | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) (limited to 'src/rpc.cpp') 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 struct Miner { btf_socket_t fd; bool authorised = false; + bool subscribed = false; // completed mining.subscribe (#19 census) std::string address; std::string worker; std::string readBuf; @@ -2582,6 +2583,20 @@ static bool SendLine(btf_socket_t fd, const json& j) static const size_t MAX_STRATUM_LINE = 64 * 1024; static const size_t MAX_POOL_MINERS = 1000; +// #19 instrumentation: the "broadcast to N miner(s)" line counts connected +// sockets, which says nothing about how many are actually mining. These split +// the connection census into connected / subscribed / authorized / recently +// active, so an operator can tell a real workforce from a pile of half-open +// sessions at a glance. +static const int64 POOL_CENSUS_SECS = 60; // how often to log the breakdown +static const int64 POOL_ACTIVE_WINDOW = 300; // a miner is "active" if it + // authorized and was seen within + // this window (one payout-rotate + // cycle) +static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not + // authorized after this long is + // idle, not merely starting up + // Non-blocking recv: pull whatever bytes are available now into buf, then // return one complete line if one is ready. Three outcomes: // true -- line is filled; buf may still contain more lines @@ -2689,6 +2704,65 @@ static void BroadcastJob(bool clean) gCurrentJob.jobId.c_str(), sent); } +// --------------------------------------------------------------------------- +// #19: connection census. "broadcast to N miner(s)" only counts authorized +// sockets that got a job; it says nothing about the rest of the connection +// table. This logs the full breakdown so an operator can tell a real workforce +// apart from half-open / never-authorized / idle sessions, and flags when the +// single select() loop is running out of room (issue #26). +// +// "Out of room" is not the same question on both platforms, and FD_SETSIZE +// answers a different one on each. On POSIX it bounds the *value* of any +// descriptor handed to FD_SET, so what matters is the highest fd we hold. On +// Windows an fd_set is a counted array of SOCKET handles whose numeric values +// are arbitrary, so what matters is how many we hold, not how large they are. +// FD_SETSIZE comes from or winsock2.h; this file must not define +// it, because redefining it after those headers changes nothing. +// --------------------------------------------------------------------------- +static void LogPoolConnectionCensus() +{ + int connected = 0, subscribed = 0, authorized = 0, active = 0, neverAuthIdle = 0; + int64 now = GetTime(); +#ifndef _WIN32 + int maxfd = 0; +#endif + for (Miner* m : gMiners) { + connected++; +#ifndef _WIN32 + if ((int)m->fd > maxfd) maxfd = (int)m->fd; +#endif + if (m->subscribed) subscribed++; + if (m->authorised) { + authorized++; + if (m->lastSeen > 0 && now - m->lastSeen <= POOL_ACTIVE_WINDOW) + active++; + } else if (now - m->connectTime >= POOL_UNAUTH_GRACE_SECS) { + neverAuthIdle++; // past the grace period and still not authorized + } + } + +#ifndef _WIN32 + int headroom = (int)FD_SETSIZE - maxfd; + LogPrint("pool", "[pool] census: connected=%d subscribed=%d authorized=%d" + " active(last %llds)=%d never-auth-idle=%d" + " (cap=%zu, maxfd=%d, FD_SETSIZE=%d)\n", + connected, subscribed, authorized, (long long)POOL_ACTIVE_WINDOW, + active, neverAuthIdle, MAX_POOL_MINERS, maxfd, (int)FD_SETSIZE); +#else + int headroom = (int)FD_SETSIZE - connected; + LogPrint("pool", "[pool] census: connected=%d subscribed=%d authorized=%d" + " active(last %llds)=%d never-auth-idle=%d" + " (cap=%zu, FD_SETSIZE=%d sockets)\n", + connected, subscribed, authorized, (long long)POOL_ACTIVE_WINDOW, + active, neverAuthIdle, MAX_POOL_MINERS, (int)FD_SETSIZE); +#endif + if (headroom <= 16) + LogPrint("pool", "[pool] WARNING: %d slots left before FD_SETSIZE=%d --" + " the select() loop is about to walk into undefined behaviour" + " (#26). Refuse new miners or rebuild with poll/epoll.\n", + headroom, (int)FD_SETSIZE); +} + // --------------------------------------------------------------------------- // Stratum message handler -- called for each complete line from a miner // Returns false if the miner should be disconnected immediately. @@ -2757,6 +2831,7 @@ static bool HandleLine(Miner* m, const std::string& rawLine, // ------------------------------------------------------------------ if (method == "mining.subscribe") { std::string sid = ToHex(&m->fd, 4); + m->subscribed = true; LogPrint("worker", "[worker->pool] subscribe from fd=%d sid=%s\n", (int)m->fd, sid.c_str()); reply(json::array({json::array({json::array({"mining.notify",sid})}),sid,4})); @@ -4593,6 +4668,7 @@ static void PoolEventLoop() int lastHeight = -1; int64 lastRefresh = GetTime(); int64 lastStatsRefresh = 0; + int64 lastCensus = 0; // #19 connection census throttle int blocksFoundThisSession = 0; int64 noJobSince = 0; // 0 = have a job bool warnedNoJob = false; @@ -4810,6 +4886,14 @@ static void PoolEventLoop() RefreshStats(blocksFoundThisSession, roundShareTotal); lastStatsRefresh = GetTime(); } + + // 6. #19: log the connection census periodically. Cheap (one pass over + // gMiners) and it is the single number an operator reads to judge pool + // health, so it should not be an overstatement. + if (GetTime() - lastCensus >= POOL_CENSUS_SECS) { + LogPoolConnectionCensus(); + lastCensus = GetTime(); + } } for (Miner* m : gMiners) { sock_close(m->fd); delete m; } -- cgit v1.2.3