@lostgradient/weft 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli/generated/operation-catalog.snapshot.json +1334 -0
- package/dist/cli/generated/operation-client.generated.d.ts +9 -2
- package/dist/cli/generated/operation-client.generated.js +2 -0
- package/dist/core/engine/activity-reconciliation.d.ts +2 -2
- package/dist/core/engine/activity-reconciliation.js +12 -8
- package/dist/core/engine/async-activity-completion.js +12 -4
- package/dist/core/engine/async-activity-records.d.ts +11 -1
- package/dist/core/engine/async-activity-records.js +4 -3
- package/dist/core/engine/attributes-tags.d.ts +11 -1
- package/dist/core/engine/attributes-tags.js +4 -3
- package/dist/core/engine/bulk-operations-purge.js +5 -2
- package/dist/core/engine/bulk-operations.js +17 -5
- package/dist/core/engine/callback-creators-bundles.js +1 -1
- package/dist/core/engine/callback-creators-core.js +4 -2
- package/dist/core/engine/checkpoint-io.js +1 -1
- package/dist/core/engine/child-workflow.js +2 -1
- package/dist/core/engine/completed-review-storage.js +1 -1
- package/dist/core/engine/condition-waiters.d.ts +45 -3
- package/dist/core/engine/condition-waiters.js +12 -3
- package/dist/core/engine/disposal.js +6 -0
- package/dist/core/engine/engine-internal-types.d.ts +12 -4
- package/dist/core/engine/fenced-write.d.ts +46 -15
- package/dist/core/engine/fenced-write.js +39 -6
- package/dist/core/engine/finalizer-state.d.ts +3 -1
- package/dist/core/engine/handle-result.d.ts +49 -1
- package/dist/core/engine/handle-result.js +95 -20
- package/dist/core/engine/index.d.ts +13 -5
- package/dist/core/engine/index.js +155 -5
- package/dist/core/engine/inline-launch-queue.js +9 -0
- package/dist/core/engine/internals.d.ts +73 -0
- package/dist/core/engine/invoke-update-handler.d.ts +19 -0
- package/dist/core/engine/invoke-update-handler.js +7 -0
- package/dist/core/engine/lease-deposition.d.ts +132 -0
- package/dist/core/engine/lease-deposition.js +31 -0
- package/dist/core/engine/lease-errors.d.ts +80 -9
- package/dist/core/engine/lease-errors.js +28 -2
- package/dist/core/engine/lifecycle/resume.js +3 -1
- package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +33 -0
- package/dist/core/engine/lifecycle/standalone-claim-acquire.js +25 -0
- package/dist/core/engine/lifecycle/start-commit.js +20 -6
- package/dist/core/engine/lifecycle/transition.d.ts +15 -1
- package/dist/core/engine/lifecycle/transition.js +6 -3
- package/dist/core/engine/operations-activity.js +1 -1
- package/dist/core/engine/operations-time.js +21 -7
- package/dist/core/engine/owner-side-signal-poll.d.ts +132 -0
- package/dist/core/engine/owner-side-signal-poll.js +22 -0
- package/dist/core/engine/owner-side-update-poll.d.ts +116 -0
- package/dist/core/engine/owner-side-update-poll.js +22 -0
- package/dist/core/engine/ownership-bootstrap.d.ts +280 -0
- package/dist/core/engine/ownership-bootstrap.js +89 -0
- package/dist/core/engine/ownership-mode-marker.d.ts +82 -0
- package/dist/core/engine/ownership-mode-marker.js +52 -0
- package/dist/core/engine/ownership-options.d.ts +22 -6
- package/dist/core/engine/ownership-options.js +42 -12
- package/dist/core/engine/pending-updates.js +35 -24
- package/dist/core/engine/queries.d.ts +70 -0
- package/dist/core/engine/queries.js +58 -4
- package/dist/core/engine/schedules.js +1 -1
- package/dist/core/engine/signals.d.ts +11 -1
- package/dist/core/engine/signals.js +11 -7
- package/dist/core/engine/sleep-timer-acknowledgements.d.ts +68 -0
- package/dist/core/engine/sleep-timer-acknowledgements.js +19 -0
- package/dist/core/engine/storage-io.d.ts +58 -10
- package/dist/core/engine/storage-io.js +27 -9
- package/dist/core/engine/termination/cleanup.d.ts +5 -4
- package/dist/core/engine/termination/complete.js +23 -4
- package/dist/core/engine/termination/finalizer-claim.js +4 -4
- package/dist/core/engine/termination/state-commit-callbacks.d.ts +15 -0
- package/dist/core/engine/termination/state-commit-callbacks.js +0 -0
- package/dist/core/engine/termination/suspend.js +1 -1
- package/dist/core/engine/update-validation.d.ts +28 -0
- package/dist/core/engine/update-validation.js +37 -0
- package/dist/core/engine/updates.d.ts +17 -16
- package/dist/core/engine/updates.js +24 -47
- package/dist/core/engine/wake-ownership-check.d.ts +86 -0
- package/dist/core/engine/wake-ownership-check.js +19 -0
- package/dist/core/engine/wake-ownership-guard.d.ts +46 -0
- package/dist/core/engine/wake-ownership-guard.js +23 -0
- package/dist/core/engine/workflow-claim-codec.d.ts +64 -0
- package/dist/core/engine/workflow-claim-codec.js +44 -0
- package/dist/core/engine/workflow-claim-cooldown.d.ts +60 -0
- package/dist/core/engine/workflow-claim-cooldown.js +17 -0
- package/dist/core/engine/workflow-claim-fold.d.ts +82 -0
- package/dist/core/engine/workflow-claim-fold.js +53 -0
- package/dist/core/engine/workflow-claim-metrics.d.ts +131 -0
- package/dist/core/engine/workflow-claim-metrics.js +62 -0
- package/dist/core/engine/workflow-claim-reclaim-scan.d.ts +96 -0
- package/dist/core/engine/workflow-claim-reclaim-scan.js +66 -0
- package/dist/core/engine/workflow-claim-reclaim-target.d.ts +107 -0
- package/dist/core/engine/workflow-claim-reclaim-target.js +155 -0
- package/dist/core/engine/workflow-claim-registry.d.ts +227 -0
- package/dist/core/engine/workflow-claim-registry.js +198 -0
- package/dist/core/engine/workflow-claim-renewal-interval.d.ts +50 -0
- package/dist/core/engine/workflow-claim-renewal-interval.js +132 -0
- package/dist/core/engine/workflow-claim-renewal-subpasses.d.ts +265 -0
- package/dist/core/engine/workflow-claim-renewal-subpasses.js +104 -0
- package/dist/core/engine/workflow-claim-renewal-task.d.ts +159 -0
- package/dist/core/engine/workflow-claim-renewal-task.js +65 -0
- package/dist/core/engine/workflow-claim-transitions.d.ts +186 -0
- package/dist/core/engine/workflow-claim-transitions.js +120 -0
- package/dist/core/inline-execution-strategy.d.ts +20 -0
- package/dist/core/inline-execution-strategy.js +15 -4
- package/dist/core/scheduler/scheduler-class.js +3 -3
- package/dist/core/scheduler/timer-sources.d.ts +13 -0
- package/dist/core/scheduler/timer-sources.js +1 -1
- package/dist/core/types/options.d.ts +17 -1
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +2 -1
- package/dist/http.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -0
- package/dist/indexeddb.js +1 -1
- package/dist/json-schema.js +3 -3
- package/dist/observability/index.js +2 -2
- package/dist/server/operations/get-task-detail-schema.d.ts +341 -0
- package/dist/server/operations/get-task-detail-schema.js +118 -0
- package/dist/server/operations/get-task-detail.d.ts +346 -0
- package/dist/server/operations/get-task-detail.js +215 -0
- package/dist/server/operations/static-registrations.js +3 -0
- package/dist/storage/auto.js +1 -1
- package/dist/storage/bun-sql.js +113 -0
- package/dist/storage/compressed-storage.js +1 -1
- package/dist/storage/index.d.ts +38 -35
- package/dist/storage/interface.d.ts +87 -125
- package/dist/storage/interface.js +1 -1
- package/dist/storage/key-encoding.d.ts +51 -0
- package/dist/storage/key-encoding.js +13 -0
- package/dist/storage/key-prefixes.d.ts +1 -1
- package/dist/storage/key-prefixes.js +3 -0
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon.js +2 -2
- package/dist/storage/node-sqlite.js +113 -0
- package/dist/storage/ownership-keys.d.ts +55 -0
- package/dist/storage/ownership-keys.js +6 -0
- package/dist/storage/postgres.js +2 -2
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/testing.js +1 -1
- package/dist/storage/turso.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web-extension.js +1 -1
- package/dist/worker/protocol.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WeftError } from '../weft-error.ts';
|
|
2
|
+
import type { FencingOwnershipMode } from './workflow-claim-codec.ts';
|
|
2
3
|
/**
|
|
3
4
|
* Thrown when `ownership: 'lease'` is configured and the engine cannot acquire the
|
|
4
5
|
* storage lease within the configured wait window — another live instance still
|
|
@@ -82,18 +83,88 @@ export declare class EngineLeaseNotHeldError extends WeftError<'EngineLeaseNotHe
|
|
|
82
83
|
constructor();
|
|
83
84
|
}
|
|
84
85
|
/**
|
|
85
|
-
* Thrown internally on the durable-write path when `ownership
|
|
86
|
-
* configured and a fenced write's CAS fails because the held
|
|
87
|
-
* stale — a successor instance has taken
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
86
|
+
* Thrown internally on the durable-write path when either fencing `ownership`
|
|
87
|
+
* mode is configured and a fenced write's CAS fails because the held epoch is
|
|
88
|
+
* stale — a successor instance has taken over, so this instance has been
|
|
89
|
+
* deposed. It is the local unwind that stops the commit from advancing
|
|
90
|
+
* in-memory state past a durable write that did not land; the actual "this
|
|
91
|
+
* engine is deposed" reaction (set the `deposed` flag, warn the operator, tear
|
|
92
|
+
* the engine down) is driven by {@link handleDeposition} at the detection
|
|
92
93
|
* site, not by this error propagating — the inline strategy swallows turn
|
|
93
94
|
* rejections, so the throw never reaches a handler. Internal-only: it is never
|
|
94
95
|
* surfaced to user code (deposition reaches operators through the
|
|
95
|
-
* `WeftEngineLeaseLostWarning` process warning
|
|
96
|
+
* `WeftEngineLeaseLostWarning` process warning, or, under `workflow-lease`,
|
|
97
|
+
* through `WeftWorkflowClaimLostWarning`).
|
|
98
|
+
*
|
|
99
|
+
* `workflowId` is additive: under the global `ownership: 'lease'` mode
|
|
100
|
+
* deposition is store-wide and no single workflow is implicated, so existing
|
|
101
|
+
* call sites that construct `new EngineDeposedError()` with no argument keep
|
|
102
|
+
* working unchanged. Under `ownership: 'workflow-lease'`, deposition is scoped
|
|
103
|
+
* to one workflow's fenced epoch, so callers on that path supply the id and it
|
|
104
|
+
* folds into the message.
|
|
96
105
|
*/
|
|
97
106
|
export declare class EngineDeposedError extends WeftError<'EngineDeposedError'> {
|
|
98
|
-
|
|
107
|
+
readonly workflowId: string | undefined;
|
|
108
|
+
constructor(workflowId?: string);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Thrown when an explicit, single-workflow public API — {@link Engine.resume},
|
|
112
|
+
* or a per-workflow step inside a bulk operation — loses the `acquire`,
|
|
113
|
+
* `takeover`, or standalone-resume CAS for a workflow's ownership claim under
|
|
114
|
+
* `ownership: 'workflow-lease'`, because another engine still holds a live,
|
|
115
|
+
* unexpired claim on it. Never thrown from background scanning: `recoverAll`
|
|
116
|
+
* and the scheduler tick isolate the loss to that one workflow and continue
|
|
117
|
+
* with the rest of the sweep, exactly as a background scan skips a workflow it
|
|
118
|
+
* cannot acquire rather than failing the whole pass.
|
|
119
|
+
*
|
|
120
|
+
* Like the other lease errors in this module, this does not carry a stable
|
|
121
|
+
* {@link WeftErrorCode} — match by `instanceof` rather than `.code`.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* import { WorkflowClaimUnavailableError } from '@lostgradient/weft';
|
|
126
|
+
*
|
|
127
|
+
* function isClaimContested(error: unknown): boolean {
|
|
128
|
+
* return error instanceof WorkflowClaimUnavailableError;
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export declare class WorkflowClaimUnavailableError extends WeftError<'WorkflowClaimUnavailableError'> {
|
|
133
|
+
readonly workflowId: string;
|
|
134
|
+
readonly heldBy: string | null;
|
|
135
|
+
constructor(workflowId: string, heldBy: string | null);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Thrown at engine construction when Gate 2 (the store-wide
|
|
139
|
+
* `ownership-mode-marker` check) finds the store already stamped with a
|
|
140
|
+
* different fencing mode than this engine is configured with. Fired
|
|
141
|
+
* immediately after Gate 1 (storage `conditionalBatch` capability) passes, for
|
|
142
|
+
* `ownership: 'lease'` or `ownership: 'workflow-lease'` — `ownership: 'none'`
|
|
143
|
+
* engines never touch the marker. Raised before `recoverAll()` or any storage
|
|
144
|
+
* write beyond the marker read/CAS attempt, so construction fails closed
|
|
145
|
+
* rather than letting two incompatible fencing modes coexist against one
|
|
146
|
+
* store.
|
|
147
|
+
*
|
|
148
|
+
* Unlike the other lease errors in this module, this one DOES carry a stable
|
|
149
|
+
* {@link WeftErrorCode} — it is a configuration-time boot blocker an operator
|
|
150
|
+
* needs to route and alert on, not an internal fencing unwind.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* import { OwnershipModeMismatchError } from '@lostgradient/weft';
|
|
155
|
+
*
|
|
156
|
+
* function describeMismatch(error: OwnershipModeMismatchError): string {
|
|
157
|
+
* return `configured "${error.configuredMode}" but store is "${error.storedMode}" ` +
|
|
158
|
+
* `(established ${new Date(error.establishedAt).toISOString()})`;
|
|
159
|
+
* }
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export declare class OwnershipModeMismatchError extends WeftError<'OwnershipModeMismatchError'> {
|
|
163
|
+
/** This engine's configured `ownership` option. */
|
|
164
|
+
readonly configuredMode: FencingOwnershipMode;
|
|
165
|
+
/** The fencing mode read from the store's `ownership-mode-marker`. */
|
|
166
|
+
readonly storedMode: FencingOwnershipMode;
|
|
167
|
+
/** The marker's `establishedAt` timestamp, for diagnosing when the mismatch was introduced. */
|
|
168
|
+
readonly establishedAt: number;
|
|
169
|
+
constructor(configuredMode: FencingOwnershipMode, storedMode: FencingOwnershipMode, establishedAt: number);
|
|
99
170
|
}
|
|
@@ -24,7 +24,33 @@ export class EngineLeaseNotHeldError extends WeftError {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export class EngineDeposedError extends WeftError {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
workflowId;
|
|
28
|
+
constructor(workflowId) {
|
|
29
|
+
const scopeClause = workflowId === void 0 ? "another instance now holds the ownership lease at a newer epoch" : `another engine now holds workflow "${workflowId}"'s ownership claim at a newer epoch`;
|
|
30
|
+
super("EngineDeposedError", `Engine was deposed: a fenced durable write lost its CAS race because ${scopeClause}. This engine is halting.`);
|
|
31
|
+
this.workflowId = workflowId;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class WorkflowClaimUnavailableError extends WeftError {
|
|
36
|
+
workflowId;
|
|
37
|
+
heldBy;
|
|
38
|
+
constructor(workflowId, heldBy) {
|
|
39
|
+
const heldClause = heldBy === null ? "" : ` (currently held by "${heldBy}")`;
|
|
40
|
+
super("WorkflowClaimUnavailableError", `Could not acquire the ownership claim for workflow "${workflowId}"${heldClause}. Another engine still holds a live, unexpired claim for it. Retry later, or investigate whether the holding engine has crashed without releasing (it will become eligible for takeover once its claim expires).`);
|
|
41
|
+
this.workflowId = workflowId;
|
|
42
|
+
this.heldBy = heldBy;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class OwnershipModeMismatchError extends WeftError {
|
|
47
|
+
configuredMode;
|
|
48
|
+
storedMode;
|
|
49
|
+
establishedAt;
|
|
50
|
+
constructor(configuredMode, storedMode, establishedAt) {
|
|
51
|
+
super("OwnershipModeMismatchError", `This engine is configured with ownership: '${configuredMode}', but the store's ownership-mode-marker records '${storedMode}' (established at ${new Date(establishedAt).toISOString()}). Every engine sharing a store under a fencing mode must agree on that mode. Stop every engine pointed at this store, pick one mode, ` + "and restart them all \u2014 mixing modes against one store is unsafe.");
|
|
52
|
+
this.configuredMode = configuredMode;
|
|
53
|
+
this.storedMode = storedMode;
|
|
54
|
+
this.establishedAt = establishedAt;
|
|
29
55
|
}
|
|
30
56
|
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
loadWorkflowStartHeaders,
|
|
26
26
|
setWorkflowStartHeaders
|
|
27
27
|
} from "./shared.js";
|
|
28
|
+
import { acquireStandaloneClaimBeforeResume } from "./standalone-claim-acquire.js";
|
|
28
29
|
async function prepareRecoveredServicesOrFail(internals, state, callbacks, resolverInfo) {
|
|
29
30
|
return reprovideRecoveredServices(internals, state, callbacks.failWorkflowForUnavailableServices, callbacks.handleCleanupError, callbacks.dispatchEvent, resolverInfo);
|
|
30
31
|
}
|
|
@@ -159,7 +160,7 @@ async function reactivateSuspendedWorkflowState(internals, state) {
|
|
|
159
160
|
const previousState = { ...state };
|
|
160
161
|
state.status = "running";
|
|
161
162
|
state.updatedAt = internals.options.getNow();
|
|
162
|
-
await commitFencedEngineWrite(internals, [
|
|
163
|
+
await commitFencedEngineWrite(internals, state.id, [
|
|
163
164
|
{ type: "put", key: KEYS.workflow(state.id), value: encode(state) },
|
|
164
165
|
...buildWorkflowVisibilityIndexTransition(state.id, previousState, state).batchOps,
|
|
165
166
|
...state.executionDeadline !== void 0 ? buildTimerBatchOperations({
|
|
@@ -177,6 +178,7 @@ export async function resumeWorkflowFromStorage(internals, workflowId, dispatchR
|
|
|
177
178
|
const state = decodeWorkflowState(stateBytes);
|
|
178
179
|
if (state.status !== "running" && state.status !== "suspended")
|
|
179
180
|
throw Error(`Cannot resume workflow "${workflowId}": status is "${state.status}", expected "running" or "suspended"`);
|
|
181
|
+
await acquireStandaloneClaimBeforeResume(internals, workflowId);
|
|
180
182
|
const checkpointBytes = await internals.storage.get(KEYS.checkpoint(workflowId));
|
|
181
183
|
if (!checkpointBytes)
|
|
182
184
|
throw Error(`Checkpoint not found for workflow "${workflowId}"`);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR 0002 § "Two additional transitions": `acquire (standalone resume)`. A
|
|
3
|
+
* workflow found `running` OR `suspended` at recovery has no other enabling
|
|
4
|
+
* write here to fold a claim into, so this commits the acquire fragment
|
|
5
|
+
* ALONE — the one named exception to "never a standalone commit". Attempted
|
|
6
|
+
* BEFORE `prepareResumeState`, `onRecoveredWorkflow`, and generator relaunch,
|
|
7
|
+
* so no user code — and no earlier fenced write this function makes, such as
|
|
8
|
+
* the history-policy circuit breaker or a services-unavailable failure —
|
|
9
|
+
* can run without a held claim.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately covers `running` and `suspended` the same way. The ADR
|
|
12
|
+
* describes `suspended` as folding `acquire` into the status flip itself
|
|
13
|
+
* (`reactivateSuspendedWorkflowState`'s commit in `resume.ts`), but the
|
|
14
|
+
* EARLIER fenced writes above need a held claim regardless of final status,
|
|
15
|
+
* so one early standalone acquire is the pragmatic shape for both — a
|
|
16
|
+
* documented deviation from that row, not an oversight.
|
|
17
|
+
*
|
|
18
|
+
* A no-op when folding does not apply (`ownership !== 'workflow-lease'`, no
|
|
19
|
+
* registry constructed yet) or when this engine already tracks a claim for
|
|
20
|
+
* `workflowId` — a parked signal-driven wake (`inline-parking.ts`) or a bulk
|
|
21
|
+
* retry that folded `acquire` into its own reactivation write before calling
|
|
22
|
+
* `resume` (`bulk-operations.ts`) — PROVIDED `wakeOwnershipCheck` confirms
|
|
23
|
+
* the durable holder still matches the cached generation. A stalled, expired
|
|
24
|
+
* engine keeps `currentEpoch(workflowId) !== null` until its next renewal
|
|
25
|
+
* CAS detects the loss; trusting the cache alone would replay the parked
|
|
26
|
+
* generator against a successor. `acquire()` itself is unsafe as the
|
|
27
|
+
* re-check (it can win the CAS against a still-valid claim), so only the
|
|
28
|
+
* durable holder is re-read (no write) and compared.
|
|
29
|
+
*
|
|
30
|
+
* @module core/engine/lifecycle/standalone-claim-acquire
|
|
31
|
+
*/
|
|
32
|
+
import type { EngineInternals } from '../internals.ts';
|
|
33
|
+
export declare function acquireStandaloneClaimBeforeResume(internals: EngineInternals, workflowId: string): Promise<void>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { WorkflowClaimUnavailableError } from "../lease-errors.js";
|
|
2
|
+
import { wakeOwnershipCheck } from "../wake-ownership-check.js";
|
|
3
|
+
export async function acquireStandaloneClaimBeforeResume(internals, workflowId) {
|
|
4
|
+
if (internals.options.ownershipMode !== "workflow-lease")
|
|
5
|
+
return;
|
|
6
|
+
const registry = internals.workflowClaimRegistry;
|
|
7
|
+
if (registry === null)
|
|
8
|
+
return;
|
|
9
|
+
const cachedEpoch = registry.currentEpoch(workflowId);
|
|
10
|
+
if (cachedEpoch !== null) {
|
|
11
|
+
const check = await wakeOwnershipCheck({
|
|
12
|
+
storage: internals.storage,
|
|
13
|
+
workflowId,
|
|
14
|
+
wakeKind: "signal",
|
|
15
|
+
expectedEngineId: registry.engineId,
|
|
16
|
+
expectedEpoch: cachedEpoch
|
|
17
|
+
});
|
|
18
|
+
if (check.status === "discarded")
|
|
19
|
+
throw new WorkflowClaimUnavailableError(workflowId, check.observedEngineId);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const result = await registry.acquire(workflowId);
|
|
23
|
+
if (result.status === "lost-race")
|
|
24
|
+
throw new WorkflowClaimUnavailableError(workflowId, result.heldBy);
|
|
25
|
+
}
|
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
commitFencedEngineWrite,
|
|
5
5
|
commitFencedEngineWriteAllowingPreconditionFailure
|
|
6
6
|
} from "../fenced-write.js";
|
|
7
|
+
import {
|
|
8
|
+
commitWithWorkflowClaimFold,
|
|
9
|
+
prepareWorkflowClaimFold,
|
|
10
|
+
throwWorkflowClaimUnavailable
|
|
11
|
+
} from "../workflow-claim-fold.js";
|
|
7
12
|
import { buildStartBatchOperations } from "./start-batch.js";
|
|
8
13
|
|
|
9
14
|
export class StartIdempotencyRaceLostError extends Error {
|
|
@@ -13,13 +18,20 @@ export class StartIdempotencyRaceLostError extends Error {
|
|
|
13
18
|
}
|
|
14
19
|
}
|
|
15
20
|
const WORKFLOW_CONCURRENCY_ADMISSION_MAX_ATTEMPTS = 5;
|
|
16
|
-
async function persistStartBatch(internals, startOperations, conditions) {
|
|
21
|
+
async function persistStartBatch(internals, workflowId, startOperations, conditions, claimFold, isDelayedStart) {
|
|
22
|
+
if (claimFold) {
|
|
23
|
+
const result = await commitWithWorkflowClaimFold(internals, claimFold, startOperations, conditions.map((entry) => entry.condition), "workflow claim acquisition");
|
|
24
|
+
if (result.status === "committed")
|
|
25
|
+
return "committed";
|
|
26
|
+
return result.claimConflict ? "claim-lost" : "precondition-lost";
|
|
27
|
+
}
|
|
28
|
+
const fenceWorkflowId = isDelayedStart ? null : workflowId;
|
|
17
29
|
if (conditions.length === 0) {
|
|
18
|
-
await commitFencedEngineWrite(internals, startOperations, [], () => Error("Workflow start lost its CAS race."));
|
|
19
|
-
return
|
|
30
|
+
await commitFencedEngineWrite(internals, fenceWorkflowId, startOperations, [], () => Error("Workflow start lost its CAS race."));
|
|
31
|
+
return "committed";
|
|
20
32
|
}
|
|
21
33
|
requireStorageCapability(internals.storage, "conditionalBatch", "start preconditions");
|
|
22
|
-
return commitFencedEngineWriteAllowingPreconditionFailure(internals, startOperations, conditions.map((entry) => entry.condition));
|
|
34
|
+
return await commitFencedEngineWriteAllowingPreconditionFailure(internals, fenceWorkflowId, startOperations, conditions.map((entry) => entry.condition)) ? "committed" : "precondition-lost";
|
|
23
35
|
}
|
|
24
36
|
async function hasStartPreconditionConflict(internals, conditions) {
|
|
25
37
|
for (const entry of conditions) {
|
|
@@ -57,11 +69,13 @@ export async function buildAndCommitStartBatch(context, buildIdempotentStartOper
|
|
|
57
69
|
const startOperations = buildStartBatchOperations(internals, workflowId, state, checkpoint, registration, options, state.executionDeadline, context.delayedStartTimer, context.persistedWorkflowStartHeaders, mergeAdditionalStartOperations(context.additionalStartOperations, mergeAdditionalStartOperations(idempotent?.operations, workflowConcurrency?.operations)), context.callbacks, context.purgeDeleteOperations), conditions = [
|
|
58
70
|
...tagStartPreconditions(idempotent?.conditions),
|
|
59
71
|
...tagWorkflowConcurrencyConditions(workflowConcurrency?.conditions ?? [])
|
|
60
|
-
];
|
|
61
|
-
if (
|
|
72
|
+
], isDelayedStart = context.delayedStartTimer !== void 0, claimFold = isDelayedStart ? void 0 : await prepareWorkflowClaimFold(internals, workflowId), outcome = await persistStartBatch(internals, workflowId, startOperations, conditions, claimFold, isDelayedStart);
|
|
73
|
+
if (outcome === "committed")
|
|
62
74
|
return;
|
|
63
75
|
if (await hasStartPreconditionConflict(internals, conditions))
|
|
64
76
|
throw new StartIdempotencyRaceLostError;
|
|
77
|
+
if (outcome === "claim-lost")
|
|
78
|
+
return throwWorkflowClaimUnavailable(internals, workflowId);
|
|
65
79
|
if (workflowConcurrency === void 0)
|
|
66
80
|
throw new StartIdempotencyRaceLostError;
|
|
67
81
|
}
|
|
@@ -3,6 +3,20 @@ import { type WorkflowHandle } from '../handles.ts';
|
|
|
3
3
|
import type { EngineInternals } from '../internals.ts';
|
|
4
4
|
import { type LifecycleCallbacks, type RecoverAllOptions, type RegistrationEntry } from './shared.ts';
|
|
5
5
|
export declare function recoverAll(internals: EngineInternals, callbacks: LifecycleCallbacks, options?: RecoverAllOptions): Promise<WorkflowHandle[]>;
|
|
6
|
-
|
|
6
|
+
/** Options for {@link resume}. */
|
|
7
|
+
export type ResumeOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* Skip the local-ownership fast path and always replay from durable storage.
|
|
10
|
+
*
|
|
11
|
+
* Required by ADR 0002 reclaim-driven resume: deposition drops only the
|
|
12
|
+
* registry's claim entry, so local checkpoints, parked markers, contexts and
|
|
13
|
+
* generators survive. Without this, a reclaim by the same engine returns the
|
|
14
|
+
* pre-deposition handle without reaching `resumeWorkflowFromStorage()` and
|
|
15
|
+
* renews a run that never restarted from durable state. Ordinary
|
|
16
|
+
* `engine.resume()` leaves it `false` — nothing was deposed there.
|
|
17
|
+
*/
|
|
18
|
+
readonly forceReplayFromStorage?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export declare function resume(internals: EngineInternals, workflowId: string, callbacks: LifecycleCallbacks, onRecoveredWorkflow?: RecoverAllOptions['onRecoveredWorkflow'], options?: ResumeOptions): Promise<WorkflowHandle>;
|
|
7
21
|
export declare function fork(internals: EngineInternals, sourceWorkflowId: string, options: ForkOptions | undefined, callbacks: LifecycleCallbacks): Promise<WorkflowHandle>;
|
|
8
22
|
export declare function launchWorkflowFromCheckpoint(internals: EngineInternals, workflowId: string, state: WorkflowState, checkpoint: Checkpoint, registration: RegistrationEntry, callbacks: LifecycleCallbacks): WorkflowHandle;
|
|
@@ -11,6 +11,7 @@ import { hydrateCheckpointReplayState } from "../checkpoint-replay.js";
|
|
|
11
11
|
import { WorkflowTypeNotRegisteredForRecoveryError } from "../errors.js";
|
|
12
12
|
import { commitFencedEngineWrite } from "../fenced-write.js";
|
|
13
13
|
import { getWorkflowExecutionStartedAt } from "../handles.js";
|
|
14
|
+
import { WorkflowClaimUnavailableError } from "../lease-errors.js";
|
|
14
15
|
import { normalizeForkStep, selectPersistedWorkflowStartHeaders } from "../state-utilities.js";
|
|
15
16
|
import { loadWorkflowState } from "../storage-io.js";
|
|
16
17
|
import { getComposedWorkflowInterceptor } from "../strategy-helpers.js";
|
|
@@ -73,6 +74,8 @@ async function recoverEntryOrIsolateFailure(internals, workflowId, callbacks, op
|
|
|
73
74
|
await callbacks.failWorkflowForVersionMismatch(workflowId, error);
|
|
74
75
|
return null;
|
|
75
76
|
}
|
|
77
|
+
if (error instanceof WorkflowClaimUnavailableError)
|
|
78
|
+
return null;
|
|
76
79
|
throw error;
|
|
77
80
|
}
|
|
78
81
|
}
|
|
@@ -98,9 +101,9 @@ export async function recoverAll(internals, callbacks, options) {
|
|
|
98
101
|
}
|
|
99
102
|
return handles;
|
|
100
103
|
}
|
|
101
|
-
export async function resume(internals, workflowId, callbacks, onRecoveredWorkflow) {
|
|
104
|
+
export async function resume(internals, workflowId, callbacks, onRecoveredWorkflow, options) {
|
|
102
105
|
const workflowState = await loadWorkflowState(internals, workflowId);
|
|
103
|
-
if (workflowState !== null) {
|
|
106
|
+
if (workflowState !== null && options?.forceReplayFromStorage !== !0) {
|
|
104
107
|
if (callbacks.isInlineWorkflowLocallyOwned(workflowId, workflowState.status) || callbacks.hasLocalCheckpointOwnership(workflowId, workflowState.status)) {
|
|
105
108
|
await enforceHistoryPolicyBeforeReplayById(internals, workflowId, callbacks);
|
|
106
109
|
return callbacks.getHandle(workflowId);
|
|
@@ -130,7 +133,7 @@ export async function fork(internals, sourceWorkflowId, options, callbacks) {
|
|
|
130
133
|
let forkStarted = !1;
|
|
131
134
|
try {
|
|
132
135
|
const forkCheckpointBytes = serializeCheckpoint(forkCheckpoint);
|
|
133
|
-
await commitFencedEngineWrite(internals, buildForkBatchOperations(internals, workflowId, forkState, forkCheckpoint, forkCheckpointBytes, persistedWorkflowStartHeaders, callbacks), [], () => Error(`Fork of workflow "${workflowId}" lost its CAS race.`));
|
|
136
|
+
await commitFencedEngineWrite(internals, workflowId, buildForkBatchOperations(internals, workflowId, forkState, forkCheckpoint, forkCheckpointBytes, persistedWorkflowStartHeaders, callbacks), [], () => Error(`Fork of workflow "${workflowId}" lost its CAS race.`));
|
|
134
137
|
internals.eventLogHeads.set(workflowId, EMPTY_EVENT_HEAD);
|
|
135
138
|
setWorkflowStartHeaders(internals, workflowId, persistedWorkflowStartHeaders, callbacks);
|
|
136
139
|
const handle = launchWorkflowFromCheckpoint(internals, workflowId, forkState, forkCheckpoint, registration, callbacks);
|
|
@@ -128,7 +128,7 @@ export async function executeActivityOperationResult(internals, workflowId, oper
|
|
|
128
128
|
if (executionOptions.reconciliationCompletion === "immediate-fenced") {
|
|
129
129
|
const finishImmediateReconciliationCommit = executionOptions.beforeImmediateReconciliationCommit?.();
|
|
130
130
|
try {
|
|
131
|
-
await commitActivityReconciliationTransitionWithFencedWrite(internals, reference, started, completedRecord);
|
|
131
|
+
await commitActivityReconciliationTransitionWithFencedWrite(internals, workflowId, reference, started, completedRecord);
|
|
132
132
|
} finally {
|
|
133
133
|
finishImmediateReconciliationCommit?.();
|
|
134
134
|
}
|
|
@@ -2,13 +2,16 @@ import { KEYS, storageHas } from "../../storage/interface.js";
|
|
|
2
2
|
import { deserializeCheckpoint } from "../checkpoint.js";
|
|
3
3
|
import { encode } from "../codec.js";
|
|
4
4
|
import { buildTimerBatchOperations, normalizeStorageTimestamp } from "../scheduler.js";
|
|
5
|
+
import { notifyConditionWaiters, notifyConditionWaitersForTimerFire } from "./condition-waiters.js";
|
|
5
6
|
import { commitFencedEngineWrite } from "./fenced-write.js";
|
|
6
7
|
import { reprovideRecoveredServices } from "./lifecycle/recovered-services.js";
|
|
7
8
|
import {
|
|
8
9
|
acknowledgeSupersededSleepTimers,
|
|
9
10
|
handleSleepTimerWithAcknowledgement,
|
|
10
|
-
resolveSleepTimer
|
|
11
|
+
resolveSleepTimer,
|
|
12
|
+
retainDiscardedDurableTimer
|
|
11
13
|
} from "./sleep-timer-acknowledgements.js";
|
|
14
|
+
import { commitWithWorkflowClaimFold, prepareWorkflowClaimFold } from "./workflow-claim-fold.js";
|
|
12
15
|
import { buildWorkflowVisibilityIndexTransition } from "./workflow-indexes.js";
|
|
13
16
|
export function createDelayedStartTimerEntry(_internals, workflowId, scheduledStartAt, options, callbacks) {
|
|
14
17
|
return {
|
|
@@ -118,7 +121,10 @@ export async function startDelayedWorkflow(internals, entry, callbacks) {
|
|
|
118
121
|
fireAt: executionDeadline,
|
|
119
122
|
kind: "execution-deadline"
|
|
120
123
|
}));
|
|
121
|
-
await
|
|
124
|
+
const claimFold = await prepareWorkflowClaimFold(internals, entry.workflowId);
|
|
125
|
+
if (claimFold)
|
|
126
|
+
return (await commitWithWorkflowClaimFold(internals, claimFold, operations, [], "delayed-start workflow claim acquisition")).status === "committed" ? nextRunningState : null;
|
|
127
|
+
await commitFencedEngineWrite(internals, entry.workflowId, operations, [], () => Error(`Delayed-start transition for workflow "${entry.workflowId}" lost its CAS race.`));
|
|
122
128
|
return nextRunningState;
|
|
123
129
|
});
|
|
124
130
|
if (!runningState)
|
|
@@ -158,7 +164,7 @@ async function failInvalidDelayedExecutionTimeout(entry, callbacks) {
|
|
|
158
164
|
await callbacks.failWorkflow(entry.workflowId, Error(`Invalid delayed execution timeout for workflow "${entry.workflowId}"`));
|
|
159
165
|
}
|
|
160
166
|
export async function handleTimerFired(internals, entry, callbacks) {
|
|
161
|
-
if (
|
|
167
|
+
if (isReviewTimerEntry(entry)) {
|
|
162
168
|
await handleReviewTimer(internals, entry, callbacks);
|
|
163
169
|
return;
|
|
164
170
|
}
|
|
@@ -181,14 +187,22 @@ export async function handleTimerFired(internals, entry, callbacks) {
|
|
|
181
187
|
if (entry.kind === "sleep")
|
|
182
188
|
await handleSleepTimerWithAcknowledgement(internals, entry, callbacks.loadWorkflowState);
|
|
183
189
|
else if (entry.kind === "wait-condition")
|
|
184
|
-
|
|
190
|
+
if (internals.workflowClaimRegistry === null)
|
|
191
|
+
resolveConditionTimer(internals, entry);
|
|
192
|
+
else
|
|
193
|
+
await resolveConditionTimerConfirmingOwnership(internals, entry, callbacks.loadWorkflowState);
|
|
185
194
|
else if (entry.kind === "execution-deadline")
|
|
186
195
|
await callbacks.timeout(entry.workflowId);
|
|
187
196
|
}
|
|
197
|
+
function isReviewTimerEntry(entry) {
|
|
198
|
+
return entry.id.startsWith("review-escalation:") || entry.id.startsWith("review-timeout:");
|
|
199
|
+
}
|
|
188
200
|
function resolveConditionTimer(internals, entry) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
201
|
+
notifyConditionWaiters(internals, entry.workflowId);
|
|
202
|
+
}
|
|
203
|
+
async function resolveConditionTimerConfirmingOwnership(internals, entry, loadWorkflowState) {
|
|
204
|
+
if (await notifyConditionWaitersForTimerFire(internals, entry.workflowId) === "discard")
|
|
205
|
+
await retainDiscardedDurableTimer(entry.id, entry.workflowId, loadWorkflowState);
|
|
192
206
|
}
|
|
193
207
|
async function handleReviewTimer(internals, entry, callbacks) {
|
|
194
208
|
const reviewId = entry.id.split(":")[1];
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-side signal polling, specified in
|
|
3
|
+
* [ADR 0002 § Entry point classification](../../../documentation/contributing/architecture-decisions/0002-multiengine-per-workflow-ownership.md)
|
|
4
|
+
* (the paragraph beginning "Signal delivery needs more than a
|
|
5
|
+
* classification").
|
|
6
|
+
*
|
|
7
|
+
* **The problem.** Weft has no cross-engine RPC. A signal sent to any engine
|
|
8
|
+
* is durably buffered — but under `ownership: 'workflow-lease'`, only the
|
|
9
|
+
* engine that owns a workflow inspects its in-memory waiters when a signal
|
|
10
|
+
* arrives. If the signal lands on a NON-owning engine, the durable buffer is
|
|
11
|
+
* written and nobody wakes the owner: a workflow parked on
|
|
12
|
+
* `waitForSignal()` (including a `waitForSignal` branch inside `ctx.race()`)
|
|
13
|
+
* hangs until something unrelated happens to drive it.
|
|
14
|
+
*
|
|
15
|
+
* **The fix.** The owning engine re-checks the durable signal buffer for its
|
|
16
|
+
* OWN parked workflows on the same cadence it renews their claims, and wakes
|
|
17
|
+
* any whose awaited signal has since been buffered — by any engine. This
|
|
18
|
+
* bounds cross-engine signal delivery latency at one renewal interval rather
|
|
19
|
+
* than leaving it unbounded. Owner-side polling is the accepted mechanism;
|
|
20
|
+
* real cross-engine notification would be a later latency optimization, not
|
|
21
|
+
* a correctness fix.
|
|
22
|
+
*
|
|
23
|
+
* **Scope and independence.** This module is the poll pass alone: given a
|
|
24
|
+
* list of this engine's parked signal waits, probe the durable buffer for
|
|
25
|
+
* each, and wake the ones that are ready. It does not read
|
|
26
|
+
* `src/core/engine/signals.ts` or `src/core/engine/inline-parking.ts`
|
|
27
|
+
* directly — the dependencies below are small structural types this module
|
|
28
|
+
* owns, so it stays independent of those modules' concrete shapes. Each
|
|
29
|
+
* type's doc comment names the existing function it satisfies. This module
|
|
30
|
+
* is also NOT a recurring task by itself (compare
|
|
31
|
+
* `workflow-claim-renewal-task.ts`) — it is production-wired by
|
|
32
|
+
* `ownership-bootstrap.ts`'s {@link buildOwnerSideSignalPollTarget}, which
|
|
33
|
+
* composes {@link OwnerSideSignalPollSources} (from `EngineInternals` and
|
|
34
|
+
* `inline-parking.ts`) into a real {@link OwnerSideSignalPollTarget}, and by
|
|
35
|
+
* `workflow-claim-renewal-task.ts`, which calls {@link runOwnerSideSignalPoll}
|
|
36
|
+
* against that target from the same lifecycle cadence that drives claim
|
|
37
|
+
* renewal.
|
|
38
|
+
*
|
|
39
|
+
* @module core/engine/owner-side-signal-poll
|
|
40
|
+
*/
|
|
41
|
+
/** One workflow parked on `waitForSignal(signalName)` (or a `ctx.race()` branch awaiting it), that this engine owns. */
|
|
42
|
+
export type ParkedSignalWait = {
|
|
43
|
+
workflowId: string;
|
|
44
|
+
signalName: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The minimal structural shape this poll needs. Satisfied in production by
|
|
48
|
+
* `ownership-bootstrap.ts`'s `buildOwnerSideSignalPollTarget`, composed from
|
|
49
|
+
* `EngineInternals` and `signals.ts` / `inline-parking.ts` — without this
|
|
50
|
+
* module importing either.
|
|
51
|
+
*/
|
|
52
|
+
export type OwnerSideSignalPollTarget = {
|
|
53
|
+
/**
|
|
54
|
+
* Every signal wait this engine's currently-parked workflows are awaiting.
|
|
55
|
+
* Read fresh at the start of every pass — implementations may return a
|
|
56
|
+
* live or a defensive-copy array; this poll never mutates it and takes its
|
|
57
|
+
* own snapshot before iterating.
|
|
58
|
+
*
|
|
59
|
+
* Expected to be satisfied by deriving from `EngineInternals.signalWaiters`
|
|
60
|
+
* (a `Map<string, () => void>` keyed by `` `${workflowId}:${signalName}` ``,
|
|
61
|
+
* the same format `src/core/engine/signals.ts`'s `deliverBufferedSignals`
|
|
62
|
+
* reads), splitting each key back into its `workflowId`/`signalName` pair.
|
|
63
|
+
*/
|
|
64
|
+
listParkedSignalWaits(): readonly ParkedSignalWait[];
|
|
65
|
+
/**
|
|
66
|
+
* Probe whether `signalName` has already been durably buffered for
|
|
67
|
+
* `workflowId`, without consuming it — this poll only decides WHETHER to
|
|
68
|
+
* wake, never consumes the signal itself, so a losing `ctx.race()` branch
|
|
69
|
+
* is never silently dropped.
|
|
70
|
+
*
|
|
71
|
+
* Expected to be satisfied by `hasBufferedSignal` from
|
|
72
|
+
* `src/core/engine/signals.ts`.
|
|
73
|
+
*/
|
|
74
|
+
hasBufferedSignal(workflowId: string, signalName: string): Promise<boolean>;
|
|
75
|
+
/**
|
|
76
|
+
* Wake `workflowId` so it re-evaluates its parked state and, ultimately,
|
|
77
|
+
* consumes the buffered signal and resumes its generator. Resolves once
|
|
78
|
+
* the wake attempt has been dispatched; rejects on failure, which this
|
|
79
|
+
* poll catches and records without stopping the remaining wakes in the
|
|
80
|
+
* pass.
|
|
81
|
+
*
|
|
82
|
+
* Expected to be satisfied by `resumeParkedInlineWorkflow` from
|
|
83
|
+
* `src/core/engine/inline-parking.ts` (already surfaced to callers today
|
|
84
|
+
* through `SignalCallbacks.resumeParkedInlineWorkflow` in `signals.ts`).
|
|
85
|
+
* The real implementation is expected to perform its own
|
|
86
|
+
* `wakeOwnershipCheck` immediately before driving the generator, per the
|
|
87
|
+
* ADR — that check is this poll's sibling module, not this poll's
|
|
88
|
+
* responsibility.
|
|
89
|
+
*/
|
|
90
|
+
wakeWorkflow(workflowId: string): Promise<void>;
|
|
91
|
+
};
|
|
92
|
+
/** One parked signal wait's outcome within a single poll pass. */
|
|
93
|
+
export type OwnerSideSignalPollOutcome = {
|
|
94
|
+
workflowId: string;
|
|
95
|
+
signalName: string;
|
|
96
|
+
status: 'woken';
|
|
97
|
+
} | {
|
|
98
|
+
workflowId: string;
|
|
99
|
+
signalName: string;
|
|
100
|
+
status: 'not-buffered';
|
|
101
|
+
} | {
|
|
102
|
+
workflowId: string;
|
|
103
|
+
signalName: string;
|
|
104
|
+
status: 'wake-failed';
|
|
105
|
+
error: unknown;
|
|
106
|
+
};
|
|
107
|
+
/** The result of one full owner-side signal poll pass ({@link runOwnerSideSignalPoll}). */
|
|
108
|
+
export type OwnerSideSignalPollResult = {
|
|
109
|
+
/** `getNow()` read at the start of the pass, before any probe. */
|
|
110
|
+
startedAt: number;
|
|
111
|
+
/** `getNow()` read after every probe/wake has settled. */
|
|
112
|
+
finishedAt: number;
|
|
113
|
+
/** One entry per parked signal wait the pass attempted, in iteration order. */
|
|
114
|
+
outcomes: OwnerSideSignalPollOutcome[];
|
|
115
|
+
/** `outcomes.filter(o => o.status === 'woken').length`, precomputed for observability consumers. */
|
|
116
|
+
wokenCount: number;
|
|
117
|
+
};
|
|
118
|
+
/** Options for {@link runOwnerSideSignalPoll}. */
|
|
119
|
+
export type OwnerSideSignalPollOptions = {
|
|
120
|
+
target: OwnerSideSignalPollTarget;
|
|
121
|
+
/** Wall-clock source (ms), injected so tests never depend on real time. */
|
|
122
|
+
getNow: () => number;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Run exactly one owner-side signal poll pass: snapshot this engine's
|
|
126
|
+
* currently-parked signal waits, probe the durable buffer for each, and wake
|
|
127
|
+
* the ones that are ready. A wake failure for one parked workflow is
|
|
128
|
+
* captured as a `'wake-failed'` outcome and never stops the remaining
|
|
129
|
+
* parked workflows in the same pass from being probed and, where ready,
|
|
130
|
+
* woken.
|
|
131
|
+
*/
|
|
132
|
+
export declare function runOwnerSideSignalPoll(options: OwnerSideSignalPollOptions): Promise<OwnerSideSignalPollResult>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export async function runOwnerSideSignalPoll(options) {
|
|
2
|
+
const { target, getNow } = options, startedAt = getNow(), parkedWaits = target.listParkedSignalWaits(), outcomes = [];
|
|
3
|
+
for (const { workflowId, signalName } of parkedWaits) {
|
|
4
|
+
if (!await target.hasBufferedSignal(workflowId, signalName)) {
|
|
5
|
+
outcomes.push({ workflowId, signalName, status: "not-buffered" });
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
await target.wakeWorkflow(workflowId);
|
|
10
|
+
outcomes.push({ workflowId, signalName, status: "woken" });
|
|
11
|
+
} catch (error) {
|
|
12
|
+
outcomes.push({ workflowId, signalName, status: "wake-failed", error });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const finishedAt = getNow();
|
|
16
|
+
return {
|
|
17
|
+
startedAt,
|
|
18
|
+
finishedAt,
|
|
19
|
+
outcomes,
|
|
20
|
+
wokenCount: outcomes.filter((outcome) => outcome.status === "woken").length
|
|
21
|
+
};
|
|
22
|
+
}
|