@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
155 lines • 7.57 kB
TypeScript
import type { LeaseCapableStorage } from '../storage/storage-provider.js';
/**
* The lock name the pruner asks for. Deliberately a single fleet-wide constant:
* every replica pruning the SAME store is exactly the set that should contend,
* and two apps sharing one Telescope store share the entries table too, so they
* should share the lock as well.
*/
export declare const PRUNE_LOCK_KEY = "telescope:prune";
/**
* A lease that was successfully acquired. Handed back inside a
* {@link TelescopePruneLockResult} so it is unreachable without first checking
* that the acquire actually succeeded.
*/
export interface TelescopePruneLease {
/** The lock name this lease covers. */
readonly key: string;
/** The holder identity that was granted the lease. */
readonly owner: string;
/**
* Epoch-ms after which the lease is considered abandoned and MAY be granted to
* somebody else, even if `release` was never called. This is what makes a pod
* that dies mid-prune cost one TTL of fleet-wide silence rather than forever.
*/
readonly expiresAtMs: number;
/**
* Gives the lease up early. MUST be idempotent, MUST NOT throw (swallow and,
* at most, log — the TTL is the backstop), and MUST NOT release the lease if
* this holder's lease already expired and was re-granted to somebody else.
*/
release(): Promise<void>;
}
/**
* The result of an acquire attempt, as a discriminated union so the `lease` is
* unreachable until `acquired` has been narrowed — a caller physically cannot
* forget to handle "somebody else has it".
*
* `reason` distinguishes the two failures because the pruner treats them
* OPPOSITELY:
* - `'held'` — a healthy, expected outcome. Another replica is pruning; this
* one stands down silently and tries again next tick.
* - `'unavailable'` — the lock MECHANISM failed (backend down, table missing,
* permissions). The pruner then FAILS OPEN and prunes anyway, because a
* broken lock must not become "retention silently stops and the table grows
* without bound". Failing open is at worst today's behaviour.
*
* Getting this distinction wrong in a host implementation is the one thing that
* really hurts, so: if you cannot tell the two apart, return `'unavailable'`.
*/
export type TelescopePruneLockResult = {
readonly acquired: true;
readonly lease: TelescopePruneLease;
} | {
readonly acquired: false;
readonly reason: 'held' | 'unavailable';
/** Optional human-readable detail, surfaced in the pruner's log line. */
readonly detail?: string;
};
/** The request handed to {@link TelescopePruneLock.acquire}. */
export interface TelescopePruneLockRequest {
/** Lock name. Always {@link PRUNE_LOCK_KEY} today; an object so it can grow. */
readonly key: string;
/**
* Identity of the process asking. Stable for the life of the process and
* distinct between replicas (the pruner uses `<instanceId>#<pid>`).
*/
readonly owner: string;
/**
* How long the lease should survive without a release. The pruner derives it
* from `prune.lockTtlMs`, defaulted to three prune intervals, so a crashed
* holder costs at most that much fleet-wide silence.
*/
readonly ttlMs: number;
}
/**
* The seam. Implement this in the host to back Telescope's prune lock with a
* primitive you already run — e.g. a durable-workflow singleton mutex, a Redis
* SET NX PX, a Postgres advisory lock.
*
* ## Contract (all of it)
*
* 1. `acquire` MUST NOT throw. Return `{ acquired: false, reason: 'unavailable' }`
* instead. (The pruner catches a throw and treats it as `'unavailable'`
* anyway, but a host that relies on that is relying on a backstop.)
* 2. `acquire` MUST be atomic across processes for a given `key`: at most one
* caller gets `acquired: true` while a lease is live.
* 3. A lease MUST expire on its own after roughly `ttlMs` even if `release` is
* never called. A holder that is SIGKILLed must not wedge the fleet.
* 4. Re-acquiring with the SAME `owner` while that owner still holds the lease
* SHOULD succeed (refresh), so a restart with a stable identity is not locked
* out by its own previous lease.
* 5. `lease.release()` MUST be idempotent, MUST NOT throw, and MUST NOT release a
* lease that has since been granted to a different owner.
* 6. Precision is NOT required. Clock skew, an expiry that fires while the
* holder is still working, two simultaneous winners — all acceptable. The
* lock is advisory; the only cost of getting it wrong is a duplicated delete.
* 7. `acquire` SHOULD return promptly and MUST NOT block waiting for the lock.
* The pruner treats "held" as "skip this tick", not "queue up" — waiting
* would rebuild the pile-up this whole change exists to remove.
*
* ## What the pruner does with it
*
* Once per cycle, before any delete: `acquire({ key, owner, ttlMs })`. On
* `'held'` the cycle is skipped entirely (no `PruneRun` is recorded, `pruneNow()`
* resolves `0`). On `'unavailable'` the cycle runs unlocked and one warning is
* logged per streak. On success the cycle runs and the lease is released in a
* `finally`, whether the cycle succeeded, failed, or threw.
*
* @example a host implementation over a singleton-mutex job engine
* ```ts
* class DurablePruneLock implements TelescopePruneLock {
* constructor(private readonly durable: DurableClient) {}
* async acquire({ key, owner, ttlMs }: TelescopePruneLockRequest) {
* try {
* const started = await this.durable.tryStartSingleton(key, { ttlMs, owner });
* if (!started) return pruneLockHeld('another pod holds the singleton');
* return pruneLockAcquired({
* key,
* owner,
* expiresAtMs: Date.now() + ttlMs,
* release: () => this.durable.finishSingleton(key, owner).catch(() => undefined),
* });
* } catch (error) {
* return pruneLockUnavailable(String(error));
* }
* }
* }
* ```
*/
export interface TelescopePruneLock {
acquire(request: TelescopePruneLockRequest): Promise<TelescopePruneLockResult>;
}
/** Builds the success arm of a {@link TelescopePruneLockResult}. */
export declare function pruneLockAcquired(lease: TelescopePruneLease): TelescopePruneLockResult;
/** Builds the "somebody else has it" arm — the healthy, expected refusal. */
export declare function pruneLockHeld(detail?: string): TelescopePruneLockResult;
/** Builds the "the lock mechanism itself failed" arm — the pruner fails OPEN on this. */
export declare function pruneLockUnavailable(detail?: string): TelescopePruneLockResult;
/**
* The DEFAULT prune lock: a lease row in the store Telescope is already using.
*
* Telescope always has a database — that is where the entries live — so a lease
* with an owner and an expiry costs no new dependency, no new deployment
* concern, and works on every provider that implements the lease SPI. A holder
* that dies never releases; the expiry is what reclaims it, which is why the SPI
* takes a TTL rather than a plain "locked" flag.
*
* Used automatically when the configured provider {@link isLeaseCapableStorage}
* and the host supplied no `prune.lock` of its own.
*/
export declare class StorageLeasePruneLock implements TelescopePruneLock {
private readonly storage;
constructor(storage: LeaseCapableStorage);
acquire(request: TelescopePruneLockRequest): Promise<TelescopePruneLockResult>;
}
//# sourceMappingURL=prune-lock.d.ts.map