@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
92 lines • 4.09 kB
JavaScript
// packages/core/src/prune/prune-lock.ts
//
// The cross-process prune lock seam.
//
// The pruner's in-flight guard bounds ONE PROCESS to one prune cycle. A
// deployment of N replicas sharing one store still runs N cycles at once, all
// deleting the same rows. That is not incorrect — the deletes are idempotent and
// one of them wins each row — it is pure waste, and on a store that is already
// behind the waste is what keeps it behind.
//
// So this seam is deliberately SMALL and deliberately ADVISORY. It is a named
// lock with a lease: acquire, release, and a way to say "somebody else has it".
// There is no fencing token, no renewal, no quorum, and none of that is an
// oversight — a lost lease costs one duplicated DELETE, never a wrong result, so
// paying for consensus here would be paying for nothing.
//
// Telescope must not depend on a job engine, a Redis client, or anything else
// the host may not have: it mounts into any NestJS app. Hence the seam plus a
// default implementation that rides the database Telescope already has (see
// `StorageLeasePruneLock`), and an escape hatch for a host that already owns a
// better primitive (`prune.lock`).
/**
* 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 const PRUNE_LOCK_KEY = 'telescope:prune';
/** Builds the success arm of a {@link TelescopePruneLockResult}. */
export function pruneLockAcquired(lease) {
return { acquired: true, lease };
}
/** Builds the "somebody else has it" arm — the healthy, expected refusal. */
export function pruneLockHeld(detail) {
return detail === undefined
? { acquired: false, reason: 'held' }
: { acquired: false, reason: 'held', detail };
}
/** Builds the "the lock mechanism itself failed" arm — the pruner fails OPEN on this. */
export function pruneLockUnavailable(detail) {
return detail === undefined
? { acquired: false, reason: 'unavailable' }
: { acquired: false, reason: 'unavailable', detail };
}
/**
* 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 class StorageLeasePruneLock {
storage;
constructor(storage) {
this.storage = storage;
}
async acquire(request) {
const { key, owner, ttlMs } = request;
const acquiredAtMs = Date.now();
let granted;
try {
granted = await this.storage.tryAcquireLease(key, owner, ttlMs, acquiredAtMs);
}
catch (error) {
// A store that cannot answer must not stop retention: report the MECHANISM
// as broken (not "held") so the pruner fails open and prunes unlocked.
return pruneLockUnavailable(error instanceof Error ? error.message : String(error));
}
if (!granted)
return pruneLockHeld();
return pruneLockAcquired({
key,
owner,
expiresAtMs: acquiredAtMs + ttlMs,
release: async () => {
try {
await this.storage.releaseLease(key, owner);
}
catch {
// Never throw out of release: the TTL already guarantees the lease is
// reclaimed, so a failed release delays the next cycle at worst.
}
},
});
}
}
//# sourceMappingURL=prune-lock.js.map