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/src/meshbay_node/transfers.py | |
| 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/src/meshbay_node/transfers.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transfers.py | 29 |
1 files changed, 25 insertions, 4 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transfers.py b/packages/meshbay-node/src/meshbay_node/transfers.py index 533e985..dd5da5c 100644 --- a/packages/meshbay-node/src/meshbay_node/transfers.py +++ b/packages/meshbay-node/src/meshbay_node/transfers.py @@ -64,6 +64,10 @@ IDLE_TIMEOUT_SECS = 120.0 # Per account, per kind. Unbounded queues are how a node runs out of memory # politely; past this the client keeps the rest in its own list. MAX_QUEUED_PER_MEMBER = 32 +# How many times a lease may be granted and not taken up before it is closed +# rather than queued again. Without a bound the requeue is a permanent cycle, +# and a node logs the same reclaim every 30 s until it restarts. +MAX_MISSED_GRANTS = 3 # Why a lease ended, as it reaches the peer. REASON_DONE = "done" @@ -73,6 +77,7 @@ REASON_FAILED = "failed" REASON_SESSION_GONE = "session_gone" REASON_IDLE = "idle" REASON_NOT_TAKEN_UP = "not_taken_up" +REASON_ABANDONED = "abandoned" @dataclass @@ -92,6 +97,12 @@ class Lease: # first is a grant to revoke and pass on, the second a transfer to reclaim. used: bool = False last_seen: float = 0.0 + # How many grants this lease has been given and not taken up. Bounded + # because the requeue is otherwise a permanent cycle: revoked, put back, + # granted again a millisecond later because there is room, revoked 30 s + # later, for ever. Seen doing exactly that in a node's log, every 30 s, + # minutes after the transfers involved had finished. + missed_grants: int = 0 @property def member(self) -> tuple[str, str]: @@ -211,6 +222,7 @@ class TransferSlots: if lease is None or lease.state != "granted": return False lease.used = True + lease.missed_grants = 0 lease.last_seen = time.monotonic() if now is None else now return True @@ -267,11 +279,20 @@ class TransferSlots: continue if not lease.used and lease.granted_at is not None \ and now - lease.granted_at > GRANT_DEADLINE_SECS: - lease.state = "queued" + lease.missed_grants += 1 lease.granted_at = None - self.queues[lease.kind].append(lease.tr) - ended.append((lease, REASON_NOT_TAKEN_UP)) - requeued = True + if lease.missed_grants >= MAX_MISSED_GRANTS: + # It has had its chances. Closing it is what ends the cycle, + # and the peer is told so a client that is somehow still + # there can ask again from a clean state rather than hold a + # slot it has never once used. + self.leases.pop(lease.tr, None) + ended.append((lease, REASON_ABANDONED)) + else: + lease.state = "queued" + self.queues[lease.kind].append(lease.tr) + ended.append((lease, REASON_NOT_TAKEN_UP)) + requeued = True elif lease.used and now - lease.last_seen > IDLE_TIMEOUT_SECS: self.leases.pop(lease.tr, None) ended.append((lease, REASON_IDLE)) |