diff options
Diffstat (limited to 'packages/meshbay-node/tests/transfer_probe.py')
| -rwxr-xr-x | packages/meshbay-node/tests/transfer_probe.py | 114 |
1 files changed, 114 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/transfer_probe.py b/packages/meshbay-node/tests/transfer_probe.py index fb14533..bf18a5e 100755 --- a/packages/meshbay-node/tests/transfer_probe.py +++ b/packages/meshbay-node/tests/transfer_probe.py @@ -241,6 +241,113 @@ async def pull_together(client, entries, CHUNK) -> int: return 1 if failures else 0 +def _cli(*argv) -> str: + """Run `meshbay-node …` the way the operator does, and return its output.""" + import subprocess + out = subprocess.run(["meshbay-node", *argv], capture_output=True, + text=True, timeout=30) + if out.returncode != 0: + raise RuntimeError(f"meshbay-node {' '.join(argv)}: {out.stderr.strip()}") + return out.stdout + + +async def operator_checks(client, ack, group, node_id) -> int: + """The two things only the operator's side can answer. + + Both are about a promise made elsewhere: draft-v6 §2.11 says these settings + apply without a restart, and §5 of the transfer-slots plan says a lost peer + gives its slots back through a hook rather than a timeout. Neither can be + checked from inside the client, and both were wrong at some point today — + the live cap because the hot-swap wrote to an attribute that never existed, + and the operator's view because it reported the module defaults. + """ + import asyncio as _a + import json as _json + import re as _re + + failures = 0 + member_cap = int((ack.get("transfer_limits") or {}).get("download") or 0) + + # The queue has to be held by the *node* cap, not by this member's own. + # + # With both at 2, one account holding two transfers hits both at once, and + # raising the node-wide cap then correctly changes nothing — per-member is + # checked first, by design. An earlier version of this check set it up that + # way and reported the design working as a failure. So: node cap to 1, well + # under the member cap, and the third transfer is waiting on the machine. + was = _cli("transfers", "show") + prior = int(_re.search(r"download\s+\d+/(\d+)", was).group(1)) + _cli("transfers", "set", "1", "1") + + granted_tr, first = await _open_transfer(client) + tr_waiting, waiting = await _open_transfer(client) + ok = first.get("state") == "granted" and waiting.get("state") == "queued" + _line(ok, "with the node cap at 1, the second transfer waits on the machine " + f"rather than on this member's own cap of {member_cap}") + failures += not ok + + # ── 1. the operator can see it ──────────────────────────────────────── + shown = _cli("transfers", "show") + # The lease table only: "queued" also appears in each pool's summary line, + # and counting those reported three queues where there was one. + rows = [ln for ln in shown.splitlines() if " download " in ln + or " upload " in ln] + seen = sum(1 for ln in rows if " granted " in ln) + queued = sum(1 for ln in rows if " queued " in ln) + ok = seen == 1 and queued == 1 + _line(ok, f"`transfers show` lists {seen} granted and {queued} queued lease(s)") + failures += not ok + if not ok: + print(" the operator's only window into a stuck queue is wrong") + print(" " + shown.replace("\n", "\n ")) + + # ── 2. raising the cap live starts what was waiting ─────────────────── + _cli("transfers", "set", "4", "4") + try: + started = await _a.wait_for( + _wait_for_grant(client, tr_waiting), timeout=20) + except _a.TimeoutError: + started = False + _line(started, "raising the cap started the waiting transfer, with no " + "restart and no reconnection") + failures += not started + if not started: + print(" draft-v6 §2.11 promises this applies live; the setting " + "was accepted and nothing moved") + + # ── 3. a vanished peer's slots are back before anyone asks ──────────── + await client.close() + await _a.sleep(1.0) + after = _cli("transfers", "show") + ok = "nothing transferring" in after + _line(ok, "every slot came back when the peer vanished, with no timeout") + failures += not ok + if not ok: + print(" " + after.replace("\n", "\n ")) + + # Put the operator's cap back where it was found — not at a default, at + # whatever this node was running before the probe touched it. + _cli("transfers", "set", str(prior), str(prior)) + print(f"\n (node cap restored to {prior})") + + print() + print("all operator checks passed" if not failures + else f"{failures} operator check(s) failed") + return 1 if failures else 0 + + +async def _wait_for_grant(client, tr: str) -> bool: + """The node pushes the grant; nothing here polls for it. + + A grant that is decided and never sent is the "stuck at waiting" report the + whole design exists to prevent, and it looks perfectly correct in the pool. + """ + while True: + msg = await client.recv_type("transfer_state", timeout=30) + if msg.get("tr") == tr and msg.get("state") == "granted": + return True + + async def probe(args) -> int: cfg = env() hub = cfg["HUB_URL"] @@ -285,6 +392,9 @@ async def probe(args) -> int: cap = int(limits.get("download") or 0) print(f"node reports this member may run {cap} download(s) at once\n") + if args.operator: + return await operator_checks(alice, ack, group, node_id) + if args.pull: return await pull(alice, ack, args.pull, args.parallel) @@ -406,6 +516,10 @@ def main() -> int: ap.add_argument("--pull", type=int, default=0, metavar="N", help="actually download N files to completion, under a " "lease, and report where they stop") + ap.add_argument("--operator", action="store_true", + help="the two checks that need the operator's CLI: a cap " + "raised live starts what was waiting, and a vanished " + "peer's slots are back before anyone asks") ap.add_argument("--parallel", action="store_true", help="pull them at the same time on one connection, the " "way a browser does — which is when it goes wrong") |