diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-08 14:20:57 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-08 14:20:57 +0200 |
| commit | bdeffa448cdde9680fbf7bdda036746b101ddf75 (patch) | |
| tree | 04d90f4d5bf7de00e508a23519add4a6c951d656 /poc/spike4_nat.py | |
| parent | 038066c43caa8ee76dd1e04761234271e4e67ecd (diff) | |
| download | meshbay-bdeffa448cdde9680fbf7bdda036746b101ddf75.tar.gz | |
feat(node): transfer leases, pools and a queue for downloads and uploads
Step 2 of ~/next/improve-downloads.md. A download is invisible to the node: it
is a series of independent `file_req` messages, with nothing saying one started
or ended, so there is nothing to count and nothing to cap. The lease is that
missing object.
`meshbay_node/transfers.py` holds the decisions and has no asyncio and no
transport in it, on purpose. The failure modes this has to survive — a slot the
node never gets back, a client waiting on a grant the node has forgotten — are
races through a DataChannel and unprovable there; here the clock is a parameter
and every method returns what changed, so the caller does the I/O and the tests
drive the worst case directly.
What it decides:
- two pools, downloads and uploads, separate from the stream pool: different
resources with different costs, and merging them makes both caps meaningless;
- per-member cap checked *before* the node-wide one, so a member at their own
limit queues behind their own transfers rather than holding a slot a second
member has none of. Per account across their devices, or the cap becomes a
function of how many tabs somebody opens;
- a queue that skips a member at their cap instead of waiting for them —
granting strictly in arrival order lets one member's limit stall everyone;
- `tr` drawn by the client and idempotent, which is what makes a reconnect safe;
- bounded per member, because unbounded queues are how a node runs out of
memory politely.
Every way a slot comes back, with the session teardown as the one that matters
(a closed tab, a quit browser and a dead network all arrive at
`shutdown_tasks`, and none of them needs a timer): explicit close, session
gone, a grant nobody took up in 30 s passed to the next in line, and a granted
transfer silent for 120 s reclaimed with its peer told, so a widget can offer a
resume rather than sit on a lie.
`GET /api/transfers` is the operator's window: when somebody reports a transfer
stuck at waiting, it is the only thing that says whether the node ever had them
in a queue — a log cannot, when the symptom is that nothing is happening. It
carries no filename and no path, which a test pins, because this is exactly
where one would be tempting.
Three things found while writing it, two of them mine:
- the randomised property test rejected `in_use <= cap` at once, and it was
right to: lowering a cap never interrupts a running transfer, so the count
legitimately sits above the new value. The invariant is that a *new* grant
never happens past the cap;
- the sweeper was started with `self._spawn`, which ties a task to one
session's set. It died with whichever peer opened the first transfer, and
every other peer's abandoned lease then stopped being reclaimed — a node that
fills up over days with nothing in the log. It belongs to the node now, with
its strong reference on the transport context;
- the pools are node-wide while `_peer_registry` is per group (finding H1), so
a slot freed in one group can grant one in another and the peer to notify is
not in the notifier's registry. Silently wrong in the first version.
Nothing enforces a lease yet: `file_req` is untouched, no client asks, and the
node grants everything. That is step 4's flag day, and this lands alone.
1148 node, 793 hub, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'poc/spike4_nat.py')
0 files changed, 0 insertions, 0 deletions