@lostgradient/weft 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/cli/conformance.js +14 -18
- package/dist/cli/generated/operation-catalog.snapshot.json +331 -16
- package/dist/cli/generated/operation-client.generated.d.ts +39 -6
- package/dist/cli/generated/operation-client.generated.js +6 -0
- package/dist/core/context/child-workflow-pipe.d.ts +1 -1
- package/dist/core/context/child-workflow-pipe.js +1 -1
- package/dist/core/engine/construction.d.ts +2 -0
- package/dist/core/engine/construction.js +3 -0
- package/dist/core/engine/index.js +1 -0
- package/dist/core/engine/listing.d.ts +1 -0
- package/dist/core/engine/listing.js +6 -4
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +2 -1
- package/dist/core/worker-execution-dispatcher.d.ts +8 -0
- package/dist/core/worker-execution-dispatcher.js +4 -0
- package/dist/core/worker-execution-strategy-options.d.ts +16 -0
- package/dist/core/worker-execution-strategy.js +71 -89
- package/dist/core/worker-fault-handling.d.ts +45 -0
- package/dist/core/worker-fault-handling.js +88 -0
- package/dist/core/worker-protocol.d.ts +18 -1
- package/dist/core/worker-protocol.js +1 -1
- package/dist/core/worker-realm-readiness.d.ts +78 -0
- package/dist/core/worker-realm-readiness.js +127 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +28 -1
- package/dist/json-schema.js +1 -1
- package/dist/runtime/portable.d.ts +14 -0
- package/dist/runtime/portable.js +10 -0
- package/dist/server/authorization-scope.d.ts +32 -2
- package/dist/server/index.d.ts +73 -8
- package/dist/server/index.js +11 -3
- package/dist/server/operations/get-principal.d.ts +75 -0
- package/dist/server/operations/get-principal.js +41 -0
- package/dist/server/operations/get-task-diagnostics.d.ts +43 -18
- package/dist/server/operations/get-task-diagnostics.js +87 -126
- package/dist/server/operations/get-worker-diagnostics.d.ts +177 -0
- package/dist/server/operations/get-worker-diagnostics.js +117 -0
- package/dist/server/operations/list-worker-registration-rejections.d.ts +79 -0
- package/dist/server/operations/list-worker-registration-rejections.js +60 -0
- package/dist/server/operations/list-workers.js +1 -3
- package/dist/server/operations/static-registrations.d.ts +28 -0
- package/dist/server/operations/static-registrations.js +248 -0
- package/dist/server/rest-bindings.d.ts +5 -0
- package/dist/server/rest-bindings.js +34 -313
- package/dist/server/runtime/authentication-bridge.js +6 -14
- package/dist/server/runtime/context.d.ts +23 -0
- package/dist/server/runtime/task-dispatch.d.ts +7 -1
- package/dist/server/runtime/task-dispatch.js +126 -55
- package/dist/server/runtime/task-ledger-completion.d.ts +68 -0
- package/dist/server/runtime/task-ledger-completion.js +72 -0
- package/dist/server/runtime/task-ledger-recovery.d.ts +55 -0
- package/dist/server/runtime/task-ledger-recovery.js +73 -0
- package/dist/server/runtime/task-ledger-runtime.d.ts +64 -0
- package/dist/server/runtime/task-ledger-runtime.js +34 -0
- package/dist/server/runtime/task-metrics.d.ts +6 -4
- package/dist/server/runtime/task-polling.d.ts +14 -12
- package/dist/server/runtime/task-polling.js +106 -82
- package/dist/server/runtime/task-reconciliation.d.ts +35 -8
- package/dist/server/runtime/task-reconciliation.js +75 -74
- package/dist/server/runtime/task-result-resolution.d.ts +2 -10
- package/dist/server/runtime/task-result-resolution.js +0 -67
- package/dist/server/runtime/task-result-view.d.ts +82 -0
- package/dist/server/runtime/task-result-view.js +48 -0
- package/dist/server/runtime/websocket-worker-messaging.d.ts +16 -0
- package/dist/server/runtime/websocket-worker-messaging.js +32 -0
- package/dist/server/runtime/websocket-worker-registration.d.ts +41 -0
- package/dist/server/runtime/websocket-worker-registration.js +134 -0
- package/dist/server/runtime/websocket-worker.d.ts +0 -1
- package/dist/server/runtime/websocket-worker.js +64 -116
- package/dist/server/serve-internals.d.ts +9 -6
- package/dist/server/serve-internals.js +14 -40
- package/dist/server/task-ledger-codec.d.ts +40 -0
- package/dist/server/task-ledger-codec.js +195 -0
- package/dist/server/task-ledger-keys.d.ts +13 -0
- package/dist/server/task-ledger-keys.js +5 -0
- package/dist/server/task-ledger-limits.d.ts +34 -0
- package/dist/server/task-ledger-limits.js +5 -0
- package/dist/server/task-ledger-transition-helpers.d.ts +38 -0
- package/dist/server/task-ledger-transition-helpers.js +42 -0
- package/dist/server/task-ledger-transitions-cancellation.d.ts +31 -0
- package/dist/server/task-ledger-transitions-cancellation.js +73 -0
- package/dist/server/task-ledger-transitions.d.ts +129 -0
- package/dist/server/task-ledger-transitions.js +162 -0
- package/dist/server/task-ledger-types.d.ts +220 -0
- package/dist/server/task-ledger-types.js +1 -0
- package/dist/server/task-ledger.d.ts +17 -0
- package/dist/server/task-ledger.js +24 -0
- package/dist/server/task-state.d.ts +25 -152
- package/dist/server/task-state.js +0 -188
- package/dist/server/worker-admission-policy.d.ts +70 -0
- package/dist/server/worker-admission-policy.js +0 -0
- package/dist/storage/compressed-storage.js +1 -1
- package/dist/storage/index.d.ts +0 -2
- package/dist/storage/interface.d.ts +0 -2
- package/dist/storage/interface.js +1 -1
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon.js +1 -1
- package/dist/storage/postgres.js +1 -1
- 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/storage/web-extension.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web-extension.js +1 -1
- package/dist/worker/index.d.ts +2 -0
- package/dist/worker/index.js +30 -4
- package/dist/worker/manifest/canonical-json.d.ts +20 -0
- package/dist/worker/manifest/canonical-json.js +13 -0
- package/dist/worker/manifest/capabilities.d.ts +24 -0
- package/dist/worker/manifest/capabilities.js +74 -0
- package/dist/worker/manifest/content-digest.d.ts +20 -0
- package/dist/worker/manifest/content-digest.js +13 -0
- package/dist/worker/manifest/declared-shape-digest.d.ts +14 -0
- package/dist/worker/manifest/declared-shape-digest.js +4 -0
- package/dist/worker/manifest/digest.d.ts +82 -0
- package/dist/worker/manifest/digest.js +9 -0
- package/dist/worker/manifest/execution-identity.d.ts +92 -0
- package/dist/worker/manifest/execution-identity.js +36 -0
- package/dist/worker/manifest/failure.d.ts +59 -0
- package/dist/worker/manifest/failure.js +4 -0
- package/dist/worker/manifest/index.d.ts +22 -0
- package/dist/worker/manifest/index.js +23 -0
- package/dist/worker/manifest/internal-realm.d.ts +50 -0
- package/dist/worker/manifest/internal-realm.js +39 -0
- package/dist/worker/manifest/is-record.d.ts +12 -0
- package/dist/worker/manifest/is-record.js +6 -0
- package/dist/worker/manifest/json-scan.d.ts +26 -0
- package/dist/worker/manifest/json-scan.js +88 -0
- package/dist/worker/manifest/limits.d.ts +138 -0
- package/dist/worker/manifest/limits.js +1 -0
- package/dist/worker/manifest/normalize.d.ts +63 -0
- package/dist/worker/manifest/normalize.js +73 -0
- package/dist/worker/manifest/parse-json.d.ts +24 -0
- package/dist/worker/manifest/parse-json.js +15 -0
- package/dist/worker/manifest/parse.d.ts +86 -0
- package/dist/worker/manifest/parse.js +188 -0
- package/dist/worker/manifest/registry-contract-builder.d.ts +120 -0
- package/dist/worker/manifest/registry-contract-builder.js +65 -0
- package/dist/worker/manifest/types.d.ts +263 -0
- package/dist/worker/manifest/types.js +1 -0
- package/dist/worker/manifest/utf8.d.ts +13 -0
- package/dist/worker/manifest/utf8.js +7 -0
- package/dist/worker/options.d.ts +55 -8
- package/dist/worker/options.js +65 -9
- package/dist/worker/protocol-internals.d.ts +1 -2
- package/dist/worker/protocol-internals.js +0 -5
- package/dist/worker/protocol-messages.d.ts +33 -17
- package/dist/worker/protocol-schemas.d.ts +36 -74
- package/dist/worker/protocol-schemas.js +25 -18
- package/dist/worker/protocol-version.d.ts +18 -14
- package/dist/worker/protocol-version.js +1 -1
- package/dist/worker/protocol.js +1 -1
- package/dist/worker/registry/deployment-consistency.d.ts +78 -0
- package/dist/worker/registry/deployment-consistency.js +27 -0
- package/dist/worker/registry/drain.d.ts +14 -0
- package/dist/worker/registry/drain.js +24 -0
- package/dist/worker/registry/rejections.d.ts +31 -0
- package/dist/worker/registry/rejections.js +10 -0
- package/dist/worker/registry/summary.d.ts +0 -3
- package/dist/worker/registry/summary.js +2 -5
- package/dist/worker/registry/types.d.ts +10 -3
- package/dist/worker/registry.d.ts +47 -2
- package/dist/worker/registry.js +45 -32
- package/dist/workers/workflow-worker-entry.d.ts +12 -7
- package/dist/workers/workflow-worker-entry.js +9 -2
- package/package.json +3 -3
- package/dist/server/task-resolved-record.d.ts +0 -12
- package/dist/server/task-resolved-record.js +0 -35
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { decode } from "../../core/codec.js";
|
|
2
1
|
import { WorkerDisconnectedEvent } from "../../core/events.js";
|
|
3
2
|
import { handleMcpHttpRequest } from "../../mcp/http.js";
|
|
4
|
-
import { KEYS } from "../../storage/interface.js";
|
|
5
3
|
import { authContextToPrincipal, handleRequest } from "../handler.js";
|
|
6
4
|
import { handleJsonRpcHttpRequestSafely } from "../json-rpc-transport-helpers.js";
|
|
7
5
|
import {
|
|
@@ -10,6 +8,7 @@ import {
|
|
|
10
8
|
openJsonRpcWebSocketSession
|
|
11
9
|
} from "../json-rpc-websocket-runtime.js";
|
|
12
10
|
import { API_PREFIX } from "../route-model.js";
|
|
11
|
+
import { decodeRemoteTaskRecord, taskLedgerKey } from "../task-ledger.js";
|
|
13
12
|
import { buildPreflightResponse, decorateResponseWithCors, isPreflightRequest } from "./cors.js";
|
|
14
13
|
import { gateRequest } from "./request-gate.js";
|
|
15
14
|
import { handleTaskPollRequest, handleTaskResultRequest } from "./task-polling.js";
|
|
@@ -25,7 +24,7 @@ import {
|
|
|
25
24
|
replayWatchEvents
|
|
26
25
|
} from "./websocket-stream.js";
|
|
27
26
|
import { handleWebSocketUpgrade } from "./websocket-upgrade.js";
|
|
28
|
-
import { handleWorkerWebSocketMessage
|
|
27
|
+
import { handleWorkerWebSocketMessage } from "./websocket-worker.js";
|
|
29
28
|
export function deriveSupportedOpenApiSecuritySchemes(auth) {
|
|
30
29
|
const schemes = new Set;
|
|
31
30
|
if (auth?.jwt !== void 0)
|
|
@@ -206,18 +205,11 @@ function runWorkerDisconnectRequeue(context, options, workerId, _ws, cleanupWork
|
|
|
206
205
|
for (const task of inFlightTasks)
|
|
207
206
|
(async () => {
|
|
208
207
|
try {
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
211
|
-
const record = decode(existing);
|
|
212
|
-
if (!isInflightRecord(record)) {
|
|
213
|
-
console.error(`[weft] Corrupt inflight record for task "${task.operationId}" \u2014 skipping reassignment`);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
208
|
+
const record = decodeRemoteTaskRecord(await options.engine.storage.get(taskLedgerKey(task.operationId)));
|
|
209
|
+
if (record !== null && record.state === "leased")
|
|
216
210
|
await reassignOrExpireTask(context, options, task.operationId, record, "worker-disconnect");
|
|
217
|
-
|
|
218
|
-
console.warn(`[weft] No
|
|
219
|
-
await options.engine.storage.delete(inflightKey);
|
|
220
|
-
}
|
|
211
|
+
else
|
|
212
|
+
console.warn(`[weft] No leased ledger record found for task "${task.operationId}" \u2014 skipping reassignment`);
|
|
221
213
|
} catch (error) {
|
|
222
214
|
console.error(`[weft] Failed to reassign task "${task.operationId}" from worker "${workerId}":`, error);
|
|
223
215
|
}
|
|
@@ -13,6 +13,19 @@ import type { createLiveOperationRegistry, createLiveRestBindings } from '../res
|
|
|
13
13
|
import type { TaskQueue } from '../task-queue.ts';
|
|
14
14
|
import type { WorkflowEventFeed } from '../workflow-event-feed.ts';
|
|
15
15
|
import type { ResolvedCorsPolicy } from './cors.ts';
|
|
16
|
+
/**
|
|
17
|
+
* Startup task-ledger recovery gate (WFT-23). `ready` resolves once every
|
|
18
|
+
* non-terminal ledger record has been reconstructed into the in-memory
|
|
19
|
+
* registry, deadline tracker, and task queue indexes; it rejects
|
|
20
|
+
* (permanently — a settled promise replays the same outcome to every future
|
|
21
|
+
* awaiter) if the recovery scan itself failed. Task-plane entry points
|
|
22
|
+
* (`dispatchTaskImpl`, long-poll claim/result, and worker registration) await
|
|
23
|
+
* this before touching the ledger, so a scan failure blocks new claims with
|
|
24
|
+
* an actionable error instead of silently continuing with partial indexes.
|
|
25
|
+
*/
|
|
26
|
+
export type TaskLedgerRecoveryGate = Readonly<{
|
|
27
|
+
ready: Promise<void>;
|
|
28
|
+
}>;
|
|
16
29
|
/**
|
|
17
30
|
* Internal closure state for a single `serve()` invocation.
|
|
18
31
|
*
|
|
@@ -77,4 +90,14 @@ export interface ServerContext {
|
|
|
77
90
|
readonly processingOperations: Set<string>;
|
|
78
91
|
/** Mutex: prevents concurrent reconciliation scans from running simultaneously. */
|
|
79
92
|
reconciliationRunning: boolean;
|
|
93
|
+
/** Startup task-ledger recovery gate — see {@link TaskLedgerRecoveryGate}. */
|
|
94
|
+
readonly taskLedgerRecovery: TaskLedgerRecoveryGate;
|
|
95
|
+
/**
|
|
96
|
+
* Set once `server.stop()`'s timer-clearing disposer has run. Startup
|
|
97
|
+
* recovery's scan loop and `scheduleDelayedDispatch` both check this before
|
|
98
|
+
* doing further work so a still-running recovery scan (or a reconciliation
|
|
99
|
+
* pass racing shutdown) cannot arm a new timer or issue a durable write
|
|
100
|
+
* after `pendingTimers` has already been cleared.
|
|
101
|
+
*/
|
|
102
|
+
stopping: boolean;
|
|
80
103
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { ServeOptions, TaskDispatch } from '../index.ts';
|
|
2
2
|
import type { ServerContext } from './context.ts';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Schedule a delayed dispatch, tracking the timer for cleanup on shutdown.
|
|
5
|
+
*
|
|
6
|
+
* No-ops once `context.stopping` is set — `server.stop()`'s timer-clearing
|
|
7
|
+
* disposer has already run by then, so arming a new timer here would leak a
|
|
8
|
+
* callback that fires against a stopped server instead of being cleared.
|
|
9
|
+
*/
|
|
4
10
|
export declare function scheduleDelayedDispatch(context: ServerContext, options: ServeOptions, task: TaskDispatch, delay: number): void;
|
|
5
11
|
export declare function resolveTaskPriority(_context: ServerContext, _options: ServeOptions, task: TaskDispatch): number | undefined;
|
|
6
12
|
export declare function dispatchTaskImpl(context: ServerContext, options: ServeOptions, task: TaskDispatch): Promise<boolean>;
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import { isJSONValue } from "../../core/json.js";
|
|
2
|
+
import { buildWorkerExecutionIdentity } from "../../worker/manifest/execution-identity.js";
|
|
1
3
|
import { evictOldestAffinityEntries } from "../runtime-helpers.js";
|
|
2
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
claimQueued,
|
|
6
|
+
createQueued
|
|
7
|
+
} from "../task-ledger-transitions.js";
|
|
8
|
+
import {
|
|
9
|
+
decodeRemoteTaskRecord,
|
|
10
|
+
REMOTE_TASK_RECORD_VERSION,
|
|
11
|
+
taskLedgerKey
|
|
12
|
+
} from "../task-ledger.js";
|
|
13
|
+
import { commitTaskLedgerTransition } from "./task-ledger-runtime.js";
|
|
3
14
|
import {
|
|
4
15
|
recordTaskBacklogMetric,
|
|
5
16
|
recordTaskQueueLatencyMetric,
|
|
@@ -12,6 +23,8 @@ function clampVisibilityTimeout(value) {
|
|
|
12
23
|
return Math.min(Math.max(value, MIN_VISIBILITY_TIMEOUT), MAX_VISIBILITY_TIMEOUT);
|
|
13
24
|
}
|
|
14
25
|
export function scheduleDelayedDispatch(context, options, task, delay) {
|
|
26
|
+
if (context.stopping)
|
|
27
|
+
return;
|
|
15
28
|
const timer = setTimeout(() => {
|
|
16
29
|
context.pendingTimers.delete(timer);
|
|
17
30
|
dispatchTaskImpl(context, options, task).catch((err) => console.error(`[weft] Delayed redispatch failed for "${task.operationId}":`, err));
|
|
@@ -52,6 +65,55 @@ function recordDispatchOutcome(context, task, workerId) {
|
|
|
52
65
|
operationIds.add(task.operationId);
|
|
53
66
|
context.operationToWorkflow.set(task.operationId, task.workflowId);
|
|
54
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
|
+
function buildCreateThenClaimTransition(createInput, claimInput) {
|
|
96
|
+
return (current, now) => {
|
|
97
|
+
let queuedRecord;
|
|
98
|
+
if (current === null) {
|
|
99
|
+
const created = createQueued(null, createInput, now);
|
|
100
|
+
if (!created.ok)
|
|
101
|
+
return created;
|
|
102
|
+
queuedRecord = created.nextRecord;
|
|
103
|
+
} else if (current.state === "queued")
|
|
104
|
+
queuedRecord = current;
|
|
105
|
+
else
|
|
106
|
+
return {
|
|
107
|
+
ok: !1,
|
|
108
|
+
reason: `operation "${createInput.operationId}" is not claimable from state "${current.state}"`
|
|
109
|
+
};
|
|
110
|
+
return claimQueued(queuedRecord, { ...claimInput, expectedGeneration: queuedRecord.generation }, now);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function bareActivityName(qualifiedActivityName) {
|
|
114
|
+
const dotIndex = qualifiedActivityName.indexOf(".");
|
|
115
|
+
return dotIndex === -1 ? qualifiedActivityName : qualifiedActivityName.slice(dotIndex + 1);
|
|
116
|
+
}
|
|
55
117
|
async function selectAndReserveWorker(context, options, task, queue, visibilityTimeout) {
|
|
56
118
|
const routingOptions = buildRoutingOptions(context, task, queue), worker = context.registry.findWorker(task.activityName, routingOptions);
|
|
57
119
|
if (!worker)
|
|
@@ -59,86 +121,85 @@ async function selectAndReserveWorker(context, options, task, queue, visibilityT
|
|
|
59
121
|
const ws = context.workerSockets.get(worker.id);
|
|
60
122
|
if (!ws)
|
|
61
123
|
return !1;
|
|
62
|
-
const
|
|
124
|
+
const createInput = buildCreateQueuedInput(task, queue, visibilityTimeout), attemptToken = crypto.randomUUID(), executionIdentity = buildWorkerExecutionIdentity({
|
|
125
|
+
manifest: worker.manifest,
|
|
126
|
+
manifestDigest: worker.acceptedManifestDigest,
|
|
127
|
+
workerId: worker.id,
|
|
128
|
+
workflowType: task.workflowType,
|
|
129
|
+
activityName: bareActivityName(task.activityName)
|
|
130
|
+
});
|
|
63
131
|
context.registry.assignTask(worker.id, task.operationId, visibilityTimeout, task.fairShareKey, attemptToken);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
132
|
+
let result;
|
|
133
|
+
try {
|
|
134
|
+
result = await commitTaskLedgerTransition(options.engine.storage, task.operationId, buildCreateThenClaimTransition(createInput, {
|
|
135
|
+
attemptToken,
|
|
136
|
+
workerSessionId: worker.id,
|
|
137
|
+
...executionIdentity !== void 0 ? { executionIdentity } : {},
|
|
138
|
+
leaseDurationMilliseconds: visibilityTimeout
|
|
139
|
+
}), 1);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
context.registry.releaseReservation(task.operationId);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
if (!result.ok) {
|
|
145
|
+
context.registry.releaseReservation(task.operationId);
|
|
146
|
+
return !1;
|
|
147
|
+
}
|
|
148
|
+
context.deadlineTracker.add({
|
|
67
149
|
operationId: task.operationId,
|
|
68
|
-
|
|
69
|
-
deadline,
|
|
70
|
-
activityName: task.activityName,
|
|
71
|
-
queue,
|
|
72
|
-
input: task.input,
|
|
73
|
-
attempt: task.attempt ?? 1,
|
|
74
|
-
visibilityTimeout,
|
|
75
|
-
retryPolicy: task.retryPolicy,
|
|
76
|
-
workflowId: task.workflowId,
|
|
77
|
-
workflowExecutionToken: task.workflowExecutionToken,
|
|
78
|
-
attemptToken
|
|
79
|
-
}, normalizedInflightRecord = await transitionQueuedToInflight(options.engine.storage, task.operationId, inflightRecord, {
|
|
80
|
-
queuedRecord: existingQueuedRecord,
|
|
81
|
-
now
|
|
150
|
+
deadline: result.record.leaseDeadline
|
|
82
151
|
});
|
|
83
152
|
ws.send(JSON.stringify({
|
|
84
153
|
type: "task",
|
|
85
154
|
operationId: task.operationId,
|
|
86
155
|
activityName: task.activityName,
|
|
87
156
|
input: task.input === void 0 ? null : task.input,
|
|
88
|
-
attempt:
|
|
157
|
+
attempt: result.record.attempt,
|
|
89
158
|
attemptToken,
|
|
90
159
|
...task.workflowExecutionToken !== void 0 && {
|
|
91
160
|
workflowExecutionToken: task.workflowExecutionToken
|
|
92
161
|
},
|
|
93
162
|
...task.headers ? { headers: task.headers } : {}
|
|
94
163
|
}));
|
|
95
|
-
recordTaskQueueLatencyMetric(context.metricsCollector,
|
|
164
|
+
recordTaskQueueLatencyMetric(context.metricsCollector, {
|
|
165
|
+
lastQueuedAt: result.record.lastQueuedAt,
|
|
166
|
+
lastDispatchedAt: Date.now()
|
|
167
|
+
});
|
|
96
168
|
recordWorkerCapacitySaturationMetric(context.metricsCollector, context.registry);
|
|
97
169
|
recordDispatchOutcome(context, task, worker.id);
|
|
98
170
|
return !0;
|
|
99
171
|
}
|
|
100
|
-
function mergeQueuedRecordLifecycle(fresh, existing) {
|
|
101
|
-
return {
|
|
102
|
-
...fresh,
|
|
103
|
-
firstQueuedAt: existing?.firstQueuedAt ?? fresh.queuedAt,
|
|
104
|
-
lastQueuedAt: fresh.queuedAt,
|
|
105
|
-
lastDispatchedAt: existing?.lastDispatchedAt,
|
|
106
|
-
startedAt: existing?.startedAt,
|
|
107
|
-
retryCount: existing?.retryCount ?? fresh.retryCount,
|
|
108
|
-
requeueCount: existing?.requeueCount ?? fresh.requeueCount,
|
|
109
|
-
lastRequeueReason: existing?.lastRequeueReason
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
172
|
async function enqueueTaskForLongPoll(context, options, task, queue, visibilityTimeout, resolvedPriority) {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
173
|
+
const storage = options.engine.storage, createInput = buildCreateQueuedInput(task, queue, visibilityTimeout), rawExisting = await storage.get(taskLedgerKey(task.operationId)), current = decodeRemoteTaskRecord(rawExisting);
|
|
174
|
+
let queuedRecord;
|
|
175
|
+
if (current === null) {
|
|
176
|
+
const result = await commitTaskLedgerTransition(storage, task.operationId, (freshCurrent, now) => createQueued(freshCurrent, createInput, now), 1);
|
|
177
|
+
if (!result.ok) {
|
|
178
|
+
const raced = decodeRemoteTaskRecord(await storage.get(taskLedgerKey(task.operationId)));
|
|
179
|
+
if (raced === null || raced.state !== "queued")
|
|
180
|
+
return !1;
|
|
181
|
+
queuedRecord = raced;
|
|
182
|
+
} else
|
|
183
|
+
queuedRecord = result.record;
|
|
184
|
+
} else if (current.state === "queued")
|
|
185
|
+
queuedRecord = current;
|
|
186
|
+
else
|
|
187
|
+
return !1;
|
|
188
|
+
const enqueued = context.taskQueue.enqueue(queue, {
|
|
127
189
|
operationId: task.operationId,
|
|
128
190
|
activityName: task.activityName,
|
|
129
191
|
input: task.input,
|
|
130
|
-
attempt,
|
|
192
|
+
attempt: queuedRecord.attempt,
|
|
131
193
|
retryPolicy: task.retryPolicy,
|
|
132
194
|
visibilityTimeout,
|
|
133
195
|
workflowId: task.workflowId,
|
|
134
196
|
workflowExecutionToken: task.workflowExecutionToken,
|
|
135
|
-
firstQueuedAt:
|
|
136
|
-
lastQueuedAt:
|
|
137
|
-
lastDispatchedAt:
|
|
138
|
-
startedAt:
|
|
139
|
-
retryCount:
|
|
140
|
-
requeueCount:
|
|
141
|
-
lastRequeueReason: normalizedQueuedRecord.lastRequeueReason,
|
|
197
|
+
firstQueuedAt: queuedRecord.firstQueuedAt,
|
|
198
|
+
lastQueuedAt: queuedRecord.lastQueuedAt,
|
|
199
|
+
lastDispatchedAt: queuedRecord.lastDispatchedAt,
|
|
200
|
+
startedAt: queuedRecord.startedAt,
|
|
201
|
+
retryCount: queuedRecord.retryCount,
|
|
202
|
+
requeueCount: queuedRecord.requeueCount,
|
|
142
203
|
...task.headers ? { headers: task.headers } : {},
|
|
143
204
|
...resolvedPriority !== void 0 ? { priority: resolvedPriority } : {}
|
|
144
205
|
});
|
|
@@ -146,6 +207,16 @@ async function enqueueTaskForLongPoll(context, options, task, queue, visibilityT
|
|
|
146
207
|
return enqueued;
|
|
147
208
|
}
|
|
148
209
|
export async function dispatchTaskImpl(context, options, task) {
|
|
210
|
+
try {
|
|
211
|
+
await context.taskLedgerRecovery.ready;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
throw Error(`Cannot dispatch task "${task.operationId}" \u2014 startup task-ledger recovery failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
214
|
+
}
|
|
215
|
+
if (!task.workflowType)
|
|
216
|
+
throw Error(`TaskDispatch for operation "${task.operationId}" is missing required field "workflowType".`);
|
|
217
|
+
const dotIndex = task.activityName.indexOf(".");
|
|
218
|
+
if (dotIndex !== -1 && task.activityName.slice(0, dotIndex) !== task.workflowType)
|
|
219
|
+
throw Error(`TaskDispatch for operation "${task.operationId}" has activityName "${task.activityName}" whose qualifier does not match workflowType "${task.workflowType}".`);
|
|
149
220
|
const queue = task.queue ?? "default", visibilityTimeout = clampVisibilityTimeout(task.visibilityTimeout), resolvedPriority = resolveTaskPriority(context, options, task);
|
|
150
221
|
if (!validateTaskEnvelope(context, task))
|
|
151
222
|
return !1;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared two-step commit for applying a worker's task result to the durable
|
|
3
|
+
* remote task ledger (WFT-22) — used by both the WebSocket (`onTaskResultMessage`)
|
|
4
|
+
* and long-poll (`handleTaskResultRequest`) completion paths.
|
|
5
|
+
*
|
|
6
|
+
* `Leased --> Completing --> Terminal` is committed as two separate durable
|
|
7
|
+
* writes, not one, even though the caller already holds the full result: the
|
|
8
|
+
* project brief models `Completing` as an observable intermediate state
|
|
9
|
+
* specifically so a crash between the two writes leaves a durable, recoverable
|
|
10
|
+
* marker ("Completing prevents visibility scanning or disconnect handling from
|
|
11
|
+
* requeueing an attempt while its terminal result is being applied") that
|
|
12
|
+
* WFT-23's recovery is designed to resume. Collapsing the two transitions into
|
|
13
|
+
* one write would make that state unobservable and defeat the point of having
|
|
14
|
+
* it.
|
|
15
|
+
*
|
|
16
|
+
* A submitter can itself resume that crash window: if the record is already
|
|
17
|
+
* `completing` with the same `attemptToken` AND the same `pendingResultDigest`
|
|
18
|
+
* — the worker retrying the identical result after a server crash between the
|
|
19
|
+
* two writes, or a benign duplicate submission — this skips straight to
|
|
20
|
+
* `commitTerminalResult` instead of rejecting a legitimate resubmission with
|
|
21
|
+
* "expected task state leased". A `completing` record whose digest or token
|
|
22
|
+
* differs is a genuinely different result and is rejected normally.
|
|
23
|
+
*
|
|
24
|
+
* On exhausted retries — specifically when the *second* write
|
|
25
|
+
* (`commitTerminalFromCompleting`) exhausts its CAS retry budget after the
|
|
26
|
+
* record is already durably `completing` — this attempts one best-effort
|
|
27
|
+
* `Completing --> DeadLettered` write (WFT-24's `commitDeadLetter`,
|
|
28
|
+
* `../task-ledger-transitions.ts`) so a sustained, operation-specific
|
|
29
|
+
* storage write failure becomes an operator-visible dead letter instead of a
|
|
30
|
+
* silently stuck record. If the *first* write (`beginCompletion`) exhausts
|
|
31
|
+
* instead, the record never left `leased` and no dead letter is attempted —
|
|
32
|
+
* the visibility scanner's ordinary expiry path already covers that case.
|
|
33
|
+
* If the dead-letter write itself also fails, this falls back to the plain
|
|
34
|
+
* `ok: false` result exactly as before WFT-24: the record stays `completing`,
|
|
35
|
+
* a worker resubmitting the identical result can still resume through the
|
|
36
|
+
* `resuming` branch above, and no data is lost.
|
|
37
|
+
*
|
|
38
|
+
* @module server/runtime/task-ledger-completion
|
|
39
|
+
*/
|
|
40
|
+
import type { Storage } from '../../storage/interface.ts';
|
|
41
|
+
import type { ServeOptions } from '../index.ts';
|
|
42
|
+
import { type RemoteTaskCompleting, type RemoteTaskDeadLettered, type RemoteTaskTerminal } from '../task-ledger.ts';
|
|
43
|
+
export type TaskLedgerCompletionInput = Readonly<{
|
|
44
|
+
operationId: string;
|
|
45
|
+
attemptToken: string;
|
|
46
|
+
status: 'completed' | 'failed';
|
|
47
|
+
value?: unknown;
|
|
48
|
+
error?: string;
|
|
49
|
+
}>;
|
|
50
|
+
export type TaskLedgerCompletionResult = Readonly<{
|
|
51
|
+
ok: true;
|
|
52
|
+
completing: RemoteTaskCompleting;
|
|
53
|
+
terminal: RemoteTaskTerminal;
|
|
54
|
+
}> | Readonly<{
|
|
55
|
+
ok: false;
|
|
56
|
+
reason: string;
|
|
57
|
+
deadLettered?: RemoteTaskDeadLettered;
|
|
58
|
+
}>;
|
|
59
|
+
/**
|
|
60
|
+
* Dispatch {@link TaskResultDeadLetteredEvent} for a record `commitTaskLedgerCompletion`
|
|
61
|
+
* dead-lettered. Shared by the WebSocket and long-poll completion paths so
|
|
62
|
+
* both build the event from the same fields — `workflowId`/`activityName`/`queue`
|
|
63
|
+
* come from the dead-lettered record itself (present on every `RemoteTaskBase`),
|
|
64
|
+
* not from the caller's original request, so the event is accurate even if the
|
|
65
|
+
* caller only had partial information.
|
|
66
|
+
*/
|
|
67
|
+
export declare function dispatchTaskDeadLetteredEvent(options: ServeOptions, operationId: string, deadLettered: RemoteTaskDeadLettered, workerId: string | undefined): void;
|
|
68
|
+
export declare function commitTaskLedgerCompletion(storage: Storage, input: TaskLedgerCompletionInput): Promise<TaskLedgerCompletionResult>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { TaskResultDeadLetteredEvent } from "../../core/events.js";
|
|
2
|
+
import { isJSONValue } from "../../core/json.js";
|
|
3
|
+
import { sha256Hex } from "../../worker/manifest/content-digest.js";
|
|
4
|
+
import {
|
|
5
|
+
beginCompletion,
|
|
6
|
+
commitDeadLetter,
|
|
7
|
+
commitTerminalResult
|
|
8
|
+
} from "../task-ledger-transitions.js";
|
|
9
|
+
import {
|
|
10
|
+
decodeRemoteTaskRecord,
|
|
11
|
+
taskLedgerKey
|
|
12
|
+
} from "../task-ledger.js";
|
|
13
|
+
import { commitTaskLedgerTransition } from "./task-ledger-runtime.js";
|
|
14
|
+
const COMPLETION_MAX_ATTEMPTS = 3, DEAD_LETTER_MAX_ATTEMPTS = 1;
|
|
15
|
+
async function pendingResultDigest(input) {
|
|
16
|
+
return sha256Hex(JSON.stringify({
|
|
17
|
+
status: input.status,
|
|
18
|
+
value: input.value ?? null,
|
|
19
|
+
error: input.error ?? null
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
function commitTerminalFromCompleting(storage, input, resultDigest) {
|
|
23
|
+
return commitTaskLedgerTransition(storage, input.operationId, (current, now) => commitTerminalResult(current, {
|
|
24
|
+
attemptToken: input.attemptToken,
|
|
25
|
+
resultDigest,
|
|
26
|
+
...input.status === "failed" && input.error !== void 0 ? { error: input.error } : {}
|
|
27
|
+
}, now), COMPLETION_MAX_ATTEMPTS);
|
|
28
|
+
}
|
|
29
|
+
async function attemptDeadLetter(storage, input, resultDigest, persistenceFailureReason) {
|
|
30
|
+
const deadLettered = await commitTaskLedgerTransition(storage, input.operationId, (current, now) => commitDeadLetter(current, {
|
|
31
|
+
attemptToken: input.attemptToken,
|
|
32
|
+
resultDigest,
|
|
33
|
+
...isJSONValue(input.value) ? { value: input.value } : {},
|
|
34
|
+
...input.error !== void 0 ? { error: input.error } : {},
|
|
35
|
+
persistenceFailureReason
|
|
36
|
+
}, now), DEAD_LETTER_MAX_ATTEMPTS);
|
|
37
|
+
return deadLettered.ok ? deadLettered.record : void 0;
|
|
38
|
+
}
|
|
39
|
+
export function dispatchTaskDeadLetteredEvent(options, operationId, deadLettered, workerId) {
|
|
40
|
+
options.engine.dispatchEvent(new TaskResultDeadLetteredEvent({
|
|
41
|
+
operationId,
|
|
42
|
+
workflowId: deadLettered.workflowId,
|
|
43
|
+
activityName: deadLettered.activityName,
|
|
44
|
+
queue: deadLettered.queue,
|
|
45
|
+
workerId,
|
|
46
|
+
errorMessage: deadLettered.persistenceFailureReason
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
export async function commitTaskLedgerCompletion(storage, input) {
|
|
50
|
+
const resultDigest = await pendingResultDigest(input), existing = decodeRemoteTaskRecord(await storage.get(taskLedgerKey(input.operationId)));
|
|
51
|
+
if (existing !== null && existing.state === "completing" && existing.attemptToken === input.attemptToken && existing.pendingResultDigest === resultDigest) {
|
|
52
|
+
const committed = await commitTerminalFromCompleting(storage, input, resultDigest);
|
|
53
|
+
if (!committed.ok) {
|
|
54
|
+
const deadLettered = await attemptDeadLetter(storage, input, resultDigest, committed.reason);
|
|
55
|
+
return { ...committed, ...deadLettered !== void 0 ? { deadLettered } : {} };
|
|
56
|
+
}
|
|
57
|
+
return { ok: !0, completing: existing, terminal: committed.record };
|
|
58
|
+
}
|
|
59
|
+
const begun = await commitTaskLedgerTransition(storage, input.operationId, (current) => beginCompletion(current, {
|
|
60
|
+
attemptToken: input.attemptToken,
|
|
61
|
+
pendingStatus: input.status,
|
|
62
|
+
pendingResultDigest: resultDigest
|
|
63
|
+
}), COMPLETION_MAX_ATTEMPTS);
|
|
64
|
+
if (!begun.ok)
|
|
65
|
+
return begun;
|
|
66
|
+
const committed = await commitTerminalFromCompleting(storage, input, resultDigest);
|
|
67
|
+
if (!committed.ok) {
|
|
68
|
+
const deadLettered = await attemptDeadLetter(storage, input, resultDigest, committed.reason);
|
|
69
|
+
return { ...committed, ...deadLettered !== void 0 ? { deadLettered } : {} };
|
|
70
|
+
}
|
|
71
|
+
return { ok: !0, completing: begun.record, terminal: committed.record };
|
|
72
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup task-ledger recovery (WFT-23).
|
|
3
|
+
*
|
|
4
|
+
* A one-shot full scan of the durable `task-ledger:` keyspace, run once per
|
|
5
|
+
* `serve()` call before the recovery gate on `ServerContext.taskLedgerRecovery`
|
|
6
|
+
* opens (see `buildServerContext` in `serve-internals.ts`). Reconstructs the
|
|
7
|
+
* in-memory registry, deadline tracker, and task-queue indexes that
|
|
8
|
+
* `restoreInflightTasks` used to rebuild from the retired `op:inflight:`
|
|
9
|
+
* keyspace — those indexes are process-local caches; the ledger is the
|
|
10
|
+
* durable authority, so nothing here writes state, only rehydrates memory to
|
|
11
|
+
* match what storage already says.
|
|
12
|
+
*
|
|
13
|
+
* One branch per non-terminal state:
|
|
14
|
+
* - `queued` — redispatch through the normal dispatch path so a lost
|
|
15
|
+
* `scheduleDelayedDispatch` timer (or a record this process
|
|
16
|
+
* never dispatched at all) still becomes runnable.
|
|
17
|
+
* - `leased`, deadline still in the future — rehydrate registry ownership
|
|
18
|
+
* and the deadline tracker so heartbeats and completions
|
|
19
|
+
* from the original worker are recognized after reconnect.
|
|
20
|
+
* - `leased`, deadline already passed — requeue or exhaust through the same
|
|
21
|
+
* `reassignOrExpireTask` CAS path `scanExpiredTasks` and
|
|
22
|
+
* `reconcileOrphanedRecords` use; deletion is never correct
|
|
23
|
+
* here (see `RequeueExpiredAttemptInput`'s contract).
|
|
24
|
+
* - `completing` — rehydrate registry ownership only. The pending result
|
|
25
|
+
* value isn't persisted (only its digest), so recovery
|
|
26
|
+
* cannot resolve this state itself; only the worker's
|
|
27
|
+
* redelivered `taskResult`, landing on
|
|
28
|
+
* `commitTaskLedgerCompletion`'s crash-resumption branch,
|
|
29
|
+
* can. Recovery's job is to make sure that redelivery still
|
|
30
|
+
* passes the WebSocket ownership guard after a restart.
|
|
31
|
+
* - `cancelling` — rehydrate registry ownership only, for the same reason:
|
|
32
|
+
* nothing in the current runtime calls `commitCancellation`
|
|
33
|
+
* yet (a future project's scope), so there is no action to
|
|
34
|
+
* resume, only ownership to preserve.
|
|
35
|
+
* - `terminal`, `deadLettered` — already resolved; skipped.
|
|
36
|
+
*
|
|
37
|
+
* A single corrupt or unrecognized record is logged and skipped — ordinary
|
|
38
|
+
* data hygiene, not a recovery failure. A failure in the scan itself (the
|
|
39
|
+
* storage iterator throwing) propagates and rejects the whole gate: per the
|
|
40
|
+
* project brief, "a scan failure leaves the remote worker plane unhealthy
|
|
41
|
+
* and prevents new claims rather than logging and continuing with partial
|
|
42
|
+
* indexes."
|
|
43
|
+
*
|
|
44
|
+
* @module server/runtime/task-ledger-recovery
|
|
45
|
+
*/
|
|
46
|
+
import type { ServeOptions } from '../index.ts';
|
|
47
|
+
import type { ServerContext } from './context.ts';
|
|
48
|
+
/**
|
|
49
|
+
* Run startup task-ledger recovery to completion. Resolves once every
|
|
50
|
+
* non-terminal record has been reconstructed; rejects if the scan itself
|
|
51
|
+
* failed. Callers gate task-plane entry points on the resulting promise
|
|
52
|
+
* rather than awaiting this directly from within another recovery-time
|
|
53
|
+
* scan — see `ServerContext.taskLedgerRecovery`'s doc comment.
|
|
54
|
+
*/
|
|
55
|
+
export declare function runTaskLedgerRecovery(context: ServerContext, options: ServeOptions): Promise<void>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeRemoteTaskRecord
|
|
3
|
+
} from "../task-ledger.js";
|
|
4
|
+
import { scheduleDelayedDispatch } from "./task-dispatch.js";
|
|
5
|
+
import { reassignOrExpireTask, taskDispatchFromLedgerRecord } from "./task-reconciliation.js";
|
|
6
|
+
function rebuildWorkflowIndex(context, operationId, workflowId) {
|
|
7
|
+
if (!workflowId)
|
|
8
|
+
return;
|
|
9
|
+
let operationIds = context.workflowOperations.get(workflowId);
|
|
10
|
+
if (!operationIds) {
|
|
11
|
+
operationIds = new Set;
|
|
12
|
+
context.workflowOperations.set(workflowId, operationIds);
|
|
13
|
+
}
|
|
14
|
+
operationIds.add(operationId);
|
|
15
|
+
context.operationToWorkflow.set(operationId, workflowId);
|
|
16
|
+
}
|
|
17
|
+
function rehydrateWorkerOwnership(context, record, now) {
|
|
18
|
+
const remaining = Math.max(0, record.leaseDeadline - now);
|
|
19
|
+
context.registry.assignTask(record.workerSessionId, record.operationId, remaining, record.fairShareKey, record.attemptToken);
|
|
20
|
+
const tracked = context.registry.getWorkerTasks(record.workerSessionId).find((task) => task.operationId === record.operationId);
|
|
21
|
+
if (tracked)
|
|
22
|
+
tracked.visibilityTimeout = record.visibilityTimeoutMilliseconds;
|
|
23
|
+
rebuildWorkflowIndex(context, record.operationId, record.workflowId);
|
|
24
|
+
}
|
|
25
|
+
async function recoverTaskLedgerRecord(context, options, decoded, now) {
|
|
26
|
+
switch (decoded.state) {
|
|
27
|
+
case "queued": {
|
|
28
|
+
const delay = Math.max(0, decoded.availableAt - now);
|
|
29
|
+
scheduleDelayedDispatch(context, options, taskDispatchFromLedgerRecord(decoded), delay);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
case "leased": {
|
|
33
|
+
if (decoded.leaseDeadline > now) {
|
|
34
|
+
rehydrateWorkerOwnership(context, decoded, now);
|
|
35
|
+
context.deadlineTracker.add({
|
|
36
|
+
operationId: decoded.operationId,
|
|
37
|
+
deadline: decoded.leaseDeadline
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
await reassignOrExpireTask(context, options, decoded.operationId, decoded);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
case "completing":
|
|
45
|
+
case "cancelling": {
|
|
46
|
+
rehydrateWorkerOwnership(context, decoded, now);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
case "terminal":
|
|
50
|
+
case "deadLettered":
|
|
51
|
+
return;
|
|
52
|
+
default: {
|
|
53
|
+
const exhaustive = decoded;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function runTaskLedgerRecovery(context, options) {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
for await (const [key, value] of options.engine.storage.scan("task-ledger:")) {
|
|
60
|
+
if (context.stopping)
|
|
61
|
+
return;
|
|
62
|
+
try {
|
|
63
|
+
const decoded = decodeRemoteTaskRecord(value);
|
|
64
|
+
if (decoded === null) {
|
|
65
|
+
console.error(`[weft] Unrecognized task ledger record at "${key}" during startup recovery \u2014 skipping`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
await recoverTaskLedgerRecord(context, options, decoded, now);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(`[weft] Failed to recover task ledger record at "${key}" \u2014 skipping:`, error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|