/** * Step-2 epoch fencing for `ownership: 'lease'` (issue #470), extended by ADR * 0002 to per-workflow fencing under `ownership: 'workflow-lease'`. * * Step 1 made a rolling deploy a clean lease handoff but added no correctness * backstop: a deposed zombie instance (GC pause, partition) that emerged after * its lease expired could still issue a plain durable write and corrupt the * successor's state. Step 2 closes that hole by conditioning every * engine-generator-owned per-workflow durable write on the held lease epoch, so * a deposed instance's write loses its CAS instead of landing. * * The correctness guarantee lives entirely in the epoch CONDITION: a deposed * instance's commit CAS-fails regardless of any in-process flag, because the * successor bumped `lease:epoch` when it took over. The {@link EngineInternals.deposed} * flag and the engine teardown are hygiene on top of that — they stop the engine * from silently spinning on swallowed write losses, surface the deposition to the * operator, and release resources. They are not what makes the system safe. * * ADR 0002 adds a SECOND, independent fencing token: `wf-owner-epoch:`. * Every caller of {@link commitFencedEngineWrite} / {@link * commitFencedEngineWriteAllowingPreconditionFailure} now states, via the * required `workflowId` parameter, whether the write is scoped to one * workflow's execution (pass its id) or is engine-scoped/cross-workflow (pass * `null`) — an optional parameter would let a call site silently forget the * fence, which is exactly the correctness hole this mechanism exists to close. * Under `ownership: 'workflow-lease'` with a non-null `workflowId`, the write * is fenced on THAT workflow's claim epoch instead of the global lease epoch; * losing that fence deposes only that one workflow (warn + throw), never the * whole engine. Under `'lease'` and `'none'`, and under `'workflow-lease'` * with `workflowId: null`, behavior is byte-for-byte unchanged from Step 2 — * `workflowId` is only consulted in the one new branch. * * This module is allow-listed for import only from `src/core/engine/**`. */ import type { BatchOperation, ConditionalBatchCondition } from '../../storage/interface.ts'; import type { EngineInternals } from './internals.ts'; /** * Reject an engine-owned work entry point (start, startOrSignal, fork, resume) * when `ownership: 'lease'` is configured but the engine does not currently hold * the lease. The lease is acquired at the two boot gates ({@link Engine.create} * and {@link Engine.recoverAll}); a directly constructed engine that does engine * work before `recoverAll()` would otherwise durably write workflow state without * single-writer ownership and without having recovered existing runs. Placed at * each public awaited entry so the caller gets a clean {@link EngineLeaseNotHeldError} * — without this, fork, resume, and the schedule mutators (`schedule`, * `pauseSchedule`, `resumeSchedule`, `cancelSchedule`, and `updateSchedule`) would * reach `resolveFenceEpochOrHalt` with no held epoch and be misreported as a * deposition (warn + teardown) rather than the true "lease not held yet" * condition. A no-op under `ownership: 'none'` and under `ownership: * 'workflow-lease'` — the analogous "claim not held yet" guard for a * workflow-scoped write is a later stage's concern (folding `acquire()` into * the enabling write), not this global-lease-only assertion. */ export declare function assertLeaseHeldForEngineWork(internals: EngineInternals): void; /** * Commit an engine-generator-owned durable write, fenced on the applicable * epoch: the global lease epoch under `ownership: 'lease'`, this workflow's * claim epoch under `ownership: 'workflow-lease'` when `workflowId` is * non-null, or — under `'none'` and every other `workflowId`/mode * combination — byte-for-byte unfenced. A lost CAS race throws the * caller-supplied `onLostRace` error so existing retry semantics apply; a * deposition halts (the engine under global `'lease'`, or just this one * workflow under `'workflow-lease'`) and throws {@link EngineDeposedError}. * The helper owns the batch-vs-conditionalBatch decision — pass plain * operations plus whatever base conditions you already need. * * @param internals - the engine internals (ownership mode, lease manager, * workflow claim registry, deposed flag) * @param workflowId - the workflow this write is scoped to, fenced on that * workflow's claim epoch under `ownership: 'workflow-lease'`; or `null` for * an engine-scoped/cross-workflow write, which is never fenced on a * per-workflow claim regardless of ownership mode. Required — there is no * default — so every call site states its scope explicitly. * @param operations - the durable operations to commit atomically * @param baseConditions - CAS conditions the caller already requires (may be empty) * @param onLostRace - builds the error thrown on a same-epoch lost CAS race */ export declare function commitFencedEngineWrite(internals: EngineInternals, workflowId: string | null, operations: BatchOperation[], baseConditions: ConditionalBatchCondition[], onLostRace: () => Error): Promise; /** * Like {@link commitFencedEngineWrite}, but the caller treats a base-precondition * failure as a legitimate outcome rather than an error — used by the idempotent * start path, where a `false` means a concurrent same-key caller already wrote the * record (resolve to the existing run) rather than "retry". Returns `true` when the * batch committed and `false` when a base condition failed. Deposition is still a * hard halt: if the epoch condition is the one that failed, this drives the * applicable deposition path and throws {@link EngineDeposedError} — a deposed * engine (or a deposed single workflow, under `workflow-lease`) must never report a * precondition-failure the caller would read as "already exists" and silently * move on. * * @param internals - the engine internals * @param workflowId - see {@link commitFencedEngineWrite} — the workflow this * write is scoped to, or `null` for an engine-scoped write * @param operations - the durable operations to commit atomically * @param baseConditions - the caller's required CAS conditions (non-empty in * practice — this path is for conditional starts, which always carry one) */ export declare function commitFencedEngineWriteAllowingPreconditionFailure(internals: EngineInternals, workflowId: string | null, operations: BatchOperation[], baseConditions: ConditionalBatchCondition[]): Promise;