From f5def7625e6369d117cc8f02b7c0244b70d6f779 Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Wed, 2 Sep 2026 05:49:27 +0000 Subject: pool: reap miners that have said nothing for ten minutes A Miner left gMiners in exactly one place: RecvLine reporting a clean disconnect. Half-open TCP sessions send no FIN and raise no error, so they lived forever -- on the live pool authorize outpaced disconnect 2:1, over half the table never submitted a share, and the leak grew toward MAX_POOL_MINERS, past which the accept path refuses real miners. Two changes. First, an idle reap in the event loop's miner sweep: any miner whose last activity is older than POOL_IDLE_TIMEOUT_SECS (600s) is closed and erased through exactly the bookkeeping the clean-disconnect path uses -- log, sock_close, erase via std::find, delete -- collected into a side vector first so the sweep's iteration stays valid. Shares an idle miner already earned stay in roundShareCount, just as they do when a miner cleanly disconnects mid-round. To give the reap something honest to measure, lastSeen is now stamped on every complete line received (authorize and valid shares already stamped it; a miner that only subscribes, or sends unrecognised methods, previously looked dead from birth), with connectTime standing in for a connection that never completed a line. Ten minutes is far beyond the 15s vardiff share target and the 45s stall watchdog, so no miner that is actually hashing comes close. Second, SO_KEEPALIVE on sockets accepted by the loopback stratum listener, so a half-open session eventually errors out of recv and lands in the reap path on its own. This is a backstop only: the default cadence before the first probe is hours on most stacks, so the idle timeout fires long before keepalive does. Miners arriving via a relay ride a loopback socket pair whose real transport is the rendezvous socket owned by the tunnel pump threads, which keepalive cannot reach across -- for those the idle reap is also what finally tears the tunnel down, since closing the app end unblocks the pumps. Closes #6 --- src/rpc.cpp | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) (limited to 'src/rpc.cpp') diff --git a/src/rpc.cpp b/src/rpc.cpp index 23e25ef..df7ca05 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -2596,6 +2596,18 @@ static void LocalAcceptFn(void*) break; continue; } + // #6: let the OS probe a peer that has gone silent, so a half-open + // session eventually errors out of recv and lands in the event + // loop's existing reap path instead of sitting in gMiners forever. + // This is a backstop, not the mechanism: the default cadence before + // the first probe is hours on most stacks, so the idle timeout in + // PoolEventLoop fires long before keepalive does. Miners arriving + // via a relay ride a loopback socket pair whose remote transport is + // the rendezvous socket owned by the tunnel pump threads; keepalive + // cannot reach across that, which is part of why the idle timeout + // exists. + int keepalive = 1; + setsockopt(c, SOL_SOCKET, SO_KEEPALIVE, (char*)&keepalive, sizeof(keepalive)); LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n"); PushReady((btf_socket_t)c); } @@ -2639,6 +2651,16 @@ static const int64 POOL_ACTIVE_WINDOW = 300; // a miner is "active" if it static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not // authorized after this long is // idle, not merely starting up +// #6: a Miner used to leave gMiners only when RecvLine reported a clean +// disconnect. A half-open session (miner host died, NAT state expired, +// cable pulled) sends no FIN and reports no error, so it sat in the table +// forever -- authorize outpaced disconnect 2:1 on the live pool, and the +// table grew toward MAX_POOL_MINERS, past which real miners are refused. +// lastSeen is stamped on every complete line received; a miner that has +// said nothing for this long is gone in every way that matters. Ten +// minutes is far beyond the vardiff target (a share every 15s) and the +// stall watchdog (45s), so no miner that is actually hashing comes close. +static const int64 POOL_IDLE_TIMEOUT_SECS = 600; // Non-blocking recv: pull whatever bytes are available now into buf, then // return one complete line if one is ready. Three outcomes: @@ -4940,6 +4962,35 @@ static void PoolEventLoop() m->hashRate *= pow(0.5, (silent - grace) / expected); } + // 3.7. Idle reap (#6). Mirror the clean-disconnect path exactly -- + // log, close, erase from gMiners via std::find, delete -- just with + // a different reason. Shares the miner already earned stay in + // roundShareCount, exactly as they do when a miner cleanly + // disconnects mid-round. connectTime stands in for lastSeen on a + // connection that never completed a line. Erasing inside the loop + // above would invalidate the iteration, so collect then reap. + { + int64 now = GetTime(); + std::vector idle; + for (Miner* m : gMiners) { + int64 last = (m->lastSeen > 0) ? m->lastSeen : m->connectTime; + if (now - last > POOL_IDLE_TIMEOUT_SECS) + idle.push_back(m); + } + for (Miner* m : idle) { + LogPrint("worker", "[pool] dropping idle miner %s -- nothing" + " received in over %llds (session shares: %llu," + " round shares: %llu)\n", + m->worker.empty() ? "(never authorized)" : m->worker.c_str(), + (long long)POOL_IDLE_TIMEOUT_SECS, + (unsigned long long)m->sessionShares, + (unsigned long long)m->roundShares); + sock_close(m->fd); + gMiners.erase(std::find(gMiners.begin(), gMiners.end(), m)); + delete m; + } + } + // 4. Select over miner sockets if (!gMiners.empty()) { fd_set fds; FD_ZERO(&fds); @@ -4962,6 +5013,11 @@ static void PoolEventLoop() // terminate the whole node -- not just the pool. Every // line arrives from an unauthenticated socket. while (RecvLine(m->fd, m->readBuf, line, disc)) { + // Any complete line is proof of life (#6): authorize + // and valid shares already stamp lastSeen themselves, + // but a miner that only ever subscribes (or sends + // unrecognised methods) must count as alive too. + m->lastSeen = GetTime(); try { HandleLine(m, line, roundShareCount, roundShareTotal, blocksFoundThisSession); -- cgit v1.2.3