diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-24 00:10:41 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-24 00:10:41 +0200 |
| commit | 059eb0318daf27d98bd1c9532705ff406c0a10f8 (patch) | |
| tree | 662f03d0df9306d2b8f861ce394835d3748ae3f8 /packages/meshbay-hub/tests/test_transport_contracts.py | |
| parent | 2d657f40ebd697e4332c95d7a57bbb292ff46012 (diff) | |
| download | meshbay-059eb0318daf27d98bd1c9532705ff406c0a10f8.tar.gz | |
fix(hub): Search reaches each group with one offer, and a busy hub is not a dead node0.15
The other half of the 4G failure. The page negotiated every group twice: the
sweep opened a connection, read the index and closed it, then the warm-up
opened the same group again. The sweep, the warm-up and the tiles each had a
concurrency ceiling of their own, and together they went past what the hub
admits per account. Whatever the hub refused was then reported as "node
unreachable" and remembered as down, which put that group last next time.
- Every connection goes through ConnectionPool, which holds the page's one
ceiling (six at once, sized for twenty groups on a phone) and keeps what
the sweep opened for the tiles. A visit costs one offer per group. A
refresh costs none for a connection that answers a four-second ping, and
a connection that died while the phone slept is replaced, not waited on.
- A connection whose index is being read is held against eviction. With
more groups than the pool keeps, it was otherwise the least recently used
one.
- Negotiations still under way when the page closes close what they get,
and a sweep cut short that way remembers nobody as down.
- transport.js sends an offer again on 429, 502 or 503, honouring
Retry-After, with jittered waits of about twenty seconds at worst. Search
counts each retry as progress. A 404, 403 or 504 still fails at once, so
a dead node costs no time.
The fan-out tests assumed a ceiling of three and were re-measured: four dead
groups of twelve now hold nothing back, even on a first visit. The pool and
the retry run as shipped code, lifted as text, against a fake clock. Each
guard was checked by removing it and seeing its test fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests/test_transport_contracts.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_transport_contracts.py | 9 |
1 files changed, 7 insertions, 2 deletions
diff --git a/packages/meshbay-hub/tests/test_transport_contracts.py b/packages/meshbay-hub/tests/test_transport_contracts.py index 3c2b0b6..28d6a10 100644 --- a/packages/meshbay-hub/tests/test_transport_contracts.py +++ b/packages/meshbay-hub/tests/test_transport_contracts.py @@ -531,13 +531,18 @@ def test_search_tries_every_node_the_hub_offers(): def test_search_connects_in_one_place(): - """Two call sites with their own connect is how one of them kept `nodes[0]`.""" + """Two call sites with their own connect is how one of them kept `nodes[0]`, + and how the sweep and the warm-up each negotiated the same group: every + connection goes through the pool, and only the pool calls `connectToGroup`.""" code = _code_only(SEARCH_PAGE.read_text(encoding="utf-8")) assert code.count("transport.connect(") == 1 + # The definition, and the pool's one call. + assert code.count("connectToGroup(") == 2 pool = code[code.index("async _doConnect("):code.index("_evict() {")] index = code[code.index("async function fetchGroupIndex("): code.index("async function fetchAllIndexes(")] - assert "connectToGroup(" in pool and "connectToGroup(" in index + assert "connectToGroup(" in pool + assert "pool.connect(" in index and "connectToGroup(" not in index # node:start only links an unlinked node key to the hub account when it is |