@lostgradient/weft 0.24.0 → 0.24.1
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.js +14 -20
- package/dist/core/engine/bulk-operations-retry.d.ts +9 -0
- package/dist/core/engine/bulk-operations-retry.js +177 -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.js +8 -167
- package/dist/core/engine/callback-creators-bundles.js +4 -2
- 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/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/index.d.ts +18 -10
- package/dist/core/engine/index.js +21 -10
- 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-revision-groups.d.ts +75 -0
- package/dist/core/engine/lifecycle/recovery-revision-groups.js +64 -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.js +18 -9
- package/dist/core/engine/lifecycle/shared.d.ts +43 -0
- package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +12 -0
- package/dist/core/engine/lifecycle/standalone-claim-acquire.js +6 -0
- 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 +22 -7
- package/dist/core/engine/lifecycle/start-commit.js +49 -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.js +1 -1
- package/dist/core/engine/lifecycle/start-precondition-attribution.d.ts +49 -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-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 +66 -3
- package/dist/core/engine/lifecycle/start-terminal-conflict-purge.js +13 -3
- package/dist/core/engine/lifecycle/start.d.ts +16 -6
- package/dist/core/engine/lifecycle/start.js +38 -68
- package/dist/core/engine/lifecycle/transition.d.ts +3 -4
- package/dist/core/engine/lifecycle/transition.js +30 -80
- package/dist/core/engine/lifecycle.d.ts +4 -2
- package/dist/core/engine/lifecycle.js +6 -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-revision-fire.d.ts +17 -0
- package/dist/core/engine/schedule-revision-fire.js +3 -0
- package/dist/core/engine/schedule-run.js +2 -1
- package/dist/core/engine/schedules.d.ts +2 -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/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/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 +4 -3
- package/dist/core/engine/validation/schedule.js +15 -26
- package/dist/core/engine/validation.js +3 -0
- package/dist/core/engine/workflow-claim-reclaim-target.d.ts +1 -1
- package/dist/core/engine/workflow-claim-reclaim-target.js +16 -2
- 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/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/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 +1 -1
- 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/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/runtime/task-dispatch-envelope.d.ts +14 -0
- package/dist/server/runtime/task-dispatch-envelope.js +32 -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.js +12 -37
- 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 +9 -0
- package/dist/server/task-ledger-codec.js +4 -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 +2 -1
- package/dist/server/task-queue-types.d.ts +2 -0
- package/dist/storage/bun-sql.js +3 -1
- 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/index.d.ts +3 -1
- package/dist/storage/interface.d.ts +2 -0
- package/dist/storage/interface.js +1 -1
- package/dist/storage/key-prefixes.d.ts +1 -1
- package/dist/storage/key-prefixes.js +1 -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 +2 -2
- package/dist/storage/node-sqlite.js +3 -1
- 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 +1 -1
- 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
|
@@ -60,8 +60,8 @@ declare const taskDiagnosticItemSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
60
60
|
}>;
|
|
61
61
|
state: z.ZodEnum<{
|
|
62
62
|
"dead-lettered": "dead-lettered";
|
|
63
|
-
queued: "queued";
|
|
64
63
|
resolved: "resolved";
|
|
64
|
+
queued: "queued";
|
|
65
65
|
inflight: "inflight";
|
|
66
66
|
capacity: "capacity";
|
|
67
67
|
}>;
|
|
@@ -122,8 +122,8 @@ declare const getTaskDiagnosticsOutput: z.ZodObject<{
|
|
|
122
122
|
}>;
|
|
123
123
|
state: z.ZodEnum<{
|
|
124
124
|
"dead-lettered": "dead-lettered";
|
|
125
|
-
queued: "queued";
|
|
126
125
|
resolved: "resolved";
|
|
126
|
+
queued: "queued";
|
|
127
127
|
inflight: "inflight";
|
|
128
128
|
capacity: "capacity";
|
|
129
129
|
}>;
|
|
@@ -217,7 +217,7 @@ export declare function createGetTaskDiagnosticsOperation(options?: GetTaskDiagn
|
|
|
217
217
|
}, {
|
|
218
218
|
items: ({
|
|
219
219
|
kind: "dead-lettered" | "stuck-queued" | "stale-inflight" | "retry-storm" | "all-workers-at-capacity";
|
|
220
|
-
state: "dead-lettered" | "
|
|
220
|
+
state: "dead-lettered" | "resolved" | "queued" | "inflight" | "capacity";
|
|
221
221
|
retryCount: number;
|
|
222
222
|
requeueCount: number;
|
|
223
223
|
evidence: string[];
|
|
@@ -279,7 +279,7 @@ export declare const getTaskDiagnosticsOperation: import("../operation-catalog.t
|
|
|
279
279
|
}, {
|
|
280
280
|
items: ({
|
|
281
281
|
kind: "dead-lettered" | "stuck-queued" | "stale-inflight" | "retry-storm" | "all-workers-at-capacity";
|
|
282
|
-
state: "dead-lettered" | "
|
|
282
|
+
state: "dead-lettered" | "resolved" | "queued" | "inflight" | "capacity";
|
|
283
283
|
retryCount: number;
|
|
284
284
|
requeueCount: number;
|
|
285
285
|
evidence: string[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `weft.workflows.revisions.preload` operation + REST binding.
|
|
3
|
+
*
|
|
4
|
+
* Loads, validates, and installs one dynamic workflow source revision
|
|
5
|
+
* previously recorded via `engine.registerSource()` — a thin, documented
|
|
6
|
+
* exposure of `engine.workflows.preload()` (WFT-15/16). Modeled on
|
|
7
|
+
* `install-workflow-revision.ts` / `activate-workflow-revision.ts`.
|
|
8
|
+
*
|
|
9
|
+
* @module server/operations/preload-workflow-revision
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import type { WorkflowRevisionRecord } from '../../core/catalog/index.ts';
|
|
13
|
+
import type { UnknownRestBinding } from '../rest-bindings.ts';
|
|
14
|
+
declare const preloadWorkflowRevisionInput: z.ZodObject<{
|
|
15
|
+
name: z.ZodUnknown;
|
|
16
|
+
revision: z.ZodUnknown;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
export type PreloadWorkflowRevisionInput = z.infer<typeof preloadWorkflowRevisionInput>;
|
|
19
|
+
export type PreloadWorkflowRevisionOutput = WorkflowRevisionRecord;
|
|
20
|
+
export declare const preloadWorkflowRevisionOperation: import("../operation-catalog.ts").OperationDefinition<{
|
|
21
|
+
name: unknown;
|
|
22
|
+
revision: unknown;
|
|
23
|
+
}, Readonly<{
|
|
24
|
+
manifest: import("../../index.ts").WorkflowRevisionManifest;
|
|
25
|
+
installedAt: number;
|
|
26
|
+
}>, unknown>;
|
|
27
|
+
export declare const preloadWorkflowRevisionRestBinding: UnknownRestBinding;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isWeftError } from "../../core/weft-error.js";
|
|
3
|
+
import { defineOperation } from "../operation-registry.js";
|
|
4
|
+
import {
|
|
5
|
+
readWorkflowCatalogRestBody,
|
|
6
|
+
throwWorkflowCatalogOperationFault,
|
|
7
|
+
validateWorkflowNameField,
|
|
8
|
+
validateWorkflowRevisionField,
|
|
9
|
+
workflowsAdminAccess
|
|
10
|
+
} from "./workflow-catalog-operation-helpers.js";
|
|
11
|
+
const preloadWorkflowRevisionInput = z.object({
|
|
12
|
+
name: z.unknown().describe("Workflow name. Runtime validation requires a wire-safe identifier."),
|
|
13
|
+
revision: z.unknown().describe("The registerSource()-registered revision to load, validate, and install. Runtime validation requires a non-empty string.")
|
|
14
|
+
}), preloadWorkflowRevisionOutput = z.unknown();
|
|
15
|
+
export const preloadWorkflowRevisionOperation = defineOperation({
|
|
16
|
+
name: "weft.workflows.revisions.preload",
|
|
17
|
+
mcpExposable: !1,
|
|
18
|
+
summary: "Load, validate, and install a registered dynamic workflow source revision",
|
|
19
|
+
description: "Load, validate, and install `(name, revision)` \u2014 previously recorded via " + "engine.registerSource() \u2014 durably into the workflow catalog. Faults NotFound when no " + "source was ever registered for this exact key, and Conflict when the load fails or the loaded module fails validation.",
|
|
20
|
+
destructive: !1,
|
|
21
|
+
tags: ["Workflow Catalog"],
|
|
22
|
+
inputSchema: preloadWorkflowRevisionInput,
|
|
23
|
+
outputSchema: preloadWorkflowRevisionOutput,
|
|
24
|
+
access: workflowsAdminAccess,
|
|
25
|
+
producibleFaults: ["InvalidParams", "NotFound", "Conflict"],
|
|
26
|
+
discoverable: !0,
|
|
27
|
+
transports: { http: !0, jsonRpcHttp: !0, jsonRpcWebSocket: !0, jsonRpcStdio: !0 },
|
|
28
|
+
unknownKeyPolicy: { http: "strip", jsonRpc: "reject" },
|
|
29
|
+
invoke: async ({ input, engine }) => {
|
|
30
|
+
const e = engine, name = validateWorkflowNameField(input.name), revision = validateWorkflowRevisionField(input.revision);
|
|
31
|
+
try {
|
|
32
|
+
return await e.workflows.preload(name, revision);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (!isWeftError(error))
|
|
35
|
+
throw {
|
|
36
|
+
code: "Conflict",
|
|
37
|
+
message: `Dynamic workflow source "${name}" revision "${revision}" failed to load.`,
|
|
38
|
+
data: { reason: "load-failed" }
|
|
39
|
+
};
|
|
40
|
+
return throwWorkflowCatalogOperationFault(error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}), preloadWorkflowRevisionRestBinding = {
|
|
44
|
+
method: "POST",
|
|
45
|
+
path: "/v1/registry/workflows/:name/preload",
|
|
46
|
+
pathParamNames: ["name"],
|
|
47
|
+
operationName: "weft.workflows.revisions.preload",
|
|
48
|
+
inputSources: {
|
|
49
|
+
name: { kind: "path", pathParam: "name" },
|
|
50
|
+
revision: { kind: "body-field", bodyField: "revision" }
|
|
51
|
+
},
|
|
52
|
+
extractInput: async (request, pathParams, context) => {
|
|
53
|
+
const body = await readWorkflowCatalogRestBody(request, context);
|
|
54
|
+
return {
|
|
55
|
+
name: pathParams.name ?? "",
|
|
56
|
+
revision: body.revision
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
success: { kind: "json", status: 200 }
|
|
60
|
+
};
|
|
@@ -6,6 +6,7 @@ export type ScheduleMutableOptionsInput = {
|
|
|
6
6
|
overlap?: unknown;
|
|
7
7
|
backfill?: unknown;
|
|
8
8
|
jitter?: unknown;
|
|
9
|
+
revisionPolicy?: unknown;
|
|
9
10
|
};
|
|
10
11
|
/** Validate the mutable schedule options shared by create and update. */
|
|
11
12
|
export declare function validateScheduleMutableOptions(input: ScheduleMutableOptionsInput): ScheduleUpdateOptions;
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
import { WorkflowRevisionUnavailableError } from "../../core/engine/revision-errors.js";
|
|
1
2
|
import {
|
|
2
3
|
isValidScheduleOverlapPolicy,
|
|
4
|
+
isValidScheduleRevisionPolicy,
|
|
3
5
|
normalizeScheduleUpdateOptions
|
|
4
6
|
} from "../../core/engine/validation/schedule.js";
|
|
5
7
|
import { invalidParamsFault } from "./operation-helpers.js";
|
|
6
8
|
export { isOperationFault } from "./operation-helpers.js";
|
|
7
9
|
export function validateScheduleMutableOptions(input) {
|
|
8
|
-
const description = validateScheduleDescription(input.description), overlap = validateScheduleOverlap(input.overlap), backfill = validateScheduleBackfill(input.backfill), jitter = validateScheduleJitter(input.jitter);
|
|
10
|
+
const description = validateScheduleDescription(input.description), overlap = validateScheduleOverlap(input.overlap), backfill = validateScheduleBackfill(input.backfill), jitter = validateScheduleJitter(input.jitter), revisionPolicy = validateScheduleRevisionPolicy(input.revisionPolicy);
|
|
9
11
|
return {
|
|
10
12
|
...description !== void 0 ? { description } : {},
|
|
11
13
|
...overlap !== void 0 ? { overlap } : {},
|
|
12
14
|
...backfill !== void 0 ? { backfill } : {},
|
|
13
|
-
...jitter !== void 0 ? { jitter } : {}
|
|
15
|
+
...jitter !== void 0 ? { jitter } : {},
|
|
16
|
+
...revisionPolicy !== void 0 ? { revisionPolicy } : {}
|
|
14
17
|
};
|
|
15
18
|
}
|
|
16
19
|
function validateScheduleDescription(value) {
|
|
@@ -27,6 +30,13 @@ function validateScheduleOverlap(value) {
|
|
|
27
30
|
throw invalidParamsFault('Field "overlap" must be one of skip, queue, cancel-running, allow');
|
|
28
31
|
return value;
|
|
29
32
|
}
|
|
33
|
+
function validateScheduleRevisionPolicy(value) {
|
|
34
|
+
if (value === void 0)
|
|
35
|
+
return;
|
|
36
|
+
if (!isValidScheduleRevisionPolicy(value))
|
|
37
|
+
throw invalidParamsFault('Field "revisionPolicy" must be one of active-at-fire, pinned');
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
30
40
|
function validateScheduleBackfill(value) {
|
|
31
41
|
if (value === void 0)
|
|
32
42
|
return;
|
|
@@ -51,7 +61,25 @@ function formatJitterValidationMessage(message) {
|
|
|
51
61
|
const enginePrefix = "Invalid options.jitter: ", hasEnginePrefix = message.startsWith(enginePrefix), wireDetail = (hasEnginePrefix ? message.slice(enginePrefix.length) : message).replaceAll("options.jitter", 'Field "jitter"');
|
|
52
62
|
return hasEnginePrefix ? `Field "jitter" is invalid: ${wireDetail}` : wireDetail;
|
|
53
63
|
}
|
|
64
|
+
function mapRevisionUnavailableToFault(error) {
|
|
65
|
+
if (!(error instanceof WorkflowRevisionUnavailableError))
|
|
66
|
+
return;
|
|
67
|
+
return {
|
|
68
|
+
code: "Conflict",
|
|
69
|
+
message: error.message,
|
|
70
|
+
data: { reason: error.reason }
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function isScheduleConflictMessage(normalizedMessage) {
|
|
74
|
+
return normalizedMessage.includes("already exists") || normalizedMessage.includes("cannot be resumed");
|
|
75
|
+
}
|
|
76
|
+
function isScheduleInvalidParamsMessage(message, normalizedMessage) {
|
|
77
|
+
return message.includes("Missing required field") || normalizedMessage.includes("must be") || normalizedMessage.includes("no workflow registered") || normalizedMessage.includes("cron") || normalizedMessage.includes("interval");
|
|
78
|
+
}
|
|
54
79
|
export function mapScheduleErrorToFault(scheduleId, error) {
|
|
80
|
+
const revisionFault = mapRevisionUnavailableToFault(error);
|
|
81
|
+
if (revisionFault !== void 0)
|
|
82
|
+
return revisionFault;
|
|
55
83
|
const message = error instanceof Error ? error.message : String(error), normalizedMessage = message.toLowerCase();
|
|
56
84
|
if (normalizedMessage.includes("not found"))
|
|
57
85
|
return {
|
|
@@ -59,13 +87,13 @@ export function mapScheduleErrorToFault(scheduleId, error) {
|
|
|
59
87
|
message,
|
|
60
88
|
data: { resource: "schedule", identifier: scheduleId }
|
|
61
89
|
};
|
|
62
|
-
if (
|
|
90
|
+
if (isScheduleConflictMessage(normalizedMessage))
|
|
63
91
|
return {
|
|
64
92
|
code: "Conflict",
|
|
65
93
|
message,
|
|
66
94
|
data: { reason: message }
|
|
67
95
|
};
|
|
68
|
-
if (message
|
|
96
|
+
if (isScheduleInvalidParamsMessage(message, normalizedMessage))
|
|
69
97
|
return {
|
|
70
98
|
code: "InvalidParams",
|
|
71
99
|
message,
|
|
@@ -6,6 +6,7 @@ export type SharedScheduleRestFields = {
|
|
|
6
6
|
readonly overlap: unknown;
|
|
7
7
|
readonly backfill: unknown;
|
|
8
8
|
readonly jitter: unknown;
|
|
9
|
+
readonly revisionPolicy: unknown;
|
|
9
10
|
};
|
|
10
11
|
export declare function parseScheduleRestBodyRequestRecord(request: Request, context?: RestInputContext): Promise<Record<string, unknown>>;
|
|
11
12
|
export declare function extractSharedScheduleRestFields(record: Record<string, unknown>): SharedScheduleRestFields;
|
|
@@ -90,6 +90,10 @@ import {
|
|
|
90
90
|
} from "./list-workflow-revisions.js";
|
|
91
91
|
import { listWorkflowsOperation, listWorkflowsRestBinding } from "./list-workflows.js";
|
|
92
92
|
import { pauseScheduleOperation, pauseScheduleRestBinding } from "./pause-schedule.js";
|
|
93
|
+
import {
|
|
94
|
+
preloadWorkflowRevisionOperation,
|
|
95
|
+
preloadWorkflowRevisionRestBinding
|
|
96
|
+
} from "./preload-workflow-revision.js";
|
|
93
97
|
import { purgeWorkflowsOperation, purgeWorkflowsRestBinding } from "./purge-workflows.js";
|
|
94
98
|
import {
|
|
95
99
|
queryWorkflowOperation,
|
|
@@ -178,6 +182,7 @@ export const STATIC_REST_BINDINGS = [
|
|
|
178
182
|
getRegistryRestBinding,
|
|
179
183
|
installWorkflowRevisionRestBinding,
|
|
180
184
|
activateWorkflowRevisionRestBinding,
|
|
185
|
+
preloadWorkflowRevisionRestBinding,
|
|
181
186
|
getWorkflowRevisionRestBinding,
|
|
182
187
|
listWorkflowRevisionsRestBinding,
|
|
183
188
|
getActiveWorkflowRevisionRestBinding,
|
|
@@ -247,6 +252,7 @@ export const STATIC_REST_BINDINGS = [
|
|
|
247
252
|
getRegistryOperation,
|
|
248
253
|
installWorkflowRevisionOperation,
|
|
249
254
|
activateWorkflowRevisionOperation,
|
|
255
|
+
preloadWorkflowRevisionOperation,
|
|
250
256
|
getWorkflowRevisionOperation,
|
|
251
257
|
listWorkflowRevisionsOperation,
|
|
252
258
|
getActiveWorkflowRevisionOperation,
|
|
@@ -8,6 +8,7 @@ declare const updateScheduleInput: z.ZodObject<{
|
|
|
8
8
|
overlap: z.ZodOptional<z.ZodUnknown>;
|
|
9
9
|
backfill: z.ZodOptional<z.ZodUnknown>;
|
|
10
10
|
jitter: z.ZodOptional<z.ZodUnknown>;
|
|
11
|
+
revisionPolicy: z.ZodOptional<z.ZodUnknown>;
|
|
11
12
|
}, z.core.$strip>;
|
|
12
13
|
export type UpdateScheduleInput = z.infer<typeof updateScheduleInput>;
|
|
13
14
|
export declare const updateScheduleOperation: import("../operation-catalog.ts").OperationDefinition<{
|
|
@@ -18,6 +19,7 @@ export declare const updateScheduleOperation: import("../operation-catalog.ts").
|
|
|
18
19
|
overlap?: unknown;
|
|
19
20
|
backfill?: unknown;
|
|
20
21
|
jitter?: unknown;
|
|
22
|
+
revisionPolicy?: unknown;
|
|
21
23
|
}, null, unknown>;
|
|
22
24
|
export declare const updateScheduleRestBinding: UnknownRestBinding;
|
|
23
25
|
export {};
|
|
@@ -16,7 +16,8 @@ const updateScheduleInput = z.object({
|
|
|
16
16
|
description: z.unknown().optional().describe("Operator-facing schedule description. Runtime validation requires a string."),
|
|
17
17
|
overlap: z.unknown().optional(),
|
|
18
18
|
backfill: z.unknown().optional(),
|
|
19
|
-
jitter: z.unknown().optional()
|
|
19
|
+
jitter: z.unknown().optional(),
|
|
20
|
+
revisionPolicy: z.unknown().optional().describe('Revision policy (WFT-20). Runtime validation requires "active-at-fire" or "pinned". Omitted preserves the current policy (and pin); "pinned" always re-resolves and re-captures the pin against the revision active right now.')
|
|
20
21
|
});
|
|
21
22
|
export const updateScheduleOperation = defineOperation({
|
|
22
23
|
name: "weft.schedules.update",
|
|
@@ -52,7 +53,8 @@ export const updateScheduleOperation = defineOperation({
|
|
|
52
53
|
description: { kind: "body-field", bodyField: "description" },
|
|
53
54
|
overlap: { kind: "body-field", bodyField: "overlap" },
|
|
54
55
|
backfill: { kind: "body-field", bodyField: "backfill" },
|
|
55
|
-
jitter: { kind: "body-field", bodyField: "jitter" }
|
|
56
|
+
jitter: { kind: "body-field", bodyField: "jitter" },
|
|
57
|
+
revisionPolicy: { kind: "body-field", bodyField: "revisionPolicy" }
|
|
56
58
|
},
|
|
57
59
|
extractInput: async (request, pathParams, context) => {
|
|
58
60
|
const record = await parseScheduleRestBodyRequestRecord(request, context);
|
|
@@ -5,7 +5,12 @@ import {
|
|
|
5
5
|
} from "../../core/catalog/index.js";
|
|
6
6
|
import { MAX_CONTRACT_IDENTIFIER_BYTES } from "../../core/contract/limits.js";
|
|
7
7
|
import { parseWorkflowRevisionManifest } from "../../core/contract/manifest-parse.js";
|
|
8
|
+
import {
|
|
9
|
+
DynamicWorkflowSourceUnavailableError,
|
|
10
|
+
WorkflowSourceNotRegisteredError
|
|
11
|
+
} from "../../core/engine/dynamic-source-errors.js";
|
|
8
12
|
import { WorkflowNotRegisteredError } from "../../core/engine/errors.js";
|
|
13
|
+
import { WorkflowSourceValidationError } from "../../core/source/errors.js";
|
|
9
14
|
import { validateWorkflowOrActivityName } from "../../core/types/name-grammar.js";
|
|
10
15
|
import { readRestJsonBody } from "../rest-body.js";
|
|
11
16
|
import { invalidParamsFault, isOperationFault } from "./operation-helpers.js";
|
|
@@ -121,6 +126,28 @@ export function throwWorkflowCatalogOperationFault(error) {
|
|
|
121
126
|
message: error.message,
|
|
122
127
|
data: { reason: "catalog-conflict", weftCode: error.code }
|
|
123
128
|
};
|
|
129
|
+
if (error instanceof WorkflowSourceNotRegisteredError)
|
|
130
|
+
throw {
|
|
131
|
+
code: "NotFound",
|
|
132
|
+
message: error.message,
|
|
133
|
+
data: { resource: "workflow-source", identifier: error.workflowType, weftCode: error.code }
|
|
134
|
+
};
|
|
135
|
+
if (error instanceof DynamicWorkflowSourceUnavailableError)
|
|
136
|
+
throw {
|
|
137
|
+
code: "Conflict",
|
|
138
|
+
message: error.reason === "load-failed" ? `Dynamic workflow source "${error.workflowType}"${error.revision === void 0 ? "" : ` revision "${error.revision}"`} failed to load.` : error.message,
|
|
139
|
+
data: { reason: error.reason, weftCode: error.code }
|
|
140
|
+
};
|
|
141
|
+
if (error instanceof WorkflowSourceValidationError)
|
|
142
|
+
throw {
|
|
143
|
+
code: "Conflict",
|
|
144
|
+
message: error.message,
|
|
145
|
+
data: {
|
|
146
|
+
reason: "validation-failed",
|
|
147
|
+
sourceValidationReasons: error.reasons,
|
|
148
|
+
weftCode: error.code
|
|
149
|
+
}
|
|
150
|
+
};
|
|
124
151
|
throw error;
|
|
125
152
|
}
|
|
126
153
|
export async function readWorkflowCatalogRestBody(request, context) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the durable task-ledger envelope a fresh dispatch would create if no
|
|
3
|
+
* ledger record exists yet — split out of `task-dispatch.ts` purely to stay
|
|
4
|
+
* under this repository's 500-line-per-file ceiling, matching the existing
|
|
5
|
+
* `task-dispatch-revision.ts` precedent. There is no behavioral reason to
|
|
6
|
+
* import this module directly instead of `task-dispatch.ts`, which calls it
|
|
7
|
+
* from both `selectAndReserveWorker` and `enqueueTaskForLongPoll`.
|
|
8
|
+
*
|
|
9
|
+
* @module server/runtime/task-dispatch-envelope
|
|
10
|
+
*/
|
|
11
|
+
import type { TaskDispatch } from '../index.ts';
|
|
12
|
+
import type { CreateQueuedInput } from '../task-ledger-transitions.ts';
|
|
13
|
+
/** The durable envelope every fresh dispatch would create if no ledger record exists yet. */
|
|
14
|
+
export declare function buildCreateQueuedInput(task: TaskDispatch, queue: string, visibilityTimeout: number): CreateQueuedInput;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isJSONValue } from "../../core/json.js";
|
|
2
|
+
import { isValidWorkflowRevision, REMOTE_TASK_RECORD_VERSION } from "../task-ledger.js";
|
|
3
|
+
function buildOptionalCreateQueuedFields(task) {
|
|
4
|
+
return {
|
|
5
|
+
...task.workflowId !== void 0 ? { workflowId: task.workflowId } : {},
|
|
6
|
+
...task.workflowExecutionToken !== void 0 ? { workflowExecutionToken: task.workflowExecutionToken } : {},
|
|
7
|
+
...task.workflowRevision !== void 0 ? { workflowRevision: task.workflowRevision } : {},
|
|
8
|
+
...task.priority !== void 0 ? { priority: task.priority } : {},
|
|
9
|
+
...task.fairShareKey !== void 0 ? { fairShareKey: task.fairShareKey } : {},
|
|
10
|
+
...task.sticky && task.workflowId !== void 0 ? { stickyWorkflowId: task.workflowId } : {},
|
|
11
|
+
...task.retryPolicy !== void 0 ? { retryPolicy: task.retryPolicy } : {}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function buildCreateQueuedInput(task, queue, visibilityTimeout) {
|
|
15
|
+
const input = task.input === void 0 ? null : task.input;
|
|
16
|
+
if (!isJSONValue(input))
|
|
17
|
+
throw Error(`TaskDispatch for operation "${task.operationId}" has a non-JSON-serializable "input" \u2014 the durable task ledger requires JSON-safe input.`);
|
|
18
|
+
if (task.workflowRevision !== void 0 && !isValidWorkflowRevision(task.workflowRevision))
|
|
19
|
+
throw Error(`TaskDispatch for operation "${task.operationId}" has an invalid "workflowRevision" \u2014 it must be a non-empty, bounded identifier.`);
|
|
20
|
+
return {
|
|
21
|
+
recordVersion: REMOTE_TASK_RECORD_VERSION,
|
|
22
|
+
operationId: task.operationId,
|
|
23
|
+
workflowType: task.workflowType,
|
|
24
|
+
activityName: task.activityName,
|
|
25
|
+
queue,
|
|
26
|
+
input,
|
|
27
|
+
headers: task.headers ?? {},
|
|
28
|
+
visibilityTimeoutMilliseconds: visibilityTimeout,
|
|
29
|
+
createdAt: Date.now(),
|
|
30
|
+
...buildOptionalCreateQueuedFields(task)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch-time revision staleness gate (WFT-20) — split out of
|
|
3
|
+
* `task-dispatch.ts` purely to stay under this repository's 500-line-per-file
|
|
4
|
+
* ceiling; there is no behavioral reason to import this module directly
|
|
5
|
+
* instead of `task-dispatch.ts`, which calls it from `dispatchTaskImpl`.
|
|
6
|
+
*
|
|
7
|
+
* @module server/runtime/task-dispatch-revision
|
|
8
|
+
*/
|
|
9
|
+
import { type ConditionalBatchCondition } from '../../storage/interface.ts';
|
|
10
|
+
import type { ServeOptions, TaskDispatch } from '../index.ts';
|
|
11
|
+
/**
|
|
12
|
+
* Reject a dispatch whose caller-supplied `workflowRevision` disagrees with
|
|
13
|
+
* `task.workflowId`'s persisted `WorkflowState.revision` — a bounded,
|
|
14
|
+
* structured failure BEFORE any worker reservation or ledger write. A no-op
|
|
15
|
+
* (never reads storage) unless BOTH `workflowId` and `workflowRevision` are
|
|
16
|
+
* supplied; a no-op when no persisted `WorkflowState` exists for `workflowId`
|
|
17
|
+
* yet (nothing to compare against — the caller opted into an extra read, not
|
|
18
|
+
* an extra existence requirement).
|
|
19
|
+
*
|
|
20
|
+
* Returns the `conditionalBatch` precondition(s) (empty when this check was a
|
|
21
|
+
* no-op) that fence the CALLER's own ledger commit on the exact workflow-state
|
|
22
|
+
* bytes just read here. This check alone is only a pre-commit read: a
|
|
23
|
+
* `start-new` restart could still displace the workflow to a different
|
|
24
|
+
* revision in the gap between this read and the caller's later ledger write.
|
|
25
|
+
* The caller MUST include the returned conditions in the SAME
|
|
26
|
+
* `conditionalBatch` call that commits the ledger record, so that gap closes
|
|
27
|
+
* atomically instead of merely being read-checked up front (WFT-20).
|
|
28
|
+
*/
|
|
29
|
+
export declare function assertDispatchTargetsFreshRevision(options: ServeOptions, task: TaskDispatch): Promise<ConditionalBatchCondition[]>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { decodeWorkflowState } from "../../core/engine/validation.js";
|
|
2
|
+
import { KEYS } from "../../storage/interface.js";
|
|
3
|
+
export async function assertDispatchTargetsFreshRevision(options, task) {
|
|
4
|
+
if (task.workflowId === void 0 || task.workflowRevision === void 0)
|
|
5
|
+
return [];
|
|
6
|
+
const workflowKey = KEYS.workflow(task.workflowId), persistedBytes = await options.engine.storage.get(workflowKey);
|
|
7
|
+
if (persistedBytes === null)
|
|
8
|
+
return [];
|
|
9
|
+
const persistedRevision = decodeWorkflowState(persistedBytes).revision;
|
|
10
|
+
if (persistedRevision !== task.workflowRevision)
|
|
11
|
+
throw Error(`TaskDispatch for operation "${task.operationId}" targets workflow "${task.workflowId}" at revision "${task.workflowRevision}", but the persisted run is pinned to revision ${persistedRevision === void 0 ? "undefined (a legacy, pre-pinning record)" : `"${persistedRevision}"`} ` + "\u2014 this dispatch is stale.");
|
|
12
|
+
return [{ key: workflowKey, expectedValue: persistedBytes }];
|
|
13
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isJSONValue } from "../../core/json.js";
|
|
2
1
|
import { buildWorkerExecutionIdentity } from "../../worker/manifest/execution-identity.js";
|
|
3
2
|
import { evictOldestAffinityEntries } from "../runtime-helpers.js";
|
|
4
3
|
import {
|
|
@@ -7,9 +6,10 @@ import {
|
|
|
7
6
|
} from "../task-ledger-transitions.js";
|
|
8
7
|
import {
|
|
9
8
|
decodeRemoteTaskRecord,
|
|
10
|
-
REMOTE_TASK_RECORD_VERSION,
|
|
11
9
|
taskLedgerKey
|
|
12
10
|
} from "../task-ledger.js";
|
|
11
|
+
import { buildCreateQueuedInput } from "./task-dispatch-envelope.js";
|
|
12
|
+
import { assertDispatchTargetsFreshRevision } from "./task-dispatch-revision.js";
|
|
13
13
|
import { commitTaskLedgerTransition } from "./task-ledger-runtime.js";
|
|
14
14
|
import {
|
|
15
15
|
recordTaskBacklogMetric,
|
|
@@ -65,33 +65,6 @@ function recordDispatchOutcome(context, task, workerId) {
|
|
|
65
65
|
operationIds.add(task.operationId);
|
|
66
66
|
context.operationToWorkflow.set(task.operationId, task.workflowId);
|
|
67
67
|
}
|
|
68
|
-
function buildOptionalCreateQueuedFields(task) {
|
|
69
|
-
return {
|
|
70
|
-
...task.workflowId !== void 0 ? { workflowId: task.workflowId } : {},
|
|
71
|
-
...task.workflowExecutionToken !== void 0 ? { workflowExecutionToken: task.workflowExecutionToken } : {},
|
|
72
|
-
...task.priority !== void 0 ? { priority: task.priority } : {},
|
|
73
|
-
...task.fairShareKey !== void 0 ? { fairShareKey: task.fairShareKey } : {},
|
|
74
|
-
...task.sticky && task.workflowId !== void 0 ? { stickyWorkflowId: task.workflowId } : {},
|
|
75
|
-
...task.retryPolicy !== void 0 ? { retryPolicy: task.retryPolicy } : {}
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
function buildCreateQueuedInput(task, queue, visibilityTimeout) {
|
|
79
|
-
const input = task.input === void 0 ? null : task.input;
|
|
80
|
-
if (!isJSONValue(input))
|
|
81
|
-
throw Error(`TaskDispatch for operation "${task.operationId}" has a non-JSON-serializable "input" \u2014 the durable task ledger requires JSON-safe input.`);
|
|
82
|
-
return {
|
|
83
|
-
recordVersion: REMOTE_TASK_RECORD_VERSION,
|
|
84
|
-
operationId: task.operationId,
|
|
85
|
-
workflowType: task.workflowType,
|
|
86
|
-
activityName: task.activityName,
|
|
87
|
-
queue,
|
|
88
|
-
input,
|
|
89
|
-
headers: task.headers ?? {},
|
|
90
|
-
visibilityTimeoutMilliseconds: visibilityTimeout,
|
|
91
|
-
createdAt: Date.now(),
|
|
92
|
-
...buildOptionalCreateQueuedFields(task)
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
68
|
function buildCreateThenClaimTransition(createInput, claimInput) {
|
|
96
69
|
return (current, now) => {
|
|
97
70
|
let queuedRecord;
|
|
@@ -114,7 +87,7 @@ function bareActivityName(qualifiedActivityName) {
|
|
|
114
87
|
const dotIndex = qualifiedActivityName.indexOf(".");
|
|
115
88
|
return dotIndex === -1 ? qualifiedActivityName : qualifiedActivityName.slice(dotIndex + 1);
|
|
116
89
|
}
|
|
117
|
-
async function selectAndReserveWorker(context, options, task, queue, visibilityTimeout) {
|
|
90
|
+
async function selectAndReserveWorker(context, options, task, queue, visibilityTimeout, revisionFenceConditions) {
|
|
118
91
|
const routingOptions = buildRoutingOptions(context, task, queue), worker = context.registry.findWorker(task.activityName, routingOptions);
|
|
119
92
|
if (!worker)
|
|
120
93
|
return !1;
|
|
@@ -128,7 +101,7 @@ async function selectAndReserveWorker(context, options, task, queue, visibilityT
|
|
|
128
101
|
workflowType: task.workflowType,
|
|
129
102
|
activityName: bareActivityName(task.activityName)
|
|
130
103
|
});
|
|
131
|
-
context.registry.assignTask(worker.id, task.operationId, visibilityTimeout, task.fairShareKey, attemptToken);
|
|
104
|
+
context.registry.assignTask(worker.id, task.operationId, visibilityTimeout, task.fairShareKey, attemptToken, task.workflowRevision);
|
|
132
105
|
let result;
|
|
133
106
|
try {
|
|
134
107
|
result = await commitTaskLedgerTransition(options.engine.storage, task.operationId, buildCreateThenClaimTransition(createInput, {
|
|
@@ -136,7 +109,7 @@ async function selectAndReserveWorker(context, options, task, queue, visibilityT
|
|
|
136
109
|
workerSessionId: worker.id,
|
|
137
110
|
...executionIdentity !== void 0 ? { executionIdentity } : {},
|
|
138
111
|
leaseDurationMilliseconds: visibilityTimeout
|
|
139
|
-
}), 1);
|
|
112
|
+
}), 1, revisionFenceConditions);
|
|
140
113
|
} catch (error) {
|
|
141
114
|
context.registry.releaseReservation(task.operationId);
|
|
142
115
|
throw error;
|
|
@@ -159,6 +132,7 @@ async function selectAndReserveWorker(context, options, task, queue, visibilityT
|
|
|
159
132
|
...task.workflowExecutionToken !== void 0 && {
|
|
160
133
|
workflowExecutionToken: task.workflowExecutionToken
|
|
161
134
|
},
|
|
135
|
+
...task.workflowRevision !== void 0 && { workflowRevision: task.workflowRevision },
|
|
162
136
|
...task.headers ? { headers: task.headers } : {}
|
|
163
137
|
}));
|
|
164
138
|
recordTaskQueueLatencyMetric(context.metricsCollector, {
|
|
@@ -169,11 +143,11 @@ async function selectAndReserveWorker(context, options, task, queue, visibilityT
|
|
|
169
143
|
recordDispatchOutcome(context, task, worker.id);
|
|
170
144
|
return !0;
|
|
171
145
|
}
|
|
172
|
-
async function enqueueTaskForLongPoll(context, options, task, queue, visibilityTimeout, resolvedPriority) {
|
|
146
|
+
async function enqueueTaskForLongPoll(context, options, task, queue, visibilityTimeout, resolvedPriority, revisionFenceConditions) {
|
|
173
147
|
const storage = options.engine.storage, createInput = buildCreateQueuedInput(task, queue, visibilityTimeout), rawExisting = await storage.get(taskLedgerKey(task.operationId)), current = decodeRemoteTaskRecord(rawExisting);
|
|
174
148
|
let queuedRecord;
|
|
175
149
|
if (current === null) {
|
|
176
|
-
const result = await commitTaskLedgerTransition(storage, task.operationId, (freshCurrent, now) => createQueued(freshCurrent, createInput, now), 1);
|
|
150
|
+
const result = await commitTaskLedgerTransition(storage, task.operationId, (freshCurrent, now) => createQueued(freshCurrent, createInput, now), 1, revisionFenceConditions);
|
|
177
151
|
if (!result.ok) {
|
|
178
152
|
const raced = decodeRemoteTaskRecord(await storage.get(taskLedgerKey(task.operationId)));
|
|
179
153
|
if (raced === null || raced.state !== "queued")
|
|
@@ -194,6 +168,7 @@ async function enqueueTaskForLongPoll(context, options, task, queue, visibilityT
|
|
|
194
168
|
visibilityTimeout,
|
|
195
169
|
workflowId: task.workflowId,
|
|
196
170
|
workflowExecutionToken: task.workflowExecutionToken,
|
|
171
|
+
workflowRevision: queuedRecord.workflowRevision,
|
|
197
172
|
firstQueuedAt: queuedRecord.firstQueuedAt,
|
|
198
173
|
lastQueuedAt: queuedRecord.lastQueuedAt,
|
|
199
174
|
lastDispatchedAt: queuedRecord.lastDispatchedAt,
|
|
@@ -214,15 +189,15 @@ export async function dispatchTaskImpl(context, options, task) {
|
|
|
214
189
|
}
|
|
215
190
|
if (!task.workflowType)
|
|
216
191
|
throw Error(`TaskDispatch for operation "${task.operationId}" is missing required field "workflowType".`);
|
|
217
|
-
const dotIndex = task.activityName.indexOf(".");
|
|
192
|
+
const revisionFenceConditions = await assertDispatchTargetsFreshRevision(options, task), dotIndex = task.activityName.indexOf(".");
|
|
218
193
|
if (dotIndex !== -1 && task.activityName.slice(0, dotIndex) !== task.workflowType)
|
|
219
194
|
throw Error(`TaskDispatch for operation "${task.operationId}" has activityName "${task.activityName}" whose qualifier does not match workflowType "${task.workflowType}".`);
|
|
220
195
|
const queue = task.queue ?? "default", visibilityTimeout = clampVisibilityTimeout(task.visibilityTimeout), resolvedPriority = resolveTaskPriority(context, options, task);
|
|
221
196
|
if (!validateTaskEnvelope(context, task))
|
|
222
197
|
return !1;
|
|
223
|
-
if (await selectAndReserveWorker(context, options, task, queue, visibilityTimeout))
|
|
198
|
+
if (await selectAndReserveWorker(context, options, task, queue, visibilityTimeout, revisionFenceConditions))
|
|
224
199
|
return !0;
|
|
225
|
-
return enqueueTaskForLongPoll(context, options, task, queue, visibilityTimeout, resolvedPriority);
|
|
200
|
+
return enqueueTaskForLongPoll(context, options, task, queue, visibilityTimeout, resolvedPriority, revisionFenceConditions);
|
|
226
201
|
}
|
|
227
202
|
export function cancelTask(context, operationId) {
|
|
228
203
|
const task = context.registry.getTask(operationId);
|
|
@@ -16,7 +16,7 @@ function rebuildWorkflowIndex(context, operationId, workflowId) {
|
|
|
16
16
|
}
|
|
17
17
|
function rehydrateWorkerOwnership(context, record, now) {
|
|
18
18
|
const remaining = Math.max(0, record.leaseDeadline - now);
|
|
19
|
-
context.registry.assignTask(record.workerSessionId, record.operationId, remaining, record.fairShareKey, record.attemptToken);
|
|
19
|
+
context.registry.assignTask(record.workerSessionId, record.operationId, remaining, record.fairShareKey, record.attemptToken, record.workflowRevision);
|
|
20
20
|
const tracked = context.registry.getWorkerTasks(record.workerSessionId).find((task) => task.operationId === record.operationId);
|
|
21
21
|
if (tracked)
|
|
22
22
|
tracked.visibilityTimeout = record.visibilityTimeoutMilliseconds;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module server/runtime/task-ledger-runtime
|
|
18
18
|
*/
|
|
19
|
-
import { type Storage } from '../../storage/interface.ts';
|
|
19
|
+
import { type ConditionalBatchCondition, type Storage } from '../../storage/interface.ts';
|
|
20
20
|
import type { TaskLedgerPreconditionResult, TaskLedgerTransitionResult } from '../task-ledger-transitions.ts';
|
|
21
21
|
import { type RemoteTaskRecord } from '../task-ledger.ts';
|
|
22
22
|
export type TaskLedgerCommitResult<T extends RemoteTaskRecord> = Readonly<{
|
|
@@ -46,7 +46,19 @@ export type TaskLedgerDeleteResult = Readonly<{
|
|
|
46
46
|
* reflects reality and re-attempting the identical transition cannot change
|
|
47
47
|
* the outcome.
|
|
48
48
|
*/
|
|
49
|
-
export declare function commitTaskLedgerTransition<T extends RemoteTaskRecord>(storage: Storage, operationId: string, transitionFn: (current: RemoteTaskRecord | null, now: number) => TaskLedgerTransitionResult<T>, maxAttempts?: number
|
|
49
|
+
export declare function commitTaskLedgerTransition<T extends RemoteTaskRecord>(storage: Storage, operationId: string, transitionFn: (current: RemoteTaskRecord | null, now: number) => TaskLedgerTransitionResult<T>, maxAttempts?: number,
|
|
50
|
+
/**
|
|
51
|
+
* Extra same-transaction preconditions to fence the commit on — for
|
|
52
|
+
* example, "the target workflow's persisted state is still exactly the
|
|
53
|
+
* bytes read at dispatch time" (WFT-20's revision-staleness gate). Checked
|
|
54
|
+
* in the SAME `conditionalBatch` call as the ledger key's own CAS, so a
|
|
55
|
+
* concurrent write to any of these keys between read and commit loses the
|
|
56
|
+
* whole batch atomically, closing the gap a standalone pre-check
|
|
57
|
+
* (read-then-later-write, with unrelated work in between) cannot close.
|
|
58
|
+
* Re-supplied by the caller on each retry attempt since the caller, not
|
|
59
|
+
* this loop, owns re-reading whatever these conditions guard.
|
|
60
|
+
*/
|
|
61
|
+
additionalConditions?: readonly ConditionalBatchCondition[]): Promise<TaskLedgerCommitResult<T>>;
|
|
50
62
|
/**
|
|
51
63
|
* Read the current ledger record, check a delete-only precondition (WFT-24
|
|
52
64
|
* retention: {@link canDeleteRetainedTerminalTask}), and conditionally delete
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
storageConditionalBatch
|
|
3
|
+
} from "../../storage/interface.js";
|
|
2
4
|
import {
|
|
3
5
|
decodeRemoteTaskRecord,
|
|
4
6
|
encodeRemoteTaskRecord,
|
|
5
7
|
taskLedgerKey
|
|
6
8
|
} from "../task-ledger.js";
|
|
7
|
-
export async function commitTaskLedgerTransition(storage, operationId, transitionFn, maxAttempts = 1) {
|
|
9
|
+
export async function commitTaskLedgerTransition(storage, operationId, transitionFn, maxAttempts = 1, additionalConditions = []) {
|
|
8
10
|
const key = taskLedgerKey(operationId);
|
|
9
11
|
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
10
12
|
const rawExisting = await storage.get(key), current = decodeRemoteTaskRecord(rawExisting), result = transitionFn(current, Date.now());
|
|
11
13
|
if (!result.ok)
|
|
12
14
|
return result;
|
|
13
|
-
if (await storageConditionalBatch(storage, [{ key, expectedValue: rawExisting }], [{ type: "put", key, value: encodeRemoteTaskRecord(result.nextRecord) }]))
|
|
15
|
+
if (await storageConditionalBatch(storage, [{ key, expectedValue: rawExisting }, ...additionalConditions], [{ type: "put", key, value: encodeRemoteTaskRecord(result.nextRecord) }]))
|
|
14
16
|
return { ok: !0, record: result.nextRecord };
|
|
15
17
|
}
|
|
16
18
|
return {
|