4.5 KiB
4.5 KiB
T7.3 — Leases and fencing
| Field | Value |
|---|---|
| Phase | P7 — Distribution |
| Size | L — over 3 days |
| Status | Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | — |
Goal
TTL leases with heartbeat renewal and monotonic fence tokens, so a partitioned worker cannot write under an expired claim.
Facts (inlined — no spec read needed)
- Runs are claimed by workers under a lease with a TTL. A worker that dies has its runs reclaimed after expiry.
- Lease renewal is a heartbeat on the run record.
Suspendedruns release their lease entirely rather than heartbeating through a human's lunch break. A run awaiting human approval or a webhook must release its worker; holding an executor slot across a human decision does not scale past a handful of concurrent runs.- Fencing tokens on every lease. A partitioned worker that resumes must not write under an expired claim, and a monotonic fence in the run record makes that a rejected write rather than a silent double-execution.
- Coordination backend: Postgres advisory locks, or Redis if lease churn dominates. Redis is deferred until that churn is real.
Steps
- Add to the run record:
lease_owner,lease_expires_at,fence: u64. - Claim: atomic conditional update — claim only if unowned or expired.
Increment
fenceon every successful claim. The fence is monotonic per run. - The worker carries its fence value in memory and includes it on every write. Writes compare against the stored fence and are rejected when lower.
- Heartbeat renews
lease_expires_aton an interval well inside the TTL. A failed renewal cancels the localRunScope(T1.1) rather than continuing optimistically. - On transition to
Suspended, release the lease outright — clear the owner and stop heartbeating. On resume, re-claim, which takes a new fence. - Reclaim path: a run whose lease expired is claimable by any worker.
- Test the full sequence: partition, expiry, reclaim elsewhere, reconnect the original, assert its writes are rejected by fence.
Acceptance
- Partition a worker, let its lease expire, reclaim the run elsewhere, then reconnect the original — its writes are rejected by fence, not merely late.
Verify
Harness: two worker processes against one Postgres, plus turmoil or an
iptables-style partition for the network half. tokio::time::pause cannot be
used across processes — use a short real TTL instead.
Integration test — tests/it_leases_fencing.rs:
- Worker A claims run R (fence = 1) and begins executing.
- Partition A from the database. Let the lease expire.
- Worker B reclaims R; assert its fence is 2.
- Heal the partition. Worker A attempts a write under fence 1.
- Assert the write is rejected by the fence check — and assert the rejection reason is the fence, not a timestamp comparison and not a generic conflict.
- Assert R completed exactly once: check the external side-effect ledger for a single entry.
- Assert A's failed heartbeat cancelled its local
RunScope, so it stopped working rather than continuing optimistically. Suspended: transition a run toSuspended; assert the lease is released (owner cleared) and that no heartbeat is emitted while suspended. Resume and assert a new, higher fence.- Monotonicity: 100 claim/expire cycles; assert the fence never decreases or repeats.
Command: cargo test -p distribution leases -- --test-threads=1
False pass:
- Step 5 satisfied by a timestamp check ("your lease expired"). Under clock skew that check can pass for a stale worker; the fence comparison is what makes it clock-independent. Assert the reason.
- Step 6 omitted: both workers can execute, both can be "correct", and the double-execution is only visible in the external ledger.
- A per-worker fence counter rather than per-run monotonic — step 9 with two workers alternating catches it.
- Step 8 verified by reading a flag rather than asserting heartbeats stop.
Traps
- Checking only
lease_expires_atat write time. Clock skew makes "not expired yet" a lie, and the fence is what makes the check independent of clocks. - Heartbeating through
Suspended. It burns a worker slot for the duration of a human decision. - A per-worker fence rather than per-run monotonic. Two workers then produce incomparable tokens.
Background (not required to do this task): rust-agentic-sys.md §5.2, §7, §9.4 · rust-agentic-task.md