OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room commits / 3706610

lease: write-gate renewal at half-TTL — stop the every-tick intent-log append
renew() re-wrote the __lease__ record on EVERY sign-loop tick (~2s) against a 15s TTL — 7× more often than failover needs. Because the value carries ts:now it can never dedup, so each tick appended a fresh op to the append-only intent log forever: the ~25KB/s-at-rest leak we traced (unbounded, ×N hosted DIDs). Fix: only re-write once the lease is past HALF its TTL. That keeps `now - ts` under ~ttlMs/2 + one tick (≈9.5s ≪ 15s), so a standby never claims while the holder is alive (no split-brain), and failover still fires within ~1.5×TTL. renew() still returns true (we hold the lease) when it skips the write. Verified: fresh renews are no-ops, a stale renew writes once, holds() stays true, a standby cannot steal a live gated lease, and it DOES claim after a real TTL lapse. Full suite green (30/30, 133/133) incl. lease-handoff + failover. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3706610374831a97d212df64d0b7002ffac409df
by plankatron <fleeky@gmail.com> · 2026-07-17T23:18:07.000Z

Changed files (2) +9 -1

lib/lease.js +8 -0
……@@ -59,9 +59,17 @@ export class Lease {
5959 }
6060
6161 // Renew our hold (bumps ts). No-op (returns false) if we're not the current holder.
62 //
63 // WRITE-GATED: only actually re-write the lease once it is past HALF its TTL. The sign loop calls this
64 // every ~2s, but a fresh 15s lease does not need re-writing 7× before it could lapse. Renewing every tick
65 // appended a new `__lease__` op to the append-only intent log EVERY tick forever (ts:now defeats value
66 // dedup) — an unbounded at-rest storage leak (the ~25KB/s-at-rest we traced). Gating at ttlMs/2 keeps
67 // `now - ts` under ~ttlMs/2 + one tick (≈9.5s ≪ 15s), so no standby ever claims while we are alive, and
68 // failover stays within ~1.5×TTL. We still return true (we DO hold the lease) when we skip the write.
6269 async renew (now = Date.now()) {
6370 const cur = await this.current()
6471 if (!cur || cur.holder !== this.self) return false
72 if ((now - (cur.ts || 0)) < this.ttlMs / 2) return true // still fresh — hold stands, no write needed
6573 await this.sb.set(LEASE_KEY, { holder: this.self, epoch: cur.epoch, ts: now })
6674 return true
6775 }
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "atproto-room",
3 "version": "0.2.4",
3 "version": "0.2.5",
44 "description": "Attestation-gated, encrypted multi-writer room engine — StateBase (keet-model admission) + Lease + signer + RepoBase + LabelBase/LabelRoom + serve-membrane. Extracted from hyper-proto-peer; consumed by the atproto peer (repo devices) and the co-authored labeler.",
55 "type": "module",
66 "main": "index.js",