summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/transfers.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-08 22:54:16 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-08 22:54:16 +0200
commit6803447a8a5cc7a612d08bb858394fd7ae1b049c (patch)
tree565b2ceda2964f0e1395ea254df6014b116723ed /packages/meshbay-node/src/meshbay_node/transfers.py
parent051100ca32f72dd8f28489e1b6cb084f321b3b54 (diff)
downloadmeshbay-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.py29
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))