/** * The writer lease of one session: who may append, under which fence, until * when. * * ## The fence * * A fence is a number that only grows. Every new holding of a session mints a * fence above every earlier one, and every record carries the fence of the * lease its writer held as `gen`. An append presents its lease, and a lease * below the session's current fence is refused (`StaleSessionLeaseError`). So * a holder that stalled past its expiry — a long GC pause, a suspended * container — cannot write after somebody else took the session over, and it * finds out at its next append rather than never. * * ## On disk: the fence is a file name, `lease.json` is the view * * `/lease..json` is created by writing the body to a * private temporary file and `link`ing it into place. `link` fails with * `EEXIST` if the name exists, so of any number of simultaneous takers exactly * one wins each fence, and the name never exists without its whole body. This * is the arbitration `store/run/claim-disk.ts` measured and settled on; its * header records why a single mutable file with a lock, `wx` and `rename` * each fail to arbitrate. The current holding is the highest fence. * * `/lease.json` is then replaced (fsynced temporary, rename) with * the same body plus `v`, `kind` and `fence`, for a person or a tool to read. * It is a view: under a race an older holding's rename can land after a newer * one's, so nothing that decides anything reads it. {@link readSessionLease} * reads the fence names. * * Renewing keeps the fence and rewrites that fence's body. A renewal that * loses a race with a taker — the taker judged the lease expired a moment * before the renewal landed — finds the higher fence afterwards and reports * the lease lost, and the fence check refuses its appends either way. * * Releasing publishes a tombstone at the next fence (empty holder, expiry 0), * so the counter never rewinds and the releasing holder's own fence is stale * the instant the release lands. * * ## Who renews, and when a new fence is minted * * Both stores apply one rule ({@link LeaseClaimContext}): * * - **Renewal is by presentation.** A claim renews only when it presents the * holding it has (`renew`) and that holding is still the top fence under * the same holder. The holder string alone never renews: a second instance * or a restarted process that reuses a name gets `null` while the lease is * live, exactly as a different holder would. A renewal keeps its fence even * when it arrives after the expiry, provided nobody took the session in * between — the fence did not move, so no other writer can have appended, * and the holder's turn is still its own (spec §4.5 "running"). * - **A new fence is above everything seen.** It is one more than the higher * of the top lease fence and `above`, which the log passes as its head's * `gen`. So a lost or cleared `/` directory cannot rewind the * fence below records already written. */ /** A holding of a session's writer lease. */ export interface SessionLease { /** Opaque caller identity (a worker id, a pod name). Evidence, not authority; make it unique per process. */ readonly holder: string; /** The fencing token; every record appended under this lease carries it as `gen`. */ readonly fence: number; /** Epoch ms after which another holder may take the session. */ readonly expiresAt: number; } export interface ClaimSessionOptions { readonly holder: string; readonly ttlMs: number; /** Clock, for tests. Defaults to `Date.now()`. */ readonly now?: number; } /** An append or a release presented a lease that is no longer the session's current one. */ export declare class StaleSessionLeaseError extends Error { readonly presentedFence: number; readonly currentFence: number; readonly name = "StaleSessionLeaseError"; constructor(presentedFence: number, currentFence: number, holder?: string); } /** What a log adds to a claim: the holding it presents, and the fence floor its records set. */ export interface LeaseClaimContext { /** * The holding the claimant has. The claim renews it (same fence) when it is * still the top fence under `options.holder`; without it a live lease is * never renewed, whoever holds it. */ readonly renew?: SessionLease; /** A minted fence is above this (the log head's `gen`). Default 0. */ readonly above?: number; } /** Where a backend keeps its leases. */ export interface SessionLeaseStore { /** * Take or renew the lease. `null` when it is live and the claim does not * present it (see {@link LeaseClaimContext}). */ claim(options: ClaimSessionOptions, context?: LeaseClaimContext): Promise; /** Give the lease up early. A stale lease releases nothing. */ release(lease: SessionLease): Promise; /** The current holding, or `null` when the session was never claimed. */ current(): Promise; /** The current fence (0 when never claimed), from the cheapest source the backend has. */ fence(): Promise; } /** Whether `lease` is live at `now`: not expired, and not a release tombstone. */ export declare function isLeaseLive(lease: SessionLease | null, now: number): boolean; /** Leases in `/`, correct across processes. */ export declare class DiskSessionLeaseStore implements SessionLeaseStore { #private; readonly sessionDir: string; constructor(sessionDir: string); fence(): Promise; current(): Promise; claim(options: ClaimSessionOptions, context?: LeaseClaimContext): Promise; release(lease: SessionLease): Promise; } /** The current lease of a session directory, for a reader that holds none (a drain, a status view). */ export declare function readSessionLease(sessionDir: string): Promise; /** The lease of an in-memory session: the same rules, held in this process. */ export declare class InMemorySessionLeaseStore implements SessionLeaseStore { #private; fence(): Promise; current(): Promise; claim(options: ClaimSessionOptions, context?: LeaseClaimContext): Promise; release(lease: SessionLease): Promise; } //# sourceMappingURL=lease.d.ts.map