upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-08-01 21:27:29 -0300
committerGitHub <noreply@github.com>2026-08-01 21:27:29 -0300
commitca0b6d0c4a3520a87037ba72bb1f1ee10eb504dc (patch)
treee0b6ff645e55f42fb754da7945ecfb73d10c10fc /src
parent09007b4a0321c594083fe1b5e67edbc8b257844b (diff)
diag: count the socket leak the way that is true, and say what it found (#100)
Windows lists a wildcard-bound socket twice in MSFT_NetTCPConnection, so counting Bound rows makes the number climb whenever the node talks to more peers -- 35 of a node's 38 Bound rows shared a local port with one of its own established connections. Counting orphans instead: a 1.2.10 node at 26h held 1262 and was still taking on 30-90 an hour; nodes on 1.2.11 and 1.2.12 held 3, transient. The leak was real and 1.2.11 ended it. Adds a live-unconn column (getpeername on every tagged socket, reports 1: the listener) and rewrites the notes that said otherwise.
Diffstat (limited to 'src')
-rw-r--r--src/net.cpp38
-rw-r--r--src/sockcount.h74
2 files changed, 83 insertions, 29 deletions
diff --git a/src/net.cpp b/src/net.cpp
index 4fe5f15..b56f9f5 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -64,30 +64,38 @@ static string SockAccountingText()
64 long long nOpened[SOCK_SITES], nClosed[SOCK_SITES], nFailed[SOCK_SITES], nUntagged = 0; 64 long long nOpened[SOCK_SITES], nClosed[SOCK_SITES], nFailed[SOCK_SITES], nUntagged = 0;
65 BtfSockSnapshot(nOpened, nClosed, nFailed, &nUntagged); 65 BtfSockSnapshot(nOpened, nClosed, nFailed, &nUntagged);
66 66
67 long long nUnconn[SOCK_SITES];
68 BtfSockLiveUnconnected(nUnconn);
69
67 string str = "\n sockets by where they were created\n"; 70 string str = "\n sockets by where they were created\n";
68 str += " site opened closed live close failed\n"; 71 str += " site opened closed live close failed live unconn\n";
69 long long nTotalOpened = 0, nTotalClosed = 0, nTotalFailed = 0; 72 long long nTotalOpened = 0, nTotalClosed = 0, nTotalFailed = 0, nTotalUnconn = 0;
70 for (int i = 0; i < SOCK_SITES; i++) 73 for (int i = 0; i < SOCK_SITES; i++)
71 { 74 {
72 nTotalOpened += nOpened[i]; 75 nTotalOpened += nOpened[i];
73 nTotalClosed += nClosed[i]; 76 nTotalClosed += nClosed[i];
74 nTotalFailed += nFailed[i]; 77 nTotalFailed += nFailed[i];
78 nTotalUnconn += nUnconn[i];
75 if (nOpened[i] == 0) 79 if (nOpened[i] == 0)
76 continue; 80 continue;
77 str += strprintf(" %-24s %8lld %8lld %8lld %13lld\n", pszSockSite[i], 81 str += strprintf(" %-24s %8lld %8lld %8lld %13lld %12lld\n", pszSockSite[i],
78 nOpened[i], nClosed[i], nOpened[i] - nClosed[i], nFailed[i]); 82 nOpened[i], nClosed[i], nOpened[i] - nClosed[i], nFailed[i], nUnconn[i]);
79 } 83 }
80 str += strprintf(" %-24s %8s %8lld %8s %13s\n", "closed but never tagged", 84 str += strprintf(" %-24s %8s %8lld %8s %13s %12s\n", "closed but never tagged",
81 "-", nUntagged, "-", "-"); 85 "-", nUntagged, "-", "-", "-");
82 str += strprintf(" %-24s %8lld %8lld %8lld %13lld\n", "TOTAL", 86 str += strprintf(" %-24s %8lld %8lld %8lld %13lld %12lld\n", "TOTAL",
83 nTotalOpened, nTotalClosed, nTotalOpened - nTotalClosed, nTotalFailed); 87 nTotalOpened, nTotalClosed, nTotalOpened - nTotalClosed,
84 88 nTotalFailed, nTotalUnconn);
85 // The live total above is what this program believes it is holding. Compare 89
86 // it against what the operating system attributes to the process: on 90 // The live total is what this program believes it is holding. Comparing it
87 // Windows, `Get-NetTCPConnection | ? OwningProcess -eq <pid>`. A gap there 91 // against `Get-NetTCPConnection | ? OwningProcess -eq <pid>` needs the note
88 // is the leak, and the codes below say what the failed closes hit -- 10038 92 // in sockcount.h first: that table lists a wildcard-bound socket twice, so
89 // (WSAENOTSOCK) means the handle was already gone and this close was a 93 // a raw row count always looks larger than this. Count orphans, not rows.
90 // second one, which is a different bug from a descriptor that stayed. 94 //
95 // "live unconn" is a getpeername on every tagged socket. Anything above 1
96 // -- the listening socket -- means this program is holding a socket it
97 // never connected, which is the shape the leak hunt was looking for and
98 // never found.
91 if (nTotalFailed > 0) 99 if (nTotalFailed > 0)
92 { 100 {
93 str += " close failures by code\n"; 101 str += " close failures by code\n";
diff --git a/src/sockcount.h b/src/sockcount.h
index a42abb9..ef99414 100644
--- a/src/sockcount.h
+++ b/src/sockcount.h
@@ -4,15 +4,35 @@
4// Socket accounting -- which part of the program is holding the sockets. 4// Socket accounting -- which part of the program is holding the sockets.
5// 5//
6// A node on a 32-core machine was found with 1502 sockets in Windows' "bound" 6// A node on a 32-core machine was found with 1502 sockets in Windows' "bound"
7// state: created, never connected, never closed, each holding a distinct 7// state, each apparently holding a distinct ephemeral port. Reading the code
8// ephemeral port. That is 9% of the machine's whole dynamic port range gone in 8// did not find it: every connect() failure path in this tree closes its socket.
9// nineteen hours, and when the range runs out nothing on that machine can open 9// So count instead -- tag each socket where it is created, untag it where it is
10// an outbound connection -- not just this program. 10// closed, report the difference per site.
11// 11//
12// Reading the code did not find it. Every connect() failure path in this tree 12// The counting is what showed the counting was wrong. Windows lists a socket
13// closes its socket. So count instead: tag each socket where it is created, 13// bound to the wildcard address TWICE in MSFT_NetTCPConnection: once for the
14// untag it where it is closed, and report the difference per site. Whichever 14// binding (0.0.0.0:port, state "Bound") and once for the connection itself
15// site's live count tracks the leak is the answer. 15// (state "Established"). Measured: 35 of a node's 38 "Bound" rows had the same
16// local port as one of its established connections. The bound count therefore
17// rises because the node is talking to more peers, not because it is leaking,
18// which is why a fix that plainly worked looked like it had done nothing.
19//
20// The honest metric is the ORPHAN: a "Bound" row whose port appears nowhere
21// else in that process's table.
22//
23// $b = $c | ? State -eq 'Bound'
24// $rest = ($c | ? State -ne 'Bound').LocalPort
25// ($b | ? { $rest -notcontains $_.LocalPort }).Count
26//
27// By that metric, measured the same evening on three machines: a node on 1.2.10
28// with 26h of uptime had 1262 orphans and was still taking on 30-90 an hour; a
29// node on 1.2.11 with 4h40m had 3; a node on 1.2.12 had 3, all from its first
30// half minute. At the 1.2.10 rate the 1.2.11 node would have had ~230.
31//
32// So the leak was real and 1.2.11 ended it -- most likely the 60-second reaper
33// for undeliverable nodes, which was written for a different problem. The
34// counters below stay: they are how the question got answered, and they are how
35// it would be answered again.
16// 36//
17// Header-only on purpose. btfrv.cpp is compiled a second time, on its own, into 37// Header-only on purpose. btfrv.cpp is compiled a second time, on its own, into
18// the standalone rendezvous relay -- a separate tree that does not link net.cpp. 38// the standalone rendezvous relay -- a separate tree that does not link net.cpp.
@@ -53,12 +73,11 @@ struct BtfSockAccount
53 long long nOpened[SOCK_SITES]; 73 long long nOpened[SOCK_SITES];
54 long long nClosed[SOCK_SITES]; 74 long long nClosed[SOCK_SITES];
55 long long nCloseFailed[SOCK_SITES]; 75 long long nCloseFailed[SOCK_SITES];
56 // Why the close failed, counted per code per site. Measured on a node with 76 // Why the close failed, counted per code per site. Kept because nothing in
57 // 3h51m of uptime: the program believed it held 58 sockets and Windows 77 // this codebase had ever looked at what closesocket returns, and a close
58 // attributed 112 to the process, 47 of them bound to an ephemeral port and 78 // that fails leaves the descriptor and its port in place. It is not the
59 // never connected -- against 55 closes that had returned an error. The 79 // leak -- see the note at the top of this file -- but it is the one number
60 // shapes match, and the error code is the piece that says whether the 80 // that would say so if it ever became the leak.
61 // descriptor is still there or was already gone.
62 std::map<int, long long> mapCloseErr[SOCK_SITES]; 81 std::map<int, long long> mapCloseErr[SOCK_SITES];
63 long long nClosedUntagged; 82 long long nClosedUntagged;
64 BtfSockAccount() : nClosedUntagged(0) 83 BtfSockAccount() : nClosedUntagged(0)
@@ -145,6 +164,33 @@ inline void BtfSockSnapshot(long long* pOpened, long long* pClosed, long long* p
145 *pUntagged = a.nClosedUntagged; 164 *pUntagged = a.nClosedUntagged;
146} 165}
147 166
167// Of the sockets still tagged live, how many have no peer on the other end.
168//
169// This is the check that ended the socket hunt, by answering it with a zero.
170// The suspicion was that some site created sockets, never connected them and
171// never let go. Run against a live node it reports 1 -- the listening socket,
172// which has no peer by definition. Every other tagged socket is connected.
173//
174// See the note at the top of this file for what the pile actually was.
175inline void BtfSockLiveUnconnected(long long* pUnconnected)
176{
177 for (int i = 0; i < SOCK_SITES; i++)
178 pUnconnected[i] = 0;
179 BtfSockAccount& a = BtfSockAccounting();
180 std::lock_guard<std::mutex> lock(a.mtx);
181 for (std::map<SOCKET, int>::const_iterator mi = a.mapSite.begin(); mi != a.mapSite.end(); ++mi)
182 {
183 struct sockaddr_storage ss;
184#ifdef _WIN32
185 int len = sizeof(ss);
186#else
187 socklen_t len = sizeof(ss);
188#endif
189 if (getpeername(mi->first, (struct sockaddr*)&ss, &len) != 0)
190 pUnconnected[mi->second]++;
191 }
192}
193
148// The close-failure codes for one site, newest counts included. Separate call 194// The close-failure codes for one site, newest counts included. Separate call
149// because the table above is fixed-width and this is a list of unknown length. 195// because the table above is fixed-width and this is a list of unknown length.
150inline void BtfSockCloseErrors(int nSite, std::map<int, long long>& mapOut) 196inline void BtfSockCloseErrors(int nSite, std::map<int, long long>& mapOut)