diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-08 22:54:16 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-08 22:54:16 +0200 |
| commit | 6803447a8a5cc7a612d08bb858394fd7ae1b049c (patch) | |
| tree | 565b2ceda2964f0e1395ea254df6014b116723ed /packages/meshbay-node/tests | |
| parent | 051100ca32f72dd8f28489e1b6cb084f321b3b54 (diff) | |
| download | meshbay-6803447a8a5cc7a612d08bb858394fd7ae1b049c.tar.gz | |
fix(node): a running transfer keeps its slot, and a dead grant lets go
Two defects in the lease machinery, both found in the node's own log, neither
reachable from any test on either side.
**`touch()` was never called.** The node ignored `tr` on `file_req` entirely, so
`used` stayed False for every download ever made and the sweeper revoked each
grant thirty seconds in — while the file was transferring at 20 MB/s. The pool
was correct and the handlers were correct; the call between them was missing,
which is why neither side's tests could see it.
**The requeue was a permanent cycle.** A revoked grant went back in the queue,
was granted again a millisecond later because there was room, and was revoked
again thirty seconds on. The node logged the same two reclaims every thirty
seconds for as long as it ran — minutes after the transfers involved had
finished. Three chances now, then the lease is closed and the peer told.
`test_the_counter_never_drifts` could not have caught it: nothing drifted, the
same lease simply never left.
A lease that starts being used forgets its earlier misses: a client that took
two grants to get going is slow, not abandoned.
Also `transfers show` reported the module defaults rather than the operator's
values until something had transferred, so `transfers set 2 2` answered
"applied now" and the next line said 0/8 — indistinguishable, from outside,
from the hot-swap that did nothing for months. The test asserted the defaults
and so agreed with the bug; found by typing the command.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'packages/meshbay-node/tests')
| -rw-r--r-- | packages/meshbay-node/tests/test_transfer_slots.py | 59 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_transfer_slots_wire.py | 48 |
2 files changed, 106 insertions, 1 deletions
diff --git a/packages/meshbay-node/tests/test_transfer_slots.py b/packages/meshbay-node/tests/test_transfer_slots.py index a0ef381..7056e93 100644 --- a/packages/meshbay-node/tests/test_transfer_slots.py +++ b/packages/meshbay-node/tests/test_transfer_slots.py @@ -24,7 +24,8 @@ import pytest from meshbay_node.transfers import ( DOWNLOAD, GRANT_DEADLINE_SECS, IDLE_TIMEOUT_SECS, KINDS, - MAX_QUEUED_PER_MEMBER, REASON_IDLE, REASON_NOT_TAKEN_UP, TransferSlots, + MAX_MISSED_GRANTS, MAX_QUEUED_PER_MEMBER, REASON_ABANDONED, REASON_IDLE, + REASON_NOT_TAKEN_UP, TransferSlots, UPLOAD, ) @@ -306,3 +307,59 @@ def test_the_counter_never_drifts(seed): assert s.leases == {} assert all(q == [] for q in s.queues.values()) assert all(s.in_use(k) == 0 for k in KINDS) + + +# ── the cycle the node's own log showed ───────────────────────────────────── + +def test_a_grant_is_not_requeued_for_ever(_=None): + """ + A revoked grant went back in the queue, was granted again a millisecond + later because there was room, and was revoked again 30 s on. The node + logged the same two reclaims every 30 s for as long as it ran — minutes + after the transfers involved had finished. + + Three chances, then it is closed and the peer told, which is what ends the + cycle. `test_the_counter_never_drifts` could not see this: nothing drifted, + the same lease simply never left. + """ + s = _slots(node=4, per_member=4) + _open(s, "ghost", now=0.0) + now = 0.0 + reasons = [] + for _ in range(6): + now += GRANT_DEADLINE_SECS + 1 + ended, _granted = s.sweep(now=now) + reasons += [r for _, r in ended] + assert reasons.count(REASON_NOT_TAKEN_UP) == MAX_MISSED_GRANTS - 1 + assert reasons.count(REASON_ABANDONED) == 1 + assert "ghost" not in s.leases, "the lease is still cycling" + assert s.queues[DOWNLOAD] == [] + + +def test_a_transfer_that_is_running_is_never_revoked(_=None): + """ + The other half, and the one that mattered: nothing marked a lease used, so + `used` stayed False for a whole download and the sweeper revoked a grant + every 30 s while the file transferred at 20 MB/s. + """ + s = _slots(node=2, per_member=2) + _open(s, "live", now=0.0) + now = 0.0 + for _ in range(10): + now += GRANT_DEADLINE_SECS - 5 + assert s.touch("live", now=now), "a granted lease refused a touch" + ended, _granted = s.sweep(now=now) + assert ended == [], f"a running transfer was revoked: {ended}" + assert s.leases["live"].state == "granted" + + +def test_using_a_lease_forgives_its_earlier_misses(_=None): + """A slow start is not an abandoned one: a client that took two grants to + get going must not be closed on its third.""" + s = _slots(node=2, per_member=2) + _open(s, "slow", now=0.0) + s.sweep(now=GRANT_DEADLINE_SECS + 1) + s.sweep(now=2 * GRANT_DEADLINE_SECS + 2) + assert s.leases["slow"].missed_grants == 2 + s.touch("slow", now=2 * GRANT_DEADLINE_SECS + 3) + assert s.leases["slow"].missed_grants == 0 diff --git a/packages/meshbay-node/tests/test_transfer_slots_wire.py b/packages/meshbay-node/tests/test_transfer_slots_wire.py index afa4582..db8c17a 100644 --- a/packages/meshbay-node/tests/test_transfer_slots_wire.py +++ b/packages/meshbay-node/tests/test_transfer_slots_wire.py @@ -236,6 +236,25 @@ async def test_asking_before_anything_has_transferred_is_not_an_error(): assert snapshot["pools"][DOWNLOAD]["in_use"] == 0 +@pytest.mark.asyncio +async def test_the_caps_shown_are_the_operators_before_anything_transfers(): + """ + `transfers set 2 2` answers "applied now"; `transfers show` said 0/8 — + because the no-pool branch reported the module defaults rather than what the + operator had just set. Found by running it against a real node. The previous + test asserted the defaults, so it agreed with the bug: an operator would + have read that as the hot-swap doing nothing all over again. + """ + from meshbay_node import ops + + class _T: + _ctx = {"max_concurrent_downloads": 2, "max_concurrent_uploads": 3} + + snapshot = await ops.list_transfers({"webrtc": _T()}) + assert snapshot["pools"][DOWNLOAD]["cap"] == 2 + assert snapshot["pools"][UPLOAD]["cap"] == 3 + + # ── the sweeper's lifetime ────────────────────────────────────────────────── async def test_the_sweeper_outlives_the_session_that_started_it(ctx): @@ -296,3 +315,32 @@ async def test_the_sweeper_stops_when_the_last_lease_goes(ctx): assert ctx.get("_transfer_sweeper") is None finally: ws.TRANSFER_SWEEP_SECS = original + + +async def test_a_chunk_request_keeps_its_lease_alive(ctx): + """ + The seam that cost an afternoon. `TransferSlots.touch` existed, was tested, + and **nothing ever called it**: the node ignored `tr` on `file_req` + entirely, so `used` stayed False for every download ever made and the + sweeper revoked each grant 30 s in, while the file was transferring. + + Neither side's tests could see it — the pool was correct, the handlers were + correct, and the call between them was missing. Only the node's own log + showed it, repeating the same reclaim every 30 s. + """ + peer = _join(ctx, "s1", "alice") + peer._do_transfer_open({"tr": "t1", "bytes": 1024}) + lease = peer._slots().leases["t1"] + assert lease.used is False + + # A chunk request for a file that does not exist still counts: what marks + # the lease is the peer asking, not the node succeeding. + peer._group_ctx()["index"] = None + try: + await peer._do_file_request({"file_id": "nope", "chunk_index": 0, + "tr": "t1"}) + except Exception: + pass + assert peer._slots().leases["t1"].used is True, ( + "a chunk request under this lease did not mark it alive; the node will " + "revoke the grant in 30 seconds") |