diff options
| author | Bitflash <btf01@proton.me> | 2026-08-02 11:59:12 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-02 11:59:12 -0300 |
| commit | 758ff4bc49af78b06e37fc1e99741cb5fa8e43d6 (patch) | |
| tree | 4e9d4ed63430d7a442260d1be78d63878a2fc455 /relay/btfrv.cpp | |
| parent | fe04a2045c3080797c6f93bf7cd2e12192b8f68f (diff) | |
rendezvous: let a node park more than one registration (#113)
A registration was consumed by the first caller and registering again replaced the previous one, so a node was reachable to one peer at a time and unreachable for the gap between being paired and registering again. Measured on a live node: descriptors resolved 53/54, our own registrations paired 11/12, nothing paired and went silent -- and half of every outbound dial failed, 15 of 30 in eleven minutes.
The relay keeps a queue per pubkey capped at four, pairs with the oldest still-alive registration and discards the dead; the node runs three accept threads, each holding its own registration. The wire protocol does not change, so either side can be deployed first.
Bench with both relays built from source and waves of concurrent dialers: old+1 65%, new+1 58%, old+3 68%, new+3 98%. Each half alone lands within noise of doing nothing.
Diffstat (limited to 'relay/btfrv.cpp')
| -rw-r--r-- | relay/btfrv.cpp | 97 |
1 files changed, 73 insertions, 24 deletions
diff --git a/relay/btfrv.cpp b/relay/btfrv.cpp index b1eee55..833c47c 100644 --- a/relay/btfrv.cpp +++ b/relay/btfrv.cpp | |||
| @@ -31,6 +31,7 @@ typedef int SOCKET; | |||
| 31 | #include <cstring> | 31 | #include <cstring> |
| 32 | #include <cstdio> | 32 | #include <cstdio> |
| 33 | #include <string> | 33 | #include <string> |
| 34 | #include <deque> | ||
| 34 | #include <map> | 35 | #include <map> |
| 35 | #include <thread> | 36 | #include <thread> |
| 36 | #include <mutex> | 37 | #include <mutex> |
| @@ -44,7 +45,24 @@ static std::atomic<bool> g_stop(false); | |||
| 44 | static SOCKET g_listen = INVALID_SOCKET; | 45 | static SOCKET g_listen = INVALID_SOCKET; |
| 45 | 46 | ||
| 46 | // Services that have registered and are waiting for a client, keyed by pubkey. | 47 | // Services that have registered and are waiting for a client, keyed by pubkey. |
| 47 | static std::map<std::string, SOCKET> g_waiting; | 48 | // More than one registration per node, on purpose. |
| 49 | // | ||
| 50 | // This used to be one socket per pubkey, and a dial *consumed* it: the first | ||
| 51 | // caller took the registration and everyone else was told "not found" until the | ||
| 52 | // node noticed and registered again. A node re-registers only after its pairing | ||
| 53 | // has been handed off, so between the two there is a window where it is | ||
| 54 | // unreachable to the whole network. Measured from a node's own counters: half | ||
| 55 | // of all outbound dials failed that way -- 15 of 30 in eleven minutes -- while | ||
| 56 | // descriptors resolved 53 times out of 54 and the node's own registrations | ||
| 57 | // paired 11 times out of 12. The rendezvous was not losing anybody; it was | ||
| 58 | // letting one caller in at a time. | ||
| 59 | // | ||
| 60 | // A queue per key lets a node keep spare registrations parked, so a dial that | ||
| 61 | // arrives while another is being paired finds the next one instead of a closed | ||
| 62 | // door. Old nodes register exactly as before and simply keep one entry: the | ||
| 63 | // wire protocol does not change, so a relay running this serves both. | ||
| 64 | static const size_t MAX_WAITING_PER_KEY = 4; | ||
| 65 | static std::map<std::string, std::deque<SOCKET> > g_waiting; | ||
| 48 | static std::mutex g_mtx; | 66 | static std::mutex g_mtx; |
| 49 | 67 | ||
| 50 | // How often the reaper walks the waiting map. | 68 | // How often the reaper walks the waiting map. |
| @@ -150,16 +168,24 @@ static void ReapDeadRegistrations() | |||
| 150 | int nBefore = 0, nReaped = 0; | 168 | int nBefore = 0, nReaped = 0; |
| 151 | { | 169 | { |
| 152 | std::lock_guard<std::mutex> lk(g_mtx); | 170 | std::lock_guard<std::mutex> lk(g_mtx); |
| 153 | nBefore = (int)g_waiting.size(); | 171 | std::map<std::string, std::deque<SOCKET> >::iterator it = g_waiting.begin(); |
| 154 | std::map<std::string, SOCKET>::iterator it = g_waiting.begin(); | ||
| 155 | while (it != g_waiting.end()) | 172 | while (it != g_waiting.end()) |
| 156 | { | 173 | { |
| 157 | if (RegistrationDead(it->second)) | 174 | std::deque<SOCKET>& q = it->second; |
| 175 | nBefore += (int)q.size(); | ||
| 176 | for (std::deque<SOCKET>::iterator jt = q.begin(); jt != q.end();) | ||
| 158 | { | 177 | { |
| 159 | CLOSESOCK(it->second); | 178 | if (RegistrationDead(*jt)) |
| 160 | g_waiting.erase(it++); | 179 | { |
| 161 | nReaped++; | 180 | CLOSESOCK(*jt); |
| 181 | jt = q.erase(jt); | ||
| 182 | nReaped++; | ||
| 183 | } | ||
| 184 | else | ||
| 185 | ++jt; | ||
| 162 | } | 186 | } |
| 187 | if (q.empty()) | ||
| 188 | g_waiting.erase(it++); | ||
| 163 | else | 189 | else |
| 164 | ++it; | 190 | ++it; |
| 165 | } | 191 | } |
| @@ -209,34 +235,57 @@ static void HandleIncoming(SOCKET s) | |||
| 209 | { | 235 | { |
| 210 | // Register and wait; a future client will pair us. | 236 | // Register and wait; a future client will pair us. |
| 211 | std::lock_guard<std::mutex> lk(g_mtx); | 237 | std::lock_guard<std::mutex> lk(g_mtx); |
| 212 | // If a stale service for this key exists, drop it. | 238 | std::deque<SOCKET>& q = g_waiting[pubkey]; |
| 213 | std::map<std::string, SOCKET>::iterator it = g_waiting.find(pubkey); | 239 | |
| 214 | if (it != g_waiting.end()) CLOSESOCK(it->second); | 240 | // Drop registrations this node has already lost the far end of, rather |
| 215 | g_waiting[pubkey] = s; | 241 | // than dropping the previous one on principle. The old code closed the |
| 216 | // leave the socket open in the map; do not close here. | 242 | // prior registration on every new one, which is why a node could never |
| 243 | // hold a spare. | ||
| 244 | for (std::deque<SOCKET>::iterator it = q.begin(); it != q.end();) | ||
| 245 | { | ||
| 246 | if (RegistrationDead(*it)) { CLOSESOCK(*it); it = q.erase(it); } | ||
| 247 | else ++it; | ||
| 248 | } | ||
| 249 | |||
| 250 | // A ceiling, because a stranger can open these. Oldest goes first: it | ||
| 251 | // is the one most likely to be stale. | ||
| 252 | while (q.size() >= MAX_WAITING_PER_KEY) | ||
| 253 | { | ||
| 254 | CLOSESOCK(q.front()); | ||
| 255 | q.pop_front(); | ||
| 256 | } | ||
| 257 | q.push_back(s); | ||
| 258 | // leave the socket open in the queue; do not close here. | ||
| 217 | } | 259 | } |
| 218 | else if (role == 'C') | 260 | else if (role == 'C') |
| 219 | { | 261 | { |
| 220 | SOCKET svc = INVALID_SOCKET; | 262 | SOCKET svc = INVALID_SOCKET; |
| 221 | { | 263 | { |
| 222 | std::lock_guard<std::mutex> lk(g_mtx); | 264 | std::lock_guard<std::mutex> lk(g_mtx); |
| 223 | std::map<std::string, SOCKET>::iterator it = g_waiting.find(pubkey); | 265 | std::map<std::string, std::deque<SOCKET> >::iterator it = g_waiting.find(pubkey); |
| 224 | if (it != g_waiting.end()) | 266 | if (it != g_waiting.end()) |
| 225 | { | 267 | { |
| 226 | // Check before handing it over. Pairing a client with a dead | 268 | std::deque<SOCKET>& q = it->second; |
| 227 | // registration produces a tunnel that fails after both sides | 269 | // Take the oldest one that is still alive, discarding the dead |
| 228 | // think they succeeded, which is the failure the dialling node | 270 | // as we go. Checking before handing it over matters: pairing a |
| 229 | // reports as "tunnel to X failed" with nothing to explain it. | 271 | // client with a dead registration produces a tunnel that fails |
| 230 | if (RegistrationDead(it->second)) | 272 | // after both sides think they succeeded, which is the failure |
| 273 | // the dialling node reports as "tunnel to X failed" with | ||
| 274 | // nothing to explain it. | ||
| 275 | while (!q.empty()) | ||
| 231 | { | 276 | { |
| 232 | CLOSESOCK(it->second); | 277 | SOCKET candidate = q.front(); |
| 233 | g_waiting.erase(it); | 278 | q.pop_front(); |
| 279 | if (RegistrationDead(candidate)) | ||
| 280 | { | ||
| 281 | CLOSESOCK(candidate); | ||
| 282 | continue; | ||
| 283 | } | ||
| 284 | svc = candidate; | ||
| 285 | break; | ||
| 234 | } | 286 | } |
| 235 | else | 287 | if (q.empty()) |
| 236 | { | ||
| 237 | svc = it->second; | ||
| 238 | g_waiting.erase(it); | 288 | g_waiting.erase(it); |
| 239 | } | ||
| 240 | } | 289 | } |
| 241 | } | 290 | } |
| 242 | if (svc == INVALID_SOCKET) | 291 | if (svc == INVALID_SOCKET) |