aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-08 14:20:57 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-08 14:20:57 +0200
commitbdeffa448cdde9680fbf7bdda036746b101ddf75 (patch)
tree04d90f4d5bf7de00e508a23519add4a6c951d656 /packages/meshbay-common
parent038066c43caa8ee76dd1e04761234271e4e67ecd (diff)
downloadmeshbay-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 'packages/meshbay-common')
-rw-r--r--packages/meshbay-common/src/meshbay_common/protocol.py14
1 files changed, 14 insertions, 0 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py
index e092353..4890d3b 100644
--- a/packages/meshbay-common/src/meshbay_common/protocol.py
+++ b/packages/meshbay-common/src/meshbay_common/protocol.py
@@ -96,6 +96,20 @@ class MNP:
# never serve the GEK in plaintext. Members obtain it by unwrapping their own
# ECIES bundle. The constants lingered after the handlers were deleted, leaving
# the wire contract looking as though the endpoint still existed.
+ # Transfer slots. A download is otherwise invisible to the node -- 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: `tr` is drawn by the client like `upload_id`, covers
+ # a job rather than a file, and dies with the connection.
+ #
+ # One reply type with a state field, not four: a client that must switch on
+ # the message type to discover it is still waiting is a client that will get
+ # one branch wrong. Carries no filename and no path -- `tr` is opaque,
+ # `bytes` and `chunks` are numbers -- so it stays in clear like
+ # INDEX_PROGRESS, for the same stated reason.
+ TRANSFER_OPEN = "transfer_open" # client -> node: I want a slot
+ TRANSFER_CLOSE = "transfer_close" # client -> node: I am done with it
+ TRANSFER_STATE = "transfer_state" # node -> client: granted/queued/closed
FILE_UPLOAD = "file_upload" # client pushes file chunk to node
FILE_UPLOAD_ACK = "file_upload_ack" # node acknowledges chunk receipt
DIR_CREATE = "dir_create" # client → node: make a directory