@lostgradient/weft 0.24.0 → 0.24.2
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/conformance.js +5 -2
- package/dist/cli/generated/operation-catalog.snapshot.json +95 -1
- package/dist/cli/generated/operation-client.generated.d.ts +28 -2
- package/dist/cli/generated/operation-client.generated.js +2 -0
- package/dist/cli/parse-schedule-arguments.js +15 -3
- package/dist/cli/schedule.js +2 -1
- package/dist/cli/types.d.ts +2 -1
- package/dist/client/http-client-requests.js +6 -9
- package/dist/client/start-body.js +1 -0
- package/dist/core/catalog/index.d.ts +2 -2
- package/dist/core/catalog/index.js +9 -2
- package/dist/core/catalog/reference-counts.d.ts +29 -14
- package/dist/core/catalog/removal.d.ts +63 -10
- package/dist/core/catalog/removal.js +18 -2
- package/dist/core/catalog/storage-io.d.ts +23 -0
- package/dist/core/catalog/storage-io.js +1 -1
- package/dist/core/engine/activity-resolution.js +17 -17
- package/dist/core/engine/bulk-operations-purge.d.ts +1 -1
- package/dist/core/engine/bulk-operations-purge.js +17 -22
- package/dist/core/engine/bulk-operations-retry.d.ts +24 -0
- package/dist/core/engine/bulk-operations-retry.js +190 -0
- package/dist/core/engine/bulk-operations-shared.d.ts +18 -0
- package/dist/core/engine/bulk-operations-shared.js +14 -0
- package/dist/core/engine/bulk-operations.d.ts +3 -2
- package/dist/core/engine/bulk-operations.js +10 -169
- package/dist/core/engine/callback-creators-bundles.js +5 -3
- package/dist/core/engine/callback-creators-core.js +9 -1
- package/dist/core/engine/callback-creators-schedule.js +2 -2
- package/dist/core/engine/catalog-readiness.js +2 -0
- package/dist/core/engine/catalog-removal.d.ts +107 -3
- package/dist/core/engine/catalog-removal.js +66 -13
- package/dist/core/engine/catalog-tombstone-recovery.d.ts +56 -0
- package/dist/core/engine/catalog-tombstone-recovery.js +39 -0
- package/dist/core/engine/child-workflow.d.ts +13 -1
- package/dist/core/engine/child-workflow.js +22 -4
- package/dist/core/engine/constraints.js +5 -1
- package/dist/core/engine/construction.d.ts +1 -1
- package/dist/core/engine/decode-revision.d.ts +32 -0
- package/dist/core/engine/decode-revision.js +9 -0
- package/dist/core/engine/disposal.d.ts +6 -2
- package/dist/core/engine/disposal.js +24 -10
- package/dist/core/engine/dynamic-source-errors.d.ts +81 -0
- package/dist/core/engine/dynamic-source-errors.js +25 -0
- package/dist/core/engine/dynamic-source-execution.d.ts +173 -0
- package/dist/core/engine/dynamic-source-execution.js +108 -0
- package/dist/core/engine/engine-internal-types.d.ts +16 -0
- package/dist/core/engine/engine-workflows-namespace.d.ts +10 -0
- package/dist/core/engine/engine-workflows-namespace.js +3 -1
- package/dist/core/engine/generation-codec.d.ts +22 -0
- package/dist/core/engine/generation-codec.js +2 -0
- package/dist/core/engine/index.d.ts +18 -10
- package/dist/core/engine/index.js +22 -11
- package/dist/core/engine/inline-launch-queue.js +1 -1
- package/dist/core/engine/internals.d.ts +26 -30
- package/dist/core/engine/lifecycle/checkpoint-launch.d.ts +29 -0
- package/dist/core/engine/lifecycle/checkpoint-launch.js +75 -0
- package/dist/core/engine/lifecycle/delayed-start-registration.d.ts +30 -0
- package/dist/core/engine/lifecycle/delayed-start-registration.js +11 -0
- package/dist/core/engine/lifecycle/fork-helpers.d.ts +33 -1
- package/dist/core/engine/lifecycle/fork-helpers.js +3 -1
- package/dist/core/engine/lifecycle/recovery-isolation.d.ts +20 -0
- package/dist/core/engine/lifecycle/recovery-isolation.js +26 -0
- package/dist/core/engine/lifecycle/recovery-revision-groups.d.ts +75 -0
- package/dist/core/engine/lifecycle/recovery-revision-groups.js +64 -0
- package/dist/core/engine/lifecycle/resume-body.d.ts +44 -0
- package/dist/core/engine/lifecycle/resume-body.js +211 -0
- package/dist/core/engine/lifecycle/resume-generation-guard.d.ts +58 -0
- package/dist/core/engine/lifecycle/resume-generation-guard.js +11 -0
- package/dist/core/engine/lifecycle/resume.d.ts +46 -3
- package/dist/core/engine/lifecycle/resume.js +21 -201
- package/dist/core/engine/lifecycle/shared.d.ts +43 -0
- package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +80 -2
- package/dist/core/engine/lifecycle/standalone-claim-acquire.js +20 -4
- package/dist/core/engine/lifecycle/start-commit-errors.d.ts +12 -0
- package/dist/core/engine/lifecycle/start-commit-errors.js +6 -0
- package/dist/core/engine/lifecycle/start-commit.d.ts +24 -7
- package/dist/core/engine/lifecycle/start-commit.js +57 -15
- package/dist/core/engine/lifecycle/start-exec.d.ts +18 -2
- package/dist/core/engine/lifecycle/start-exec.js +7 -5
- package/dist/core/engine/lifecycle/start-or-signal-create.js +1 -3
- package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +8 -2
- package/dist/core/engine/lifecycle/start-or-signal-resolution.js +11 -3
- package/dist/core/engine/lifecycle/start-or-signal.js +1 -1
- package/dist/core/engine/lifecycle/start-precondition-attribution.d.ts +55 -0
- package/dist/core/engine/lifecycle/start-precondition-attribution.js +13 -0
- package/dist/core/engine/lifecycle/start-revision-resolution.d.ts +34 -0
- package/dist/core/engine/lifecycle/start-revision-resolution.js +11 -0
- package/dist/core/engine/lifecycle/start-schedule-timing.d.ts +11 -0
- package/dist/core/engine/lifecycle/start-schedule-timing.js +21 -0
- package/dist/core/engine/lifecycle/start-state.d.ts +16 -0
- package/dist/core/engine/lifecycle/start-state.js +60 -0
- package/dist/core/engine/lifecycle/start-terminal-conflict-purge.d.ts +146 -4
- package/dist/core/engine/lifecycle/start-terminal-conflict-purge.js +33 -4
- package/dist/core/engine/lifecycle/start.d.ts +54 -7
- package/dist/core/engine/lifecycle/start.js +53 -90
- package/dist/core/engine/lifecycle/transition.d.ts +12 -4
- package/dist/core/engine/lifecycle/transition.js +40 -95
- package/dist/core/engine/lifecycle.d.ts +5 -2
- package/dist/core/engine/lifecycle.js +7 -6
- package/dist/core/engine/listing.js +15 -1
- package/dist/core/engine/memo-durable-activity.js +4 -1
- package/dist/core/engine/nonterminal-revision-count.d.ts +25 -0
- package/dist/core/engine/nonterminal-revision-count.js +16 -0
- package/dist/core/engine/operations-time.d.ts +5 -23
- package/dist/core/engine/operations-time.js +13 -11
- package/dist/core/engine/ownership-bootstrap.d.ts +1 -1
- package/dist/core/engine/pinned-schedule-revision-count.d.ts +26 -0
- package/dist/core/engine/pinned-schedule-revision-count.js +19 -0
- package/dist/core/engine/pinned-schedule-revision.d.ts +76 -0
- package/dist/core/engine/pinned-schedule-revision.js +53 -0
- package/dist/core/engine/registration.d.ts +30 -0
- package/dist/core/engine/registration.js +17 -2
- package/dist/core/engine/retention.js +9 -2
- package/dist/core/engine/revision-errors.d.ts +71 -0
- package/dist/core/engine/revision-errors.js +13 -0
- package/dist/core/engine/schedule-overlap.js +2 -1
- package/dist/core/engine/schedule-revision-fire.d.ts +17 -0
- package/dist/core/engine/schedule-revision-fire.js +3 -0
- package/dist/core/engine/schedule-run.d.ts +10 -0
- package/dist/core/engine/schedule-run.js +2 -1
- package/dist/core/engine/schedules.d.ts +11 -1
- package/dist/core/engine/schedules.js +28 -9
- package/dist/core/engine/source-diagnostics.d.ts +76 -0
- package/dist/core/engine/source-diagnostics.js +105 -0
- package/dist/core/engine/source-registration.js +4 -2
- package/dist/core/engine/source-resolution.d.ts +25 -9
- package/dist/core/engine/source-resolution.js +49 -14
- package/dist/core/engine/source-runtime-state.d.ts +92 -0
- package/dist/core/engine/source-runtime-state.js +36 -0
- package/dist/core/engine/storage-io.d.ts +19 -2
- package/dist/core/engine/storage-io.js +21 -2
- package/dist/core/engine/termination/complete.js +1 -1
- package/dist/core/engine/termination/finalizer-registration.d.ts +18 -0
- package/dist/core/engine/termination/finalizer-registration.js +21 -0
- package/dist/core/engine/termination/finalizer.d.ts +16 -19
- package/dist/core/engine/termination/finalizer.js +3 -3
- package/dist/core/engine/termination/suspend.js +3 -0
- package/dist/core/engine/time-operation-callbacks.d.ts +32 -0
- package/dist/core/engine/time-operation-callbacks.js +0 -0
- package/dist/core/engine/validation/schedule-cadence.d.ts +16 -0
- package/dist/core/engine/validation/schedule-cadence.js +20 -0
- package/dist/core/engine/validation/schedule-options.d.ts +4 -1
- package/dist/core/engine/validation/schedule-options.js +14 -0
- package/dist/core/engine/validation/schedule-revision.d.ts +24 -0
- package/dist/core/engine/validation/schedule-revision.js +21 -0
- package/dist/core/engine/validation/schedule-warnings.d.ts +11 -0
- package/dist/core/engine/validation/schedule-warnings.js +5 -0
- package/dist/core/engine/validation/schedule.d.ts +29 -4
- package/dist/core/engine/validation/schedule.js +19 -36
- package/dist/core/engine/validation.js +13 -17
- package/dist/core/engine/workflow-claim-reclaim-target.d.ts +1 -1
- package/dist/core/engine/workflow-claim-reclaim-target.js +22 -8
- package/dist/core/engine/workflow-claim-registry.d.ts +79 -75
- package/dist/core/engine/workflow-claim-registry.js +15 -2
- package/dist/core/engine/workflow-generation-fence.d.ts +74 -0
- package/dist/core/engine/workflow-generation-fence.js +19 -0
- package/dist/core/engine/workflow-retention-deadline.d.ts +16 -0
- package/dist/core/engine/workflow-retention-deadline.js +33 -0
- package/dist/core/events/event-map.d.ts +5 -0
- package/dist/core/events/index.d.ts +1 -0
- package/dist/core/events/index.js +1 -0
- package/dist/core/events/workflow-source-events.d.ts +102 -0
- package/dist/core/events/workflow-source-events.js +57 -0
- package/dist/core/execution-strategy.d.ts +7 -0
- package/dist/core/inline-execution-strategy.context-options.d.ts +1 -1
- package/dist/core/inline-execution-strategy.js +1 -1
- package/dist/core/start-workflow-validation.d.ts +15 -0
- package/dist/core/start-workflow-validation.js +11 -1
- package/dist/core/types/checkpoint.d.ts +14 -0
- package/dist/core/types/schedules.d.ts +52 -1
- package/dist/core/types/state.d.ts +16 -0
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +4 -1
- package/dist/core/worker-execution-strategy.d.ts +1 -0
- package/dist/core/worker-execution-strategy.js +13 -1
- package/dist/core/worker-fault-handling.d.ts +10 -0
- package/dist/core/worker-fault-handling.js +1 -0
- package/dist/core/worker-inbound-message.d.ts +1 -0
- package/dist/core/worker-inbound-message.js +1 -0
- package/dist/core/worker-protocol-guard.js +4 -0
- package/dist/core/worker-protocol.d.ts +7 -0
- package/dist/core/worker-protocol.js +2 -1
- package/dist/core/worker-turn-watchdog.d.ts +8 -1
- package/dist/core/worker-turn-watchdog.js +3 -2
- package/dist/core/workflow-identifiers.d.ts +37 -0
- package/dist/core/workflow-identifiers.js +19 -1
- package/dist/diagnostics/format.js +5 -0
- package/dist/diagnostics/types.d.ts +24 -0
- package/dist/diagnostics/version-check.js +8 -2
- package/dist/http.js +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +7 -0
- package/dist/indexeddb.js +1 -1
- package/dist/json-schema.js +3 -3
- package/dist/server/fault-to-json-rpc.js +2 -1
- package/dist/server/fleet-event-feed.js +6 -6
- package/dist/server/index.d.ts +11 -0
- package/dist/server/operation-fault.d.ts +2 -0
- package/dist/server/operation-fault.js +2 -1
- package/dist/server/operations/create-schedule.d.ts +2 -0
- package/dist/server/operations/create-schedule.js +10 -6
- package/dist/server/operations/get-catalog-diagnostics.d.ts +28 -0
- package/dist/server/operations/get-catalog-diagnostics.js +10 -2
- package/dist/server/operations/get-task-detail-schema.d.ts +8 -0
- package/dist/server/operations/get-task-detail-schema.js +1 -0
- package/dist/server/operations/get-task-detail.d.ts +8 -0
- package/dist/server/operations/get-task-detail.js +1 -0
- package/dist/server/operations/get-task-diagnostics.d.ts +4 -4
- package/dist/server/operations/preload-workflow-revision.d.ts +28 -0
- package/dist/server/operations/preload-workflow-revision.js +60 -0
- package/dist/server/operations/schedule-faults.d.ts +1 -0
- package/dist/server/operations/schedule-faults.js +32 -4
- package/dist/server/operations/schedule-rest-body.d.ts +1 -0
- package/dist/server/operations/schedule-rest-body.js +2 -1
- package/dist/server/operations/static-registrations.js +6 -0
- package/dist/server/operations/update-schedule.d.ts +2 -0
- package/dist/server/operations/update-schedule.js +4 -2
- package/dist/server/operations/workflow-catalog-operation-helpers.js +27 -0
- package/dist/server/rest-binding.d.ts +11 -0
- package/dist/server/runtime/task-dispatch-envelope.d.ts +41 -0
- package/dist/server/runtime/task-dispatch-envelope.js +54 -0
- package/dist/server/runtime/task-dispatch-revision.d.ts +29 -0
- package/dist/server/runtime/task-dispatch-revision.js +13 -0
- package/dist/server/runtime/task-dispatch.d.ts +3 -1
- package/dist/server/runtime/task-dispatch.js +20 -44
- package/dist/server/runtime/task-ledger-recovery.js +1 -1
- package/dist/server/runtime/task-ledger-runtime.d.ts +14 -2
- package/dist/server/runtime/task-ledger-runtime.js +5 -3
- package/dist/server/runtime/task-polling.js +9 -3
- package/dist/server/runtime/task-reconciliation.js +2 -1
- package/dist/server/runtime/websocket-worker.js +9 -0
- package/dist/server/task-ledger-codec.d.ts +28 -0
- package/dist/server/task-ledger-codec.js +7 -1
- package/dist/server/task-ledger-transition-helpers.js +1 -0
- package/dist/server/task-ledger-types.d.ts +9 -0
- package/dist/server/task-ledger.d.ts +1 -1
- package/dist/server/task-ledger.js +3 -1
- package/dist/server/task-queue-types.d.ts +2 -0
- package/dist/storage/bun-sql.js +135 -100
- package/dist/storage/catalog-keys.d.ts +19 -0
- package/dist/storage/catalog-keys.js +3 -1
- package/dist/storage/compressed-storage.js +1 -1
- package/dist/storage/generation-keys.d.ts +52 -0
- package/dist/storage/generation-keys.js +4 -0
- package/dist/storage/index.d.ts +76 -63
- package/dist/storage/interface.d.ts +24 -230
- package/dist/storage/interface.js +1 -1
- package/dist/storage/key-prefixes.d.ts +1 -1
- package/dist/storage/key-prefixes.js +2 -0
- package/dist/storage/lease-keys.d.ts +48 -0
- package/dist/storage/lease-keys.js +8 -0
- package/dist/storage/lmdb.d.ts +30 -2
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon.js +3 -3
- package/dist/storage/node-sqlite.js +135 -100
- package/dist/storage/postgres.js +3 -3
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/signal-keys.d.ts +23 -0
- package/dist/storage/signal-keys.js +9 -0
- package/dist/storage/testing.js +1 -1
- package/dist/storage/turso.js +2 -2
- package/dist/storage/workflow-lifecycle-keys.d.ts +128 -0
- package/dist/storage/workflow-lifecycle-keys.js +46 -0
- package/dist/storage/workflow-record-keys.d.ts +98 -0
- package/dist/storage/workflow-record-keys.js +47 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web-extension.js +1 -1
- package/dist/worker/protocol-messages.d.ts +17 -0
- package/dist/worker/protocol-schemas.d.ts +32 -0
- package/dist/worker/protocol-schemas.js +7 -3
- package/dist/worker/protocol-task-result.js +35 -3
- package/dist/worker/protocol.js +1 -1
- package/dist/worker/registry/types.d.ts +2 -0
- package/dist/worker/registry.d.ts +1 -1
- package/dist/worker/registry.js +3 -1
- package/dist/workers/workflow-runner.d.ts +9 -0
- package/dist/workers/workflow-runner.js +9 -2
- package/dist/workers/workflow-worker-entry.js +9 -4
- package/package.json +2 -2
|
@@ -20,13 +20,14 @@ export function runWorkflowStartInterceptor(_internals, workflowId, workflowType
|
|
|
20
20
|
});
|
|
21
21
|
return capturedHeaders;
|
|
22
22
|
}
|
|
23
|
-
export function startWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, _callbacks) {
|
|
23
|
+
export function startWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, revision, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, _callbacks) {
|
|
24
24
|
if (nestingDepth !== 0)
|
|
25
25
|
internals.workflowNestingDepths.set(workflowId, nestingDepth);
|
|
26
|
-
internals.workflowTypeByWorkflowId.set(workflowId, workflowType);
|
|
26
|
+
internals.workflowTypeByWorkflowId.set(workflowId, { type: workflowType, revision });
|
|
27
27
|
internals.strategy.startWorkflow({
|
|
28
28
|
workflowId,
|
|
29
29
|
...workflowExecutionToken !== void 0 && { workflowExecutionToken },
|
|
30
|
+
...revision !== void 0 && { revision },
|
|
30
31
|
workflowType,
|
|
31
32
|
input,
|
|
32
33
|
checkpoint: serializeCheckpoint(checkpoint),
|
|
@@ -40,7 +41,7 @@ export function startWorkflowExecution(internals, workflowId, workflowExecutionT
|
|
|
40
41
|
}
|
|
41
42
|
});
|
|
42
43
|
}
|
|
43
|
-
export function beginWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, _registration, callbacks, onStarted) {
|
|
44
|
+
export function beginWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, revision, input, checkpoint, executionDeadline, executionStateOwnerId, _registration, callbacks, onStarted) {
|
|
44
45
|
const nestingDepth = internals.pendingNestingDepth ?? 0;
|
|
45
46
|
internals.pendingNestingDepth = void 0;
|
|
46
47
|
if (internals.inlineStrategy !== null) {
|
|
@@ -48,6 +49,7 @@ export function beginWorkflowExecution(internals, workflowId, workflowExecutionT
|
|
|
48
49
|
workflowId,
|
|
49
50
|
...workflowExecutionToken !== void 0 && { workflowExecutionToken },
|
|
50
51
|
workflowType,
|
|
52
|
+
revision,
|
|
51
53
|
input,
|
|
52
54
|
checkpoint,
|
|
53
55
|
nestingDepth,
|
|
@@ -58,7 +60,7 @@ export function beginWorkflowExecution(internals, workflowId, workflowExecutionT
|
|
|
58
60
|
return;
|
|
59
61
|
}
|
|
60
62
|
callbacks.dispatchEvent(new WorkflowStartedEvent(workflowId, workflowType, input));
|
|
61
|
-
startWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, callbacks);
|
|
63
|
+
startWorkflowExecution(internals, workflowId, workflowExecutionToken, workflowType, revision, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, callbacks);
|
|
62
64
|
onStarted?.();
|
|
63
65
|
}
|
|
64
66
|
export function assertDeferSupported(internals, options, isDelayedStart) {
|
|
@@ -73,7 +75,7 @@ export async function beginExecutionAwaitingLiveness(internals, params, workflow
|
|
|
73
75
|
if (params.isDelayed)
|
|
74
76
|
return;
|
|
75
77
|
const liveness = params.options?.defer === !1 ? Promise.withResolvers() : void 0;
|
|
76
|
-
beginWorkflowExecution(internals, workflowId, params.state.workflowExecutionToken, params.type, params.input, params.checkpoint, params.state.executionDeadline, params.state.executionStateOwnerId ?? workflowId, params.registration, callbacks, liveness ? () => liveness.resolve() : void 0);
|
|
78
|
+
beginWorkflowExecution(internals, workflowId, params.state.workflowExecutionToken, params.type, params.state.revision, params.input, params.checkpoint, params.state.executionDeadline, params.state.executionStateOwnerId ?? workflowId, params.registration, callbacks, liveness ? () => liveness.resolve() : void 0);
|
|
77
79
|
if (liveness)
|
|
78
80
|
await liveness.promise;
|
|
79
81
|
}
|
|
@@ -2,9 +2,7 @@ import { KEYS } from "../../../storage/interface.js";
|
|
|
2
2
|
import { encode } from "../../codec.js";
|
|
3
3
|
import { WorkflowAlreadyExistsError } from "../errors.js";
|
|
4
4
|
import { buildCreateBatchSignalOperations } from "../signals.js";
|
|
5
|
-
import {
|
|
6
|
-
StartIdempotencyRaceLostError
|
|
7
|
-
} from "./start-commit.js";
|
|
5
|
+
import { StartIdempotencyRaceLostError } from "./start-commit-errors.js";
|
|
8
6
|
import {
|
|
9
7
|
requireWinnerId,
|
|
10
8
|
resolveCallerIdWinnerOrRetry,
|
|
@@ -41,11 +41,17 @@ export declare function resolveExistingRunOrThrowPurged(internals: EngineInterna
|
|
|
41
41
|
* resolves it instead of racing it to a terminal-conflict). Only when the record is
|
|
42
42
|
* absent do we wait for the reservation to clear and read once more to discriminate:
|
|
43
43
|
*
|
|
44
|
-
* - **record present** — the winner committed: signal it (or conflict if terminal)
|
|
45
|
-
* and return the handle.
|
|
44
|
+
* - **record present** — the winner committed: signal it (or conflict if terminal).
|
|
46
45
|
* - **record absent after the reservation clears** — the winner aborted before
|
|
47
46
|
* committing (storage failure, oversized payload, throwing start interceptor): no
|
|
48
47
|
* run exists, so return `undefined` and let the caller retry its own create.
|
|
48
|
+
*
|
|
49
|
+
* Either terminal-conflict branch is checked against {@link wasSignalAcceptedByWinner}
|
|
50
|
+
* before throwing: a fast workflow can reach a terminal status between this
|
|
51
|
+
* loser's reads, entirely independent of how much slower or faster the winner's
|
|
52
|
+
* OWN commit path happens to be — so a same-`signalId` convergent caller (the
|
|
53
|
+
* documented "concurrent absent-target callers" contract) must not depend on
|
|
54
|
+
* catching the winner mid-flight to avoid a spurious conflict.
|
|
49
55
|
*/
|
|
50
56
|
export declare function resolveCallerIdWinnerOrRetry(internals: EngineInternals, winnerId: string, signalSpec: StartOrSignalSignal, signalId: string, callbacks: StartOrSignalCallbacks, allowTerminalRestart?: boolean): Promise<WorkflowHandle | undefined>;
|
|
51
57
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sleep } from "../../../runtime/portable.js";
|
|
2
|
-
import { KEYS } from "../../../storage/interface.js";
|
|
2
|
+
import { KEYS, storageHas } from "../../../storage/interface.js";
|
|
3
3
|
import { decode } from "../../codec.js";
|
|
4
4
|
import { IdempotencyKeyPurgedError, StartOrSignalConflictError } from "../errors.js";
|
|
5
5
|
import { loadWorkflowState } from "../storage-io.js";
|
|
@@ -23,6 +23,14 @@ async function awaitReservationCleared(internals, workflowId) {
|
|
|
23
23
|
await sleep(RESERVATION_CLEAR_RETRY_DELAY_MS);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
async function wasSignalAcceptedByWinner(internals, workflowId, signalName, signalId) {
|
|
27
|
+
return storageHas(internals.storage, KEYS.signalAcceptedResponse(workflowId, signalName, signalId));
|
|
28
|
+
}
|
|
29
|
+
async function resolveTerminalConflictOrConvergence(internals, winnerId, status, signalSpec, signalId, callbacks) {
|
|
30
|
+
if (await wasSignalAcceptedByWinner(internals, winnerId, signalSpec.name, signalId))
|
|
31
|
+
return callbacks.getHandle(winnerId);
|
|
32
|
+
throw new StartOrSignalConflictError(winnerId, status);
|
|
33
|
+
}
|
|
26
34
|
export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSpec, signalId, callbacks, allowTerminalRestart = !1) {
|
|
27
35
|
const state = await loadWorkflowState(internals, winnerId);
|
|
28
36
|
if (state !== null) {
|
|
@@ -31,7 +39,7 @@ export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSp
|
|
|
31
39
|
return callbacks.getHandle(winnerId);
|
|
32
40
|
}
|
|
33
41
|
if (!allowTerminalRestart)
|
|
34
|
-
|
|
42
|
+
return resolveTerminalConflictOrConvergence(internals, winnerId, state.status, signalSpec, signalId, callbacks);
|
|
35
43
|
}
|
|
36
44
|
await awaitReservationCleared(internals, winnerId);
|
|
37
45
|
const stateAfterReservation = await loadWorkflowState(internals, winnerId);
|
|
@@ -40,7 +48,7 @@ export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSp
|
|
|
40
48
|
if (isTerminalWorkflowStatus(stateAfterReservation.status)) {
|
|
41
49
|
if (state !== null && isSameTerminalRun(state, stateAfterReservation))
|
|
42
50
|
return;
|
|
43
|
-
|
|
51
|
+
return resolveTerminalConflictOrConvergence(internals, winnerId, stateAfterReservation.status, signalSpec, signalId, callbacks);
|
|
44
52
|
}
|
|
45
53
|
await callbacks.signalExistingWorkflow(winnerId, signalSpec.name, signalSpec.payload, signalId);
|
|
46
54
|
return callbacks.getHandle(winnerId);
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
StartWorkflowValidationError
|
|
7
7
|
} from "../../start-workflow-validation.js";
|
|
8
8
|
import { IdempotencyKeyPurgedError, StartOrSignalConflictError } from "../errors.js";
|
|
9
|
-
import { StartIdempotencyRaceLostError } from "./start-commit.js";
|
|
9
|
+
import { StartIdempotencyRaceLostError } from "./start-commit-errors.js";
|
|
10
10
|
import {
|
|
11
11
|
createWithSignalOrFallback,
|
|
12
12
|
idempotentStartOperationsFor
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide what a lost start compare-and-swap MEANS, once every positively-detectable
|
|
3
|
+
* cause has already been ruled out by the caller — an idempotency precondition, a
|
|
4
|
+
* catalog-entry removal, and a lost workflow claim are each re-checked and raise
|
|
5
|
+
* their own error before this runs.
|
|
6
|
+
*
|
|
7
|
+
* Throws when the loss is terminal; returns normally when the caller should retry
|
|
8
|
+
* workflow-concurrency admission.
|
|
9
|
+
*
|
|
10
|
+
* WFT-152: a caller-supplied id that lost its duplicate-id condition means another
|
|
11
|
+
* engine sharing this store committed a create for the same id after this start's
|
|
12
|
+
* duplicate-id read. It raises the SAME {@link WorkflowAlreadyExistsError} the
|
|
13
|
+
* in-engine `pendingStarts` guard raises for the identical collision, so a
|
|
14
|
+
* cross-engine duplicate id is indistinguishable from an in-engine one at the call
|
|
15
|
+
* site.
|
|
16
|
+
*
|
|
17
|
+
* Attribution is by ELIMINATION, never by re-reading the duplicate-id key. A
|
|
18
|
+
* re-read is unsound: the winning run can complete and be purged (or swept by
|
|
19
|
+
* retention) between the failed compare-and-swap and the check, restoring the
|
|
20
|
+
* workflow record to the very value the condition expected, so the conflict reads
|
|
21
|
+
* back as "no conflict". With no workflow-concurrency conditions in the batch,
|
|
22
|
+
* nothing else is left for the lost outcome to mean.
|
|
23
|
+
*
|
|
24
|
+
* When concurrency conditions ARE present, retry only on positive evidence that the
|
|
25
|
+
* retryable one is what missed — hence `hasWorkflowConcurrencyConflict`, invoked
|
|
26
|
+
* lazily so the extra storage read happens only on that path.
|
|
27
|
+
*
|
|
28
|
+
* That evidence is NOT proof the duplicate id was fine, and deliberately is not
|
|
29
|
+
* treated as such. The concurrency precondition is a monotonic atomic-state VERSION
|
|
30
|
+
* key (`buildWorkflowConcurrencyStartOperations` conditions on `snapshot.version`
|
|
31
|
+
* and writes `version + 1`; releasing the slot increments again rather than
|
|
32
|
+
* restoring), so once a same-id winner acquires and releases, that condition stays
|
|
33
|
+
* mismatched forever and reports a conflict regardless of what else missed. What
|
|
34
|
+
* makes retrying safe here is the caller's own earlier check: it re-reads the
|
|
35
|
+
* duplicate-id key positively and raises `WorkflowAlreadyExistsError` before this
|
|
36
|
+
* runs, so reaching this point means the workflow record currently matches the
|
|
37
|
+
* expectation — the id is free right now, and a retry re-conditions on that same
|
|
38
|
+
* still-matching value.
|
|
39
|
+
*
|
|
40
|
+
* The pre-compare-and-swap purge ABA — a winner purged between the read and the
|
|
41
|
+
* commit, making an absent record look never-used to a value-comparing condition
|
|
42
|
+
* alone — no longer reaches this function's elimination logic (WFT-153, closing
|
|
43
|
+
* the residual this comment used to describe as untracked). The caller now checks
|
|
44
|
+
* a SECOND, positive-evidence condition first (`'duplicate-id-generation'` in
|
|
45
|
+
* `buildAndCommitStartBatch`) on a durable per-id generation counter that a purge
|
|
46
|
+
* bumps but never resets, so that case throws `WorkflowAlreadyExistsError` before
|
|
47
|
+
* reaching here. This function still does not claim exclusive attribution for
|
|
48
|
+
* every conceivable cause — only that the two positively-detectable duplicate-id
|
|
49
|
+
* causes are ruled out ahead of it.
|
|
50
|
+
*
|
|
51
|
+
* With no concurrency evidence at all, fail closed: something missed, nothing
|
|
52
|
+
* retryable explains it, and a spurious `WorkflowAlreadyExistsError` is public,
|
|
53
|
+
* non-destructive, and retryable by the caller.
|
|
54
|
+
*/
|
|
55
|
+
export declare function attributeLostStartPreconditionOrRetry(workflowId: string, hasDuplicateIdCondition: boolean, hasWorkflowConcurrency: boolean, hasWorkflowConcurrencyConflict: () => Promise<boolean>): Promise<void>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { WorkflowAlreadyExistsError } from "../errors.js";
|
|
2
|
+
import { StartIdempotencyRaceLostError } from "./start-commit-errors.js";
|
|
3
|
+
export async function attributeLostStartPreconditionOrRetry(workflowId, hasDuplicateIdCondition, hasWorkflowConcurrency, hasWorkflowConcurrencyConflict) {
|
|
4
|
+
if (!hasDuplicateIdCondition) {
|
|
5
|
+
if (!hasWorkflowConcurrency)
|
|
6
|
+
throw new StartIdempotencyRaceLostError;
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (!hasWorkflowConcurrency)
|
|
10
|
+
throw new WorkflowAlreadyExistsError(workflowId);
|
|
11
|
+
if (!await hasWorkflowConcurrencyConflict())
|
|
12
|
+
throw new WorkflowAlreadyExistsError(workflowId);
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the exact revision a fresh `startWorkflow()` call — or a schedule's
|
|
3
|
+
* pin capture (WFT-20, `pinned-schedule-revision.ts`) — is about to commit
|
|
4
|
+
* to. Extracted out of `lifecycle/start.ts` so both that module and the
|
|
5
|
+
* schedule pin-capture path import the SAME logic instead of duplicating the
|
|
6
|
+
* `registeredCatalogRevisions`-fallback + `ensureWorkflowCatalogReady()`
|
|
7
|
+
* recheck rule; `start.ts` imports these back unchanged.
|
|
8
|
+
*
|
|
9
|
+
* @module core/engine/lifecycle/start-revision-resolution
|
|
10
|
+
*/
|
|
11
|
+
import type { EngineInternals } from '../internals.ts';
|
|
12
|
+
/**
|
|
13
|
+
* The exact executable artifact a start is about to run: the resolved
|
|
14
|
+
* dynamic-source candidate revision, or — for an eager registration, which
|
|
15
|
+
* never populates `resolvedRevision` — this process's own
|
|
16
|
+
* `registeredCatalogRevisions` entry for `type` (the revision of the code
|
|
17
|
+
* actually loaded here, NOT `inFlightRevision`, which for an eager type
|
|
18
|
+
* falls back to the catalog's cached ACTIVE pointer and can name a revision
|
|
19
|
+
* this process never loaded under a multi-engine deployment). Synchronous,
|
|
20
|
+
* on purpose: every top-level engine.* method already awaits
|
|
21
|
+
* `ensureWorkflowCatalogReady()` before reaching a call site that needs this,
|
|
22
|
+
* so this map is populated by the time the overwhelmingly common case gets
|
|
23
|
+
* here. `undefined` means "genuinely not cached yet"; the caller falls back
|
|
24
|
+
* to {@link resolveStartRevisionUncached}.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveCachedStartRevision(internals: EngineInternals, type: string, resolvedRevision: string | undefined): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* The rare fallback {@link resolveCachedStartRevision} defers to: a fired
|
|
29
|
+
* schedule occurrence or a delayed-start timer calls `startWorkflow`
|
|
30
|
+
* directly from background scheduler code, with no top-level
|
|
31
|
+
* `ensureWorkflowCatalogReady()` gate already awaited. Re-checks catalog
|
|
32
|
+
* readiness once, then re-reads the cache.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveStartRevisionUncached(internals: EngineInternals, type: string): Promise<string>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ensureWorkflowCatalogReady } from "../catalog-readiness.js";
|
|
2
|
+
export function resolveCachedStartRevision(internals, type, resolvedRevision) {
|
|
3
|
+
return resolvedRevision ?? internals.registeredCatalogRevisions.get(type);
|
|
4
|
+
}
|
|
5
|
+
export async function resolveStartRevisionUncached(internals, type) {
|
|
6
|
+
await ensureWorkflowCatalogReady(internals.engine);
|
|
7
|
+
const afterReadiness = internals.registeredCatalogRevisions.get(type);
|
|
8
|
+
if (afterReadiness !== void 0)
|
|
9
|
+
return afterReadiness;
|
|
10
|
+
throw Error(`Cannot start workflow "${type}": no catalog revision is registered for this eagerly-registered type, even after re-checking catalog readiness. This should be unreachable.`);
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { StartOptions } from '../../types.ts';
|
|
2
|
+
import type { EngineInternals } from '../internals.ts';
|
|
3
|
+
import { type LifecycleCallbacks } from './shared.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a start's effective delayed-start timestamp from
|
|
6
|
+
* `options.startAt`/`options.startAfter`, or `undefined` for an immediate
|
|
7
|
+
* start. Split out of `start.ts` to keep that file under the repository's
|
|
8
|
+
* line-count ceiling; this is a pure resolution step with no dependency on
|
|
9
|
+
* `startWorkflow`'s own reservation/commit state.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveScheduledStartAt(internals: EngineInternals, options: StartOptions | undefined, submissionTime: number, callbacks: LifecycleCallbacks): number | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { normalizeStorageTimestamp } from "../../scheduler.js";
|
|
2
|
+
import {
|
|
3
|
+
assertExclusiveStartWorkflowOptions,
|
|
4
|
+
coerceStartWorkflowTimestamp,
|
|
5
|
+
StartWorkflowValidationError
|
|
6
|
+
} from "../../start-workflow-validation.js";
|
|
7
|
+
import { parseStartOptionDuration } from "./start-state.js";
|
|
8
|
+
export function resolveScheduledStartAt(internals, options, submissionTime, callbacks) {
|
|
9
|
+
assertExclusiveStartWorkflowOptions(options?.startAt, options?.startAfter);
|
|
10
|
+
if (options?.startAt !== void 0)
|
|
11
|
+
return coerceStartWorkflowTimestamp(options.startAt, "options.startAt");
|
|
12
|
+
if (options?.startAfter !== void 0) {
|
|
13
|
+
const startAfterMilliseconds = parseStartOptionDuration(internals, options.startAfter, "options.startAfter", callbacks);
|
|
14
|
+
try {
|
|
15
|
+
return normalizeStorageTimestamp(submissionTime + startAfterMilliseconds, "options.startAfter");
|
|
16
|
+
} catch {
|
|
17
|
+
throw new StartWorkflowValidationError("options.startAfter must resolve to a finite, non-negative start time");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the initial in-memory {@link WorkflowState} and {@link Checkpoint}
|
|
3
|
+
* for a fresh `start()` — split out of `start.ts`, which has no headroom
|
|
4
|
+
* under the repository's 500-line implementation-file ceiling for this
|
|
5
|
+
* logic inline.
|
|
6
|
+
*
|
|
7
|
+
* @module core/engine/lifecycle/start-state
|
|
8
|
+
*/
|
|
9
|
+
import type { Checkpoint, Duration, StartOptions, TimerEntry, WorkflowState } from '../../types.ts';
|
|
10
|
+
import { type WorkflowVersionTuple } from '../../workflow-version-tuple.ts';
|
|
11
|
+
import type { EngineInternals } from '../internals.ts';
|
|
12
|
+
import { type LifecycleCallbacks } from './shared.ts';
|
|
13
|
+
export declare function parseStartOptionDuration(_internals: EngineInternals, duration: Duration, fieldName: 'options.executionTimeout' | 'options.startAfter', _callbacks: LifecycleCallbacks): number;
|
|
14
|
+
export declare function createInitialWorkflowState(internals: EngineInternals, workflowId: string, type: string, input: unknown, versionTuple: WorkflowVersionTuple, revision: string, options: StartOptions | undefined, tags: string[] | undefined, executionStateOwnerId: string | undefined, parentWorkflowId: string | undefined, parentWorkflowExecutionToken: string | undefined, delayedStartTimer: TimerEntry | undefined, callbacks: LifecycleCallbacks): WorkflowState;
|
|
15
|
+
export declare function createInitialCheckpoint(internals: EngineInternals, workflowId: string, workflowVersion: string, options: StartOptions | undefined, _callbacks: LifecycleCallbacks): Checkpoint;
|
|
16
|
+
export declare function applyRestartLineage(state: WorkflowState, displacedState: WorkflowState | null): void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createCheckpoint } from "../../checkpoint.js";
|
|
2
|
+
import { normalizeStorageTimestamp } from "../../scheduler.js";
|
|
3
|
+
import {
|
|
4
|
+
StartWorkflowValidationError,
|
|
5
|
+
parseStartWorkflowDuration
|
|
6
|
+
} from "../../start-workflow-validation.js";
|
|
7
|
+
export function parseStartOptionDuration(_internals, duration, fieldName, _callbacks) {
|
|
8
|
+
return parseStartWorkflowDuration(duration, fieldName);
|
|
9
|
+
}
|
|
10
|
+
function buildInitialIdentitySlice(workflowId, type, input, versionTuple, revision, executionStateOwnerId, parentWorkflowId, parentWorkflowExecutionToken, delayedStartTimer, now, tags) {
|
|
11
|
+
return {
|
|
12
|
+
id: workflowId,
|
|
13
|
+
type,
|
|
14
|
+
status: delayedStartTimer ? "pending" : "running",
|
|
15
|
+
input,
|
|
16
|
+
versionTuple,
|
|
17
|
+
revision,
|
|
18
|
+
workflowExecutionToken: crypto.randomUUID(),
|
|
19
|
+
...executionStateOwnerId !== void 0 && { executionStateOwnerId },
|
|
20
|
+
...parentWorkflowId !== void 0 && { parentWorkflowId },
|
|
21
|
+
...parentWorkflowExecutionToken !== void 0 && { parentWorkflowExecutionToken },
|
|
22
|
+
createdAt: now,
|
|
23
|
+
...!delayedStartTimer && { startedAt: now },
|
|
24
|
+
updatedAt: now,
|
|
25
|
+
...tags !== void 0 && { tags }
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function resolveInitialExecutionDeadline(internals, options, delayedStartTimer, now, callbacks) {
|
|
29
|
+
if (options?.executionTimeout === void 0 || delayedStartTimer)
|
|
30
|
+
return;
|
|
31
|
+
const executionTimeoutMilliseconds = parseStartOptionDuration(internals, options.executionTimeout, "options.executionTimeout", callbacks);
|
|
32
|
+
try {
|
|
33
|
+
return normalizeStorageTimestamp(now + executionTimeoutMilliseconds, "options.executionTimeout");
|
|
34
|
+
} catch {
|
|
35
|
+
throw new StartWorkflowValidationError("options.executionTimeout must resolve to a finite, non-negative deadline");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function createInitialWorkflowState(internals, workflowId, type, input, versionTuple, revision, options, tags, executionStateOwnerId, parentWorkflowId, parentWorkflowExecutionToken, delayedStartTimer, callbacks) {
|
|
39
|
+
const now = internals.options.getNow(), state = buildInitialIdentitySlice(workflowId, type, input, versionTuple, revision, executionStateOwnerId, parentWorkflowId, parentWorkflowExecutionToken, delayedStartTimer, now, tags), executionDeadline = resolveInitialExecutionDeadline(internals, options, delayedStartTimer, now, callbacks);
|
|
40
|
+
if (executionDeadline !== void 0)
|
|
41
|
+
state.executionDeadline = executionDeadline;
|
|
42
|
+
return state;
|
|
43
|
+
}
|
|
44
|
+
export function createInitialCheckpoint(internals, workflowId, workflowVersion, options, _callbacks) {
|
|
45
|
+
const checkpoint = createCheckpoint(workflowId, workflowVersion, internals.options.getNow());
|
|
46
|
+
if (options?.searchAttributes)
|
|
47
|
+
checkpoint.searchAttributes = { ...options.searchAttributes };
|
|
48
|
+
return checkpoint;
|
|
49
|
+
}
|
|
50
|
+
export function applyRestartLineage(state, displacedState) {
|
|
51
|
+
if (displacedState === null)
|
|
52
|
+
return;
|
|
53
|
+
state.restartedFrom = {
|
|
54
|
+
workflowId: displacedState.id,
|
|
55
|
+
...displacedState.workflowExecutionToken !== void 0 && {
|
|
56
|
+
workflowExecutionToken: displacedState.workflowExecutionToken
|
|
57
|
+
},
|
|
58
|
+
replacedAt: state.createdAt
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -1,7 +1,91 @@
|
|
|
1
|
-
import type { BatchOperation } from '../../../storage/interface.ts';
|
|
1
|
+
import type { BatchOperation, ConditionalBatchCondition } from '../../../storage/interface.ts';
|
|
2
2
|
import type { StartWorkflowOptions, WorkflowState } from '../../types.ts';
|
|
3
3
|
import type { EngineInternals } from '../internals.ts';
|
|
4
4
|
import { type LifecycleCallbacks } from './shared.ts';
|
|
5
|
+
/**
|
|
6
|
+
* What the duplicate-id read decided about a caller-supplied workflow id.
|
|
7
|
+
*/
|
|
8
|
+
export type StartDuplicateIdDecision = {
|
|
9
|
+
/** A prior terminal run this start will displace, or `null` for a fresh id. */
|
|
10
|
+
terminalRunToPurge: WorkflowState | null;
|
|
11
|
+
/**
|
|
12
|
+
* Compare-and-swap precondition that makes the duplicate-id check atomic with
|
|
13
|
+
* the create commit (WFT-152).
|
|
14
|
+
*
|
|
15
|
+
* The check below reads `KEYS.workflow(workflowId)` and decides whether the id
|
|
16
|
+
* is free, but that read and the create batch are separated by every build step
|
|
17
|
+
* in between. Within ONE engine `pendingStarts` holds the id across that window;
|
|
18
|
+
* two engines sharing a store share no such memory, so both previously committed
|
|
19
|
+
* blind and the second silently overwrote the first — leaving the loser with a
|
|
20
|
+
* run whose terminal transition happens on the other engine and therefore never
|
|
21
|
+
* settles its `result()` waiter. Conditioning the batch on the exact value seen
|
|
22
|
+
* here collapses that window: the loser's batch does not commit, and
|
|
23
|
+
* `buildAndCommitStartBatch` surfaces {@link WorkflowAlreadyExistsError} — the
|
|
24
|
+
* same error the in-engine `pendingStarts` guard already throws for the same
|
|
25
|
+
* collision.
|
|
26
|
+
*
|
|
27
|
+
* `expectedValue` is the RAW observed bytes, deliberately not a re-encoding of
|
|
28
|
+
* the decoded state: re-encoding is not guaranteed to round-trip byte-identically,
|
|
29
|
+
* and a condition built from one would fail against a record nothing had touched.
|
|
30
|
+
* `null` (id absent) and a prior terminal run's bytes (an
|
|
31
|
+
* `onTerminalConflict: 'start-new'` restart) are both valid expected values, so a
|
|
32
|
+
* restart is equally protected against a concurrent engine displacing the same
|
|
33
|
+
* terminal run.
|
|
34
|
+
*
|
|
35
|
+
* No `conditionalBatch` capability gate is needed. Reaching this code means a
|
|
36
|
+
* workflow is registered, and registration drains through
|
|
37
|
+
* `WorkflowCatalog#activateRegistered`, which already hard-requires that
|
|
38
|
+
* capability at `Engine.create()`. A store that cannot honour this condition
|
|
39
|
+
* cannot host an engine that could start a workflow in the first place.
|
|
40
|
+
*
|
|
41
|
+
* RESOLVED (PR #959 review; closed by WFT-153). This condition alone compares
|
|
42
|
+
* only a VALUE, so on its own it cannot distinguish "this id was never used"
|
|
43
|
+
* from "a run existed here and was purged" — a racing winner that commits,
|
|
44
|
+
* completes, and is purged (or swept by retention) before this batch commits
|
|
45
|
+
* would make `wf:<id>` absent again, matching `expectedValue: null`. That gap
|
|
46
|
+
* is now closed by {@link duplicateIdGenerationCondition} below, an ADDITIONAL
|
|
47
|
+
* condition on a durable per-id generation counter a purge bumps but never
|
|
48
|
+
* resets — see its own doc for the mechanism.
|
|
49
|
+
*/
|
|
50
|
+
duplicateIdCondition: ConditionalBatchCondition;
|
|
51
|
+
/**
|
|
52
|
+
* ADDITIONAL compare-and-swap precondition on the observed `wf-gen:<id>`
|
|
53
|
+
* bytes, closing the residual ABA hole {@link duplicateIdCondition} cannot
|
|
54
|
+
* detect (WFT-153, following WFT-152). `duplicateIdCondition` compares a
|
|
55
|
+
* VALUE, so it cannot tell "this id was never used" from "a run existed
|
|
56
|
+
* here and was purged" — if a racing winner completes and is purged before
|
|
57
|
+
* this batch commits, `wf:<id>` looks absent again and `duplicateIdCondition`
|
|
58
|
+
* alone would match. `wf-gen:<id>` is bumped in the SAME atomic batch that
|
|
59
|
+
* deletes `wf:<id>` on purge (`workflow-generation-fence.ts`) and is never
|
|
60
|
+
* otherwise touched, so a purge landing in the read-to-commit gap changes
|
|
61
|
+
* this key even though `wf:<id>` reads the same — the stale loser's
|
|
62
|
+
* condition on the pre-purge generation bytes fails where the value-only
|
|
63
|
+
* comparison could not detect it. See `storage/generation-keys.ts`.
|
|
64
|
+
*/
|
|
65
|
+
duplicateIdGenerationCondition: ConditionalBatchCondition;
|
|
66
|
+
/**
|
|
67
|
+
* The exact `wf-gen:<id>` bytes {@link duplicateIdGenerationCondition} was
|
|
68
|
+
* built from, threaded through to {@link prepareTerminalRunPurge} so a
|
|
69
|
+
* `'start-new'` restart's own displacing purge bumps the generation from
|
|
70
|
+
* this SAME observed value rather than a second, independent read. This is
|
|
71
|
+
* what makes the restart's own CAS trivially self-consistent — it can never
|
|
72
|
+
* fence itself out on its own legitimate restart, because the value it
|
|
73
|
+
* bumps from is exactly the value its own precondition checks.
|
|
74
|
+
*/
|
|
75
|
+
observedGenerationBytes: Uint8Array | null;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The decision for a start whose id was GENERATED rather than caller-supplied. A
|
|
79
|
+
* v4 UUID is effectively unique, so the duplicate-id read is skipped entirely and
|
|
80
|
+
* there is no observed value to condition on — the start keeps the unconditioned
|
|
81
|
+
* single-write hot path.
|
|
82
|
+
*/
|
|
83
|
+
export declare const GENERATED_ID_START_DECISION: {
|
|
84
|
+
readonly terminalRunToPurge: null;
|
|
85
|
+
readonly duplicateIdCondition: undefined;
|
|
86
|
+
readonly duplicateIdGenerationCondition: undefined;
|
|
87
|
+
readonly observedGenerationBytes: null;
|
|
88
|
+
};
|
|
5
89
|
/**
|
|
6
90
|
* Decide what a caller-supplied workflow id that already has a persisted record
|
|
7
91
|
* means, WITHOUT performing any destructive action. Only invoked when the caller
|
|
@@ -23,9 +107,58 @@ import { type LifecycleCallbacks } from './shared.ts';
|
|
|
23
107
|
* delete the finalizer payload before the resource is torn down, leaking it, so
|
|
24
108
|
* the restart is refused until teardown settles (which clears the marker).
|
|
25
109
|
*
|
|
26
|
-
*
|
|
110
|
+
* This read is only a point-in-time observation: another engine sharing the store
|
|
111
|
+
* can commit a create for the same id in the window between it and the create
|
|
112
|
+
* batch. The returned {@link StartDuplicateIdDecision} therefore carries a
|
|
113
|
+
* `duplicateIdCondition` holding the exact bytes seen here, so that batch can be
|
|
114
|
+
* conditioned on them — turning that window into a lost compare-and-swap rather
|
|
115
|
+
* than a blind overwrite (WFT-152). It also reads `wf-gen:<id>` in the SAME pass
|
|
116
|
+
* and carries `duplicateIdGenerationCondition`/`observedGenerationBytes` (WFT-153),
|
|
117
|
+
* closing the residual ABA window a value-only condition cannot detect — see that
|
|
118
|
+
* field's own doc.
|
|
27
119
|
*/
|
|
28
|
-
export declare function resolveTerminalConflictForRestart(internals: EngineInternals, workflowId: string, options: StartWorkflowOptions | undefined): Promise<
|
|
120
|
+
export declare function resolveTerminalConflictForRestart(internals: EngineInternals, workflowId: string, options: StartWorkflowOptions | undefined): Promise<StartDuplicateIdDecision>;
|
|
121
|
+
/**
|
|
122
|
+
* The `'reattach-only'`/`'bulk-retry-only'` fence for `startWorkflow`'s
|
|
123
|
+
* `skipAdmissionIdCheck` parameter (WFT-95 TOCTOU fix). Call immediately
|
|
124
|
+
* after {@link resolveTerminalConflictForRestart} resolves.
|
|
125
|
+
*
|
|
126
|
+
* Both call sites reach `startWorkflow` after a separate, non-atomic read
|
|
127
|
+
* confirmed an already-persisted record they mean to replay or replace — but
|
|
128
|
+
* that confirmation and `resolveTerminalConflictForRestart`'s own atomic read
|
|
129
|
+
* are not the same read:
|
|
130
|
+
*
|
|
131
|
+
* - `dispatchChildWorkflowStart()`'s crash-reattach retry confirms a matching
|
|
132
|
+
* persisted child via `loadWorkflowState()`, then leaves
|
|
133
|
+
* `options.onTerminalConflict` unset — so `resolveTerminalConflictForRestart`
|
|
134
|
+
* either finds the record still there (throws {@link WorkflowAlreadyExistsError},
|
|
135
|
+
* this function is never reached: the expected, unraced reattach) or gone
|
|
136
|
+
* (`terminalRunToPurge: null`, the ordinary fresh-create branch).
|
|
137
|
+
* - `retryFailedWorkflow()`'s checkpoint-absent fallback (`bulk-operations-retry.ts`)
|
|
138
|
+
* confirms a `failed` record via its own `loadWorkflowState()`, then always
|
|
139
|
+
* sets `options.onTerminalConflict: 'start-new'` — so
|
|
140
|
+
* `resolveTerminalConflictForRestart` either finds the SAME terminal record
|
|
141
|
+
* still there (returns its bytes as `terminalRunToPurge`, the expected,
|
|
142
|
+
* unraced purge-and-replace) or finds it gone (`terminalRunToPurge: null`,
|
|
143
|
+
* the same ordinary fresh-create branch).
|
|
144
|
+
*
|
|
145
|
+
* Under `ownership: 'workflow-lease'`, another engine can purge the matched
|
|
146
|
+
* record in the window between either call site's own confirmation read and
|
|
147
|
+
* `resolveTerminalConflictForRestart`'s. Being called with a `'reattach-only'`
|
|
148
|
+
* or `'bulk-retry-only'` `skipAdmissionIdCheck` and a `null` `terminalRunToPurge`
|
|
149
|
+
* therefore means the race happened: there is nothing left to reattach to or
|
|
150
|
+
* replace, so this re-runs strict admission rather than let a bypassed
|
|
151
|
+
* `.`/`..` id fall through into a genuinely fresh create. `coerceStartWorkflowId`
|
|
152
|
+
* throws the same `StartWorkflowValidationError` strict admission would have
|
|
153
|
+
* thrown on the caller's very first (non-retry) attempt — a clean,
|
|
154
|
+
* deterministic rejection instead of a silently created reserved-id run.
|
|
155
|
+
*
|
|
156
|
+
* A no-op for every other `skipAdmissionIdCheck` value: `true` (schedule
|
|
157
|
+
* drain) applies unconditionally and never calls this, and `undefined`
|
|
158
|
+
* (every public start surface) already went through strict admission in
|
|
159
|
+
* `prepareStartWorkflow`.
|
|
160
|
+
*/
|
|
161
|
+
export declare function enforceReplayOnlyIdFence(skipAdmissionIdCheck: boolean | 'reattach-only' | 'bulk-retry-only' | undefined, workflowId: string, terminalRunToPurge: WorkflowState | null): void;
|
|
29
162
|
/**
|
|
30
163
|
* Prepare a prior terminal run for displacement by a `'start-new'` restart WITHOUT
|
|
31
164
|
* committing the destructive delete. Returns the storage delete operations (for the
|
|
@@ -35,5 +168,14 @@ export declare function resolveTerminalConflictForRestart(internals: EngineInter
|
|
|
35
168
|
* entries. `clearPurgedWorkflowInMemoryState` runs `cleanupWaiters` to settle the
|
|
36
169
|
* old run's pending signal/update/sleep waiters; it only needs
|
|
37
170
|
* `swallowPromiseRejection`, which `LifecycleCallbacks` already exposes.
|
|
171
|
+
*
|
|
172
|
+
* Also appends the `wf-gen:<id>` bump PUT operation (WFT-153), built from
|
|
173
|
+
* `observedGenerationBytes` — the SAME bytes `resolveTerminalConflictForRestart`
|
|
174
|
+
* already read for `duplicateIdGenerationCondition` — rather than a second,
|
|
175
|
+
* independent read. Reusing that one observed value for both the outer CAS
|
|
176
|
+
* condition and the bump amount is what makes this restart's own commit
|
|
177
|
+
* trivially self-consistent: it can never fence itself out on its own
|
|
178
|
+
* legitimate restart, because the value it bumps from is exactly the value its
|
|
179
|
+
* own precondition checks (see `duplicateIdGenerationCondition`'s doc).
|
|
38
180
|
*/
|
|
39
|
-
export declare function prepareTerminalRunPurge(internals: EngineInternals, state: WorkflowState, callbacks: LifecycleCallbacks): Promise<BatchOperation[]>;
|
|
181
|
+
export declare function prepareTerminalRunPurge(internals: EngineInternals, state: WorkflowState, callbacks: LifecycleCallbacks, observedGenerationBytes: Uint8Array | null): Promise<BatchOperation[]>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { KEYS, storageHas } from "../../../storage/interface.js";
|
|
2
|
+
import { coerceStartWorkflowId } from "../../start-workflow-validation.js";
|
|
2
3
|
import {
|
|
3
4
|
clearPurgedWorkflowInMemoryState,
|
|
4
5
|
collectWorkflowPurgeDeleteOperations
|
|
@@ -6,10 +7,28 @@ import {
|
|
|
6
7
|
import { WorkflowAlreadyExistsError, WorkflowTeardownPendingError } from "../errors.js";
|
|
7
8
|
import { cleanupWaiters } from "../termination/cleanup.js";
|
|
8
9
|
import { decodeWorkflowState, isTerminalWorkflowStatus } from "../validation.js";
|
|
10
|
+
import { buildWorkflowGenerationBumpOperation } from "../workflow-generation-fence.js";
|
|
11
|
+
export const GENERATED_ID_START_DECISION = {
|
|
12
|
+
terminalRunToPurge: null,
|
|
13
|
+
duplicateIdCondition: void 0,
|
|
14
|
+
duplicateIdGenerationCondition: void 0,
|
|
15
|
+
observedGenerationBytes: null
|
|
16
|
+
};
|
|
9
17
|
export async function resolveTerminalConflictForRestart(internals, workflowId, options) {
|
|
10
|
-
const
|
|
18
|
+
const key = KEYS.workflow(workflowId), generationKey = KEYS.workflowGeneration(workflowId), [existingBytes, observedGenerationBytes] = await Promise.all([
|
|
19
|
+
internals.storage.get(key),
|
|
20
|
+
internals.storage.get(generationKey)
|
|
21
|
+
]), duplicateIdGenerationCondition = {
|
|
22
|
+
key: generationKey,
|
|
23
|
+
expectedValue: observedGenerationBytes
|
|
24
|
+
};
|
|
11
25
|
if (existingBytes === null)
|
|
12
|
-
return
|
|
26
|
+
return {
|
|
27
|
+
terminalRunToPurge: null,
|
|
28
|
+
duplicateIdCondition: { key, expectedValue: null },
|
|
29
|
+
duplicateIdGenerationCondition,
|
|
30
|
+
observedGenerationBytes
|
|
31
|
+
};
|
|
13
32
|
if (options?.onTerminalConflict !== "start-new")
|
|
14
33
|
throw new WorkflowAlreadyExistsError(workflowId);
|
|
15
34
|
const existingState = decodeWorkflowState(existingBytes);
|
|
@@ -17,12 +36,22 @@ export async function resolveTerminalConflictForRestart(internals, workflowId, o
|
|
|
17
36
|
throw new WorkflowAlreadyExistsError(workflowId);
|
|
18
37
|
if (await storageHas(internals.storage, KEYS.teardownOwed(workflowId)))
|
|
19
38
|
throw new WorkflowTeardownPendingError(workflowId);
|
|
20
|
-
return
|
|
39
|
+
return {
|
|
40
|
+
terminalRunToPurge: existingState,
|
|
41
|
+
duplicateIdCondition: { key, expectedValue: existingBytes },
|
|
42
|
+
duplicateIdGenerationCondition,
|
|
43
|
+
observedGenerationBytes
|
|
44
|
+
};
|
|
21
45
|
}
|
|
22
|
-
export
|
|
46
|
+
export function enforceReplayOnlyIdFence(skipAdmissionIdCheck, workflowId, terminalRunToPurge) {
|
|
47
|
+
if ((skipAdmissionIdCheck === "reattach-only" || skipAdmissionIdCheck === "bulk-retry-only") && terminalRunToPurge === null)
|
|
48
|
+
coerceStartWorkflowId(workflowId, "options.id");
|
|
49
|
+
}
|
|
50
|
+
export async function prepareTerminalRunPurge(internals, state, callbacks, observedGenerationBytes) {
|
|
23
51
|
const cleanupWaitersForStart = (id) => cleanupWaiters(internals, id, {
|
|
24
52
|
swallowPromiseRejection: callbacks.swallowPromiseRejection
|
|
25
53
|
}), deleteOperations = await collectWorkflowPurgeDeleteOperations(internals, state);
|
|
54
|
+
deleteOperations.push(buildWorkflowGenerationBumpOperation(state.id, observedGenerationBytes));
|
|
26
55
|
clearPurgedWorkflowInMemoryState(internals, state.id, cleanupWaitersForStart);
|
|
27
56
|
return deleteOperations;
|
|
28
57
|
}
|