/** * Cross-host lease lock for the local-files store (DEC-065). * * The advisory lock in `locks.ts` (DEC-036) recovers stale locks via * `process.kill(pid, 0)` — which only works **same-machine**. A store shared * across Pods on a ReadWriteMany volume (Scenario B of DEC-056) has holders on * different nodes that cannot probe each other's PIDs, so the PID model is * unsafe there. * * This module replaces PID-liveness with a **time-based lease**: * * - The lock file carries `{ holder, nonce, acquiredAt, renewedAt, leaseMs, * fencingToken }`. * - A lease is *expired* when `now - renewedAt > leaseMs`. Any contender may * then steal it. Staleness is decided purely by wall-clock time, so it works * regardless of which host or Pod held it. * - The atomic gate is `openSync(path, "wx")` (`O_CREAT | O_EXCL`): only one * contender wins the create, even across hosts on a POSIX-compliant shared * filesystem. * - `nonce` is a random per-acquisition id; release only unlinks the file if * the on-disk nonce still matches ours (so a holder that overran its lease * and was stolen from does not delete the new holder's lease). * - `fencingToken` is incremented on every steal (best-effort monotonic) and * returned to the caller for observability / downstream fencing. * * Clock-skew caveat: `leaseMs` must comfortably exceed (a) the longest * critical section plus (b) the maximum clock skew between hosts. The store's * read-then-write sections are sub-second, so the default 30s lease leaves a * wide margin. This is documented in DEC-065. */ import { type WithLockOptions } from "./locks.js"; export interface LeaseRecord { readonly holder: string; readonly nonce: string; readonly acquiredAt: string; readonly renewedAt: string; readonly leaseMs: number; readonly fencingToken: number; } export interface WithLeaseOptions extends WithLockOptions { /** Lease duration in ms. A lease older than this is stealable. Default 30000. */ readonly leaseMs?: number; /** Holder label (host + pid by default). Informational. */ readonly holder?: string; } export interface LeaseHandle { readonly nonce: string; readonly fencingToken: number; } /** * Synchronous lease-locked critical section. Mirrors `withLockSync` but uses * the time-based lease above, so it is safe for cross-host (cross-Pod) shared * stores. Returns `fn()`'s result. Throws `LockTimeoutError` if the lease * cannot be acquired within `timeoutMs`. * * The handle (nonce + fencingToken) is exposed to `fn` so callers that need * to fence a downstream side-effect can read the token. */ export declare function withLeaseSync(lockPath: string, fn: (handle: LeaseHandle) => T, options?: WithLeaseOptions): T; /** * Single-shot, HELD lease acquire (no busy-wait). Returns a {@link LeaseHandle} * on success or `null` if a live (non-expired) lease is currently held by * someone else. Unlike {@link withLeaseSync} the lease is NOT auto-released — * the caller owns it until it calls {@link releaseLeaseHandle}. This is the * primitive a durable owner (e.g. the objective-loop executor) needs, where the * critical section outlives a single synchronous function call. * * Stealing an EXPIRED lease still works (same O_EXCL + fencing-token semantics * as `withLeaseSync`), so a crashed holder does not wedge the resource forever. */ export declare function tryAcquireLease(lockPath: string, options?: { readonly leaseMs?: number; readonly holder?: string; }): LeaseHandle | null; /** * Release a held lease acquired via {@link tryAcquireLease}. No-op if the * on-disk nonce no longer matches (i.e. our lease was already stolen after we * overran it) — so we never delete a newer holder's lease. */ export declare function releaseLeaseHandle(lockPath: string, handle: LeaseHandle): void; /** Async variant of {@link withLeaseSync}. */ export declare function withLease(lockPath: string, fn: (handle: LeaseHandle) => T | Promise, options?: WithLeaseOptions): Promise; //# sourceMappingURL=lease.d.ts.map