@dudousxd/nestjs-durable-core 0.78.0 → 0.80.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/dist/index.cjs +1260 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +507 -21
- package/dist/index.d.ts +507 -21
- package/dist/index.js +1237 -107
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
22
22
|
// src/index.ts
|
|
23
23
|
var index_exports = {};
|
|
24
24
|
__export(index_exports, {
|
|
25
|
+
CHILD_EDGE_PREFIXES: () => CHILD_EDGE_PREFIXES,
|
|
25
26
|
CONTROL_FLOW_SIGNAL: () => CONTROL_FLOW_SIGNAL,
|
|
26
27
|
CURRENT_PROTOCOL_VERSION: () => CURRENT_PROTOCOL_VERSION,
|
|
27
28
|
CodecStateStore: () => CodecStateStore,
|
|
@@ -42,6 +43,7 @@ __export(index_exports, {
|
|
|
42
43
|
FAN_SUMMARY_FAILURES: () => FAN_SUMMARY_FAILURES,
|
|
43
44
|
FatalError: () => FatalError,
|
|
44
45
|
GatherError: () => GatherError,
|
|
46
|
+
HANDOFF_SEQ_BASE: () => HANDOFF_SEQ_BASE,
|
|
45
47
|
InMemoryAdmissionBackend: () => InMemoryAdmissionBackend,
|
|
46
48
|
InMemoryStateStore: () => InMemoryStateStore,
|
|
47
49
|
InMemoryTransport: () => InMemoryTransport,
|
|
@@ -76,9 +78,11 @@ __export(index_exports, {
|
|
|
76
78
|
WorkflowEngine: () => WorkflowEngine,
|
|
77
79
|
WorkflowSuspended: () => WorkflowSuspended,
|
|
78
80
|
aggregateAnnouncements: () => aggregateAnnouncements,
|
|
81
|
+
aggregatesCheckpoints: () => aggregatesCheckpoints,
|
|
79
82
|
announcementsOf: () => announcementsOf,
|
|
80
83
|
applyAttributeQuery: () => applyAttributeQuery,
|
|
81
84
|
assertConcurrency: () => assertConcurrency,
|
|
85
|
+
assertRoutes: () => assertRoutes,
|
|
82
86
|
attributeColumnFor: () => attributeColumnFor,
|
|
83
87
|
attributeOperand: () => attributeOperand,
|
|
84
88
|
attributePredicateOperands: () => attributePredicateOperands,
|
|
@@ -88,10 +92,14 @@ __export(index_exports, {
|
|
|
88
92
|
buildWorkflowDirectory: () => buildWorkflowDirectory,
|
|
89
93
|
canRoute: () => canRoute,
|
|
90
94
|
capableWorkers: () => capableWorkers,
|
|
95
|
+
checkpointNameKind: () => checkpointNameKind,
|
|
96
|
+
checkpointStatsOf: () => checkpointStatsOf,
|
|
91
97
|
childIdsOf: () => childIdsOf,
|
|
92
98
|
classifyWaiterToken: () => classifyWaiterToken,
|
|
93
99
|
clearDurableExecutionWrappers: () => clearDurableExecutionWrappers,
|
|
94
100
|
clearTerminalSingletonAdmission: () => clearTerminalSingletonAdmission,
|
|
101
|
+
codeAndDetails: () => codeAndDetails,
|
|
102
|
+
codeOf: () => codeOf,
|
|
95
103
|
collectMetrics: () => collectMetrics,
|
|
96
104
|
concurrencyTag: () => concurrencyTag,
|
|
97
105
|
controlPlaneDescriptor: () => controlPlaneDescriptor,
|
|
@@ -104,13 +112,20 @@ __export(index_exports, {
|
|
|
104
112
|
debug: () => debug,
|
|
105
113
|
describeWorker: () => describeWorker,
|
|
106
114
|
descriptorHash: () => descriptorHash,
|
|
115
|
+
detailsOf: () => detailsOf,
|
|
107
116
|
error: () => error,
|
|
117
|
+
etagMatches: () => etagMatches,
|
|
108
118
|
eventMatchOf: () => eventMatchOf,
|
|
109
119
|
eventMatches: () => eventMatches,
|
|
110
120
|
eventNameOf: () => eventNameOf,
|
|
111
121
|
eventPrefix: () => eventPrefix,
|
|
112
122
|
eventToken: () => eventToken,
|
|
113
123
|
facetOrigin: () => facetOrigin,
|
|
124
|
+
fanPageBounds: () => fanPageBounds,
|
|
125
|
+
fanPageEtag: () => fanPageEtag,
|
|
126
|
+
foldedCount: () => foldedCount,
|
|
127
|
+
handoffHandlerId: () => handoffHandlerId,
|
|
128
|
+
handoffName: () => handoffName,
|
|
114
129
|
hasDurableExecutionWrappers: () => hasDurableExecutionWrappers,
|
|
115
130
|
heartbeatStatus: () => heartbeatStatus,
|
|
116
131
|
indexWaitersByRun: () => indexWaitersByRun,
|
|
@@ -122,6 +137,7 @@ __export(index_exports, {
|
|
|
122
137
|
isNonEmptyRunScope: () => isNonEmptyRunScope,
|
|
123
138
|
isWorkflowControlFlowSignal: () => isWorkflowControlFlowSignal,
|
|
124
139
|
listSignalWaitersOfRuns: () => listSignalWaitersOfRuns,
|
|
140
|
+
liveHandoff: () => liveHandoff,
|
|
125
141
|
log: () => log,
|
|
126
142
|
matchesAttributes: () => matchesAttributes,
|
|
127
143
|
mergeRunFacetRows: () => mergeRunFacetRows,
|
|
@@ -132,17 +148,23 @@ __export(index_exports, {
|
|
|
132
148
|
normalizeAttributeRows: () => normalizeAttributeRows,
|
|
133
149
|
normalizeDescriptor: () => normalizeDescriptor,
|
|
134
150
|
observedAnnouncements: () => observedAnnouncements,
|
|
151
|
+
onErrorRunId: () => onErrorRunId,
|
|
135
152
|
pageFan: () => pageFan,
|
|
153
|
+
pageFanFromStore: () => pageFanFromStore,
|
|
136
154
|
parentRunIdOf: () => parentRunIdOf,
|
|
137
155
|
parseDuration: () => parseDuration,
|
|
138
156
|
partitionOfToken: () => partitionOfToken,
|
|
139
157
|
planDispatch: () => planDispatch,
|
|
140
158
|
prevCronFireMs: () => prevCronFireMs,
|
|
159
|
+
queryCheckpointsOf: () => queryCheckpointsOf,
|
|
141
160
|
readSearchAttributes: () => readSearchAttributes,
|
|
142
161
|
requiredCapabilities: () => requiredCapabilities,
|
|
143
162
|
resolveConcurrency: () => resolveConcurrency,
|
|
144
163
|
resolveRouting: () => resolveRouting,
|
|
145
164
|
resolveRunWaiting: () => resolveRunWaiting,
|
|
165
|
+
retiredHandoffName: () => retiredHandoffName,
|
|
166
|
+
routeFor: () => routeFor,
|
|
167
|
+
runDetailEtag: () => runDetailEtag,
|
|
146
168
|
runDurableExecution: () => runDurableExecution,
|
|
147
169
|
runInStepLogger: () => runInStepLogger,
|
|
148
170
|
runInStepScope: () => runInStepScope,
|
|
@@ -163,6 +185,7 @@ __export(index_exports, {
|
|
|
163
185
|
subEvent: () => subEvent,
|
|
164
186
|
subProcess: () => subProcess,
|
|
165
187
|
summarizeFan: () => summarizeFan,
|
|
188
|
+
summarizeRunFromStore: () => summarizeRunFromStore,
|
|
166
189
|
summarizeTimeline: () => summarizeTimeline,
|
|
167
190
|
tenantGroup: () => tenantGroup,
|
|
168
191
|
useDurableExecution: () => useDurableExecution,
|
|
@@ -435,10 +458,16 @@ var FatalError = class extends Error {
|
|
|
435
458
|
__name(this, "FatalError");
|
|
436
459
|
}
|
|
437
460
|
code;
|
|
438
|
-
|
|
461
|
+
/** Structured, JSON-serializable detail (e.g. validation issues). It travels with the failure to
|
|
462
|
+
* the run's error, a parent's `allSettled` item and an onError handler's input. */
|
|
463
|
+
details;
|
|
464
|
+
/** Read by the step result builders, so a dispatched step's retry policy honours it too. */
|
|
465
|
+
retryable = false;
|
|
466
|
+
constructor(message, code, details) {
|
|
439
467
|
super(message);
|
|
440
468
|
this.name = "FatalError";
|
|
441
469
|
this.code = code;
|
|
470
|
+
this.details = details;
|
|
442
471
|
}
|
|
443
472
|
};
|
|
444
473
|
var ConcurrencyLimitError = class extends Error {
|
|
@@ -516,6 +545,8 @@ var NonDeterminismError = class extends Error {
|
|
|
516
545
|
static {
|
|
517
546
|
__name(this, "NonDeterminismError");
|
|
518
547
|
}
|
|
548
|
+
/** Marks the failure as a broken deploy, which an `onError` route never takes. */
|
|
549
|
+
code = "non_determinism";
|
|
519
550
|
runId;
|
|
520
551
|
seq;
|
|
521
552
|
constructor(runId, seq, expected, recorded) {
|
|
@@ -927,6 +958,9 @@ async function runStepHandler(task, handler) {
|
|
|
927
958
|
...typeof e?.code === "string" ? {
|
|
928
959
|
code: e.code
|
|
929
960
|
} : {},
|
|
961
|
+
...e?.details !== void 0 ? {
|
|
962
|
+
details: e.details
|
|
963
|
+
} : {},
|
|
930
964
|
...typeof e?.retryable === "boolean" ? {
|
|
931
965
|
retryable: e.retryable
|
|
932
966
|
} : {}
|
|
@@ -1003,11 +1037,36 @@ __name(isFailedCompletion, "isFailedCompletion");
|
|
|
1003
1037
|
function unwrapCompletion(payload, label) {
|
|
1004
1038
|
const c = payload;
|
|
1005
1039
|
if (c && typeof c === "object" && "ok" in c && c.ok === false) {
|
|
1006
|
-
throw new FatalError(`${label} failed: ${c.error}
|
|
1040
|
+
throw new FatalError(`${label} failed: ${c.error}`, c.code, c.details);
|
|
1007
1041
|
}
|
|
1008
1042
|
return c?.value;
|
|
1009
1043
|
}
|
|
1010
1044
|
__name(unwrapCompletion, "unwrapCompletion");
|
|
1045
|
+
function failedCompletion(error2) {
|
|
1046
|
+
return {
|
|
1047
|
+
ok: false,
|
|
1048
|
+
error: error2.message,
|
|
1049
|
+
...error2.code !== void 0 ? {
|
|
1050
|
+
code: error2.code
|
|
1051
|
+
} : {},
|
|
1052
|
+
...error2.details !== void 0 ? {
|
|
1053
|
+
details: error2.details
|
|
1054
|
+
} : {}
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
__name(failedCompletion, "failedCompletion");
|
|
1058
|
+
function completionError(c) {
|
|
1059
|
+
return {
|
|
1060
|
+
message: c.error,
|
|
1061
|
+
...c.code !== void 0 ? {
|
|
1062
|
+
code: c.code
|
|
1063
|
+
} : {},
|
|
1064
|
+
...c.details !== void 0 ? {
|
|
1065
|
+
details: c.details
|
|
1066
|
+
} : {}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
__name(completionError, "completionError");
|
|
1011
1070
|
|
|
1012
1071
|
// src/concurrency.ts
|
|
1013
1072
|
var DEFAULT_CONCURRENCY_STATUSES = [
|
|
@@ -1328,7 +1387,7 @@ function allWaves(size, concurrency) {
|
|
|
1328
1387
|
};
|
|
1329
1388
|
}
|
|
1330
1389
|
__name(allWaves, "allWaves");
|
|
1331
|
-
var ENGINE_CHILD_ID = /^(.+)\.(?:all\.\d+\.(?:w\d+\.)?\d+|child\.\d+)$/;
|
|
1390
|
+
var ENGINE_CHILD_ID = /^(.+)\.(?:all\.\d+\.(?:w\d+\.)?\d+|child\.\d+|onError\.\d+)$/;
|
|
1332
1391
|
function parentRunIdOf(runId) {
|
|
1333
1392
|
return ENGINE_CHILD_ID.exec(runId)?.[1];
|
|
1334
1393
|
}
|
|
@@ -1413,7 +1472,11 @@ function canonicalizeRegistrations(regs) {
|
|
|
1413
1472
|
requires: r.requires ? [
|
|
1414
1473
|
...new Set(r.requires)
|
|
1415
1474
|
].sort() : null,
|
|
1416
|
-
origin: r.origin ?? null
|
|
1475
|
+
origin: r.origin ?? null,
|
|
1476
|
+
// Conditional, so a registration that states no routes keeps the hash it had before routes existed.
|
|
1477
|
+
...r.onError !== void 0 ? {
|
|
1478
|
+
onError: canonicalRoutes(r.onError)
|
|
1479
|
+
} : {}
|
|
1417
1480
|
}));
|
|
1418
1481
|
const byJson = /* @__PURE__ */ new Map();
|
|
1419
1482
|
for (const entry of canonical) byJson.set(JSON.stringify(entry), entry);
|
|
@@ -1422,6 +1485,20 @@ function canonicalizeRegistrations(regs) {
|
|
|
1422
1485
|
].sort().map((k) => byJson.get(k));
|
|
1423
1486
|
}
|
|
1424
1487
|
__name(canonicalizeRegistrations, "canonicalizeRegistrations");
|
|
1488
|
+
function canonicalRoutes(routes) {
|
|
1489
|
+
const byJson = /* @__PURE__ */ new Map();
|
|
1490
|
+
for (const r of routes) {
|
|
1491
|
+
const entry = {
|
|
1492
|
+
code: r.code ?? null,
|
|
1493
|
+
workflow: r.workflow
|
|
1494
|
+
};
|
|
1495
|
+
byJson.set(JSON.stringify(entry), entry);
|
|
1496
|
+
}
|
|
1497
|
+
return [
|
|
1498
|
+
...byJson.keys()
|
|
1499
|
+
].sort().map((k) => byJson.get(k));
|
|
1500
|
+
}
|
|
1501
|
+
__name(canonicalRoutes, "canonicalRoutes");
|
|
1425
1502
|
function canonicalizeForHash(d) {
|
|
1426
1503
|
const set = /* @__PURE__ */ __name((xs) => [
|
|
1427
1504
|
...new Set(xs)
|
|
@@ -1456,13 +1533,13 @@ function fnv1a64Hex(input) {
|
|
|
1456
1533
|
const OFFSET = 0xcbf29ce484222325n;
|
|
1457
1534
|
const PRIME = 0x100000001b3n;
|
|
1458
1535
|
const MASK = 0xffffffffffffffffn;
|
|
1459
|
-
let
|
|
1536
|
+
let hash2 = OFFSET;
|
|
1460
1537
|
for (let i = 0; i < input.length; i += 1) {
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1538
|
+
hash2 ^= BigInt(input.charCodeAt(i) & 255);
|
|
1539
|
+
hash2 ^= BigInt(input.charCodeAt(i) >> 8 & 255);
|
|
1540
|
+
hash2 = hash2 * PRIME & MASK;
|
|
1464
1541
|
}
|
|
1465
|
-
return
|
|
1542
|
+
return hash2.toString(16).padStart(16, "0");
|
|
1466
1543
|
}
|
|
1467
1544
|
__name(fnv1a64Hex, "fnv1a64Hex");
|
|
1468
1545
|
function descriptorHash(descriptor) {
|
|
@@ -1578,6 +1655,20 @@ function aggregateAnnouncements(descriptors) {
|
|
|
1578
1655
|
const requires = [
|
|
1579
1656
|
...requiresByJson.values()
|
|
1580
1657
|
].sort((x, y) => x.join(",").localeCompare(y.join(",")));
|
|
1658
|
+
const routesByJson = /* @__PURE__ */ new Map();
|
|
1659
|
+
for (const a of announcements) {
|
|
1660
|
+
if (a.onError === void 0) continue;
|
|
1661
|
+
const canonical = canonicalRoutes(a.onError).map((r) => r.code === null ? {
|
|
1662
|
+
workflow: r.workflow
|
|
1663
|
+
} : {
|
|
1664
|
+
code: r.code,
|
|
1665
|
+
workflow: r.workflow
|
|
1666
|
+
});
|
|
1667
|
+
routesByJson.set(JSON.stringify(canonical), canonical);
|
|
1668
|
+
}
|
|
1669
|
+
const onError = [
|
|
1670
|
+
...routesByJson.keys()
|
|
1671
|
+
].sort().map((k) => routesByJson.get(k) ?? []);
|
|
1581
1672
|
const disagreements = [];
|
|
1582
1673
|
if (groups.length > 1) disagreements.push({
|
|
1583
1674
|
axis: "group",
|
|
@@ -1593,6 +1684,14 @@ function aggregateAnnouncements(descriptors) {
|
|
|
1593
1684
|
values: requires.map((r) => r.join(","))
|
|
1594
1685
|
});
|
|
1595
1686
|
}
|
|
1687
|
+
if (onError.length > 1) {
|
|
1688
|
+
disagreements.push({
|
|
1689
|
+
axis: "onError",
|
|
1690
|
+
values: [
|
|
1691
|
+
...routesByJson.keys()
|
|
1692
|
+
].sort()
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1596
1695
|
const first = announcements[0];
|
|
1597
1696
|
if (!first) continue;
|
|
1598
1697
|
out.push({
|
|
@@ -1605,6 +1704,7 @@ function aggregateAnnouncements(descriptors) {
|
|
|
1605
1704
|
groups,
|
|
1606
1705
|
origins,
|
|
1607
1706
|
requires,
|
|
1707
|
+
onError,
|
|
1608
1708
|
runtimes: [
|
|
1609
1709
|
...new Set(announcements.map((a) => a.runtime))
|
|
1610
1710
|
].sort(),
|
|
@@ -1638,6 +1738,7 @@ function observedAnnouncements(heartbeats, covered) {
|
|
|
1638
1738
|
],
|
|
1639
1739
|
origins: [],
|
|
1640
1740
|
requires: [],
|
|
1741
|
+
onError: [],
|
|
1641
1742
|
runtimes: [],
|
|
1642
1743
|
instances: [
|
|
1643
1744
|
...instances
|
|
@@ -1996,6 +2097,13 @@ function planDispatch(requires, liveDescriptors, cp, token) {
|
|
|
1996
2097
|
__name(planDispatch, "planDispatch");
|
|
1997
2098
|
|
|
1998
2099
|
// src/interfaces.ts
|
|
2100
|
+
function checkpointNameKind(name) {
|
|
2101
|
+
if (name.startsWith("signal:child:")) return "child";
|
|
2102
|
+
if (name.startsWith("fan:")) return "folded";
|
|
2103
|
+
if (name.startsWith("spawn:")) return "spawn";
|
|
2104
|
+
return "step";
|
|
2105
|
+
}
|
|
2106
|
+
__name(checkpointNameKind, "checkpointNameKind");
|
|
1999
2107
|
var TERMINAL_RUN_STATUSES = [
|
|
2000
2108
|
"completed",
|
|
2001
2109
|
"failed",
|
|
@@ -2008,6 +2116,71 @@ function isNonEmptyRunScope(scope) {
|
|
|
2008
2116
|
}
|
|
2009
2117
|
__name(isNonEmptyRunScope, "isNonEmptyRunScope");
|
|
2010
2118
|
|
|
2119
|
+
// src/on-error.ts
|
|
2120
|
+
var UNROUTABLE_CODES = /* @__PURE__ */ new Set([
|
|
2121
|
+
"non_determinism",
|
|
2122
|
+
"nested_workflow_call"
|
|
2123
|
+
]);
|
|
2124
|
+
var HANDOFF_SEQ_BASE = 1e9;
|
|
2125
|
+
var onErrorRunId = /* @__PURE__ */ __name((runId, attempt) => `${runId}.onError.${attempt}`, "onErrorRunId");
|
|
2126
|
+
var handoffName = /* @__PURE__ */ __name((handlerId) => `signal:child:${handlerId}`, "handoffName");
|
|
2127
|
+
var retiredHandoffName = /* @__PURE__ */ __name((handlerId) => `onError:retried:${handlerId}`, "retiredHandoffName");
|
|
2128
|
+
function handoffHandlerId(runId, cp) {
|
|
2129
|
+
const prefix = `signal:child:${runId}.onError.`;
|
|
2130
|
+
return cp.seq >= HANDOFF_SEQ_BASE && cp.name.startsWith(prefix) ? cp.name.slice("signal:child:".length) : void 0;
|
|
2131
|
+
}
|
|
2132
|
+
__name(handoffHandlerId, "handoffHandlerId");
|
|
2133
|
+
function liveHandoff(runId, checkpoints) {
|
|
2134
|
+
let live;
|
|
2135
|
+
for (const cp of checkpoints) {
|
|
2136
|
+
if (handoffHandlerId(runId, cp) !== void 0 && (!live || cp.seq > live.seq)) live = cp;
|
|
2137
|
+
}
|
|
2138
|
+
return live;
|
|
2139
|
+
}
|
|
2140
|
+
__name(liveHandoff, "liveHandoff");
|
|
2141
|
+
function routeFor(routes, error2) {
|
|
2142
|
+
if (!routes?.length || error2.code && UNROUTABLE_CODES.has(error2.code)) return void 0;
|
|
2143
|
+
return (error2.code ? routes.find((r) => r.code === error2.code) : void 0) ?? routes.find((r) => r.code === void 0);
|
|
2144
|
+
}
|
|
2145
|
+
__name(routeFor, "routeFor");
|
|
2146
|
+
function assertRoutes(workflow, routes) {
|
|
2147
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2148
|
+
for (const r of routes) {
|
|
2149
|
+
if (seen.has(r.code)) {
|
|
2150
|
+
throw new Error(r.code === void 0 ? `workflow ${workflow}: more than one onError route without a code` : `workflow ${workflow}: more than one onError route for code "${r.code}"`);
|
|
2151
|
+
}
|
|
2152
|
+
seen.add(r.code);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
__name(assertRoutes, "assertRoutes");
|
|
2156
|
+
function codeOf(err) {
|
|
2157
|
+
const e = err;
|
|
2158
|
+
return [
|
|
2159
|
+
e?.code,
|
|
2160
|
+
e?.stepError?.code,
|
|
2161
|
+
e?.error?.code
|
|
2162
|
+
].find((c) => typeof c === "string");
|
|
2163
|
+
}
|
|
2164
|
+
__name(codeOf, "codeOf");
|
|
2165
|
+
function detailsOf(err) {
|
|
2166
|
+
const e = err;
|
|
2167
|
+
return e?.details ?? e?.stepError?.details ?? e?.error?.details;
|
|
2168
|
+
}
|
|
2169
|
+
__name(detailsOf, "detailsOf");
|
|
2170
|
+
function codeAndDetails(err) {
|
|
2171
|
+
const code = codeOf(err);
|
|
2172
|
+
const details = detailsOf(err);
|
|
2173
|
+
return {
|
|
2174
|
+
...code !== void 0 ? {
|
|
2175
|
+
code
|
|
2176
|
+
} : {},
|
|
2177
|
+
...details !== void 0 ? {
|
|
2178
|
+
details
|
|
2179
|
+
} : {}
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
__name(codeAndDetails, "codeAndDetails");
|
|
2183
|
+
|
|
2011
2184
|
// src/tenant-group.ts
|
|
2012
2185
|
function tenantGroup(baseGroup, tenant) {
|
|
2013
2186
|
return tenant !== void 0 && tenant !== "" && tenant !== "default" ? `${baseGroup}@${tenant}` : baseGroup;
|
|
@@ -3067,7 +3240,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3067
3240
|
kind: "local",
|
|
3068
3241
|
error: {
|
|
3069
3242
|
message: err instanceof Error ? err.message : String(err),
|
|
3070
|
-
stack: err instanceof Error ? err.stack : void 0
|
|
3243
|
+
stack: err instanceof Error ? err.stack : void 0,
|
|
3244
|
+
...codeAndDetails(err)
|
|
3071
3245
|
},
|
|
3072
3246
|
events,
|
|
3073
3247
|
attempts: attempt,
|
|
@@ -3323,12 +3497,12 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3323
3497
|
}
|
|
3324
3498
|
]));
|
|
3325
3499
|
}, "child");
|
|
3326
|
-
const
|
|
3500
|
+
const fan = /* @__PURE__ */ __name(async (workflow2, inputs, opts, settle) => {
|
|
3327
3501
|
if (inputs.length === 0) return [];
|
|
3328
3502
|
if (stepNameOf(workflow2) !== void 0) {
|
|
3329
|
-
return allSteps(workflow2, inputs, opts);
|
|
3503
|
+
return allSteps(workflow2, inputs, opts, settle);
|
|
3330
3504
|
}
|
|
3331
|
-
const mode = opts?.mode ?? "waitAll";
|
|
3505
|
+
const mode = settle ? "waitAll" : opts?.mode ?? "waitAll";
|
|
3332
3506
|
const name = workflowName(workflow2);
|
|
3333
3507
|
const waves = allWaves(inputs.length, opts?.concurrency);
|
|
3334
3508
|
const positions = inputs.map(() => pos.next());
|
|
@@ -3450,24 +3624,45 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3450
3624
|
throw new WorkflowSuspended(void 0, fanJoin(active.map((k) => allWait(runId, firstSeq, waves ? k : void 0))));
|
|
3451
3625
|
}
|
|
3452
3626
|
const outputs = [];
|
|
3627
|
+
const results = [];
|
|
3453
3628
|
const failures = [];
|
|
3454
3629
|
for (let i = 0; i < inputs.length; i += 1) {
|
|
3455
3630
|
const c = existing[i]?.output;
|
|
3456
3631
|
if (c && typeof c === "object" && "ok" in c && c.ok === false) {
|
|
3632
|
+
const { message, ...rest } = completionError({
|
|
3633
|
+
...c,
|
|
3634
|
+
error: c.error ?? "unknown"
|
|
3635
|
+
});
|
|
3636
|
+
const failure = {
|
|
3637
|
+
id: id(i),
|
|
3638
|
+
error: message,
|
|
3639
|
+
...rest
|
|
3640
|
+
};
|
|
3457
3641
|
failures.push({
|
|
3458
3642
|
index: i,
|
|
3459
|
-
|
|
3460
|
-
|
|
3643
|
+
...failure
|
|
3644
|
+
});
|
|
3645
|
+
results.push({
|
|
3646
|
+
status: "rejected",
|
|
3647
|
+
...failure
|
|
3461
3648
|
});
|
|
3462
3649
|
outputs.push(void 0);
|
|
3463
3650
|
} else {
|
|
3464
|
-
|
|
3651
|
+
const value = c?.value;
|
|
3652
|
+
results.push({
|
|
3653
|
+
status: "fulfilled",
|
|
3654
|
+
value
|
|
3655
|
+
});
|
|
3656
|
+
outputs.push(value);
|
|
3465
3657
|
}
|
|
3466
3658
|
}
|
|
3659
|
+
if (settle) return results;
|
|
3467
3660
|
if (failures.length > 0) throw new GatherError(failures, inputs.length);
|
|
3468
3661
|
return outputs;
|
|
3469
|
-
}, "
|
|
3470
|
-
const
|
|
3662
|
+
}, "fan");
|
|
3663
|
+
const all = /* @__PURE__ */ __name((workflow2, inputs, opts) => fan(workflow2, inputs, opts, false), "all");
|
|
3664
|
+
const allSettled = /* @__PURE__ */ __name((workflow2, inputs, opts) => fan(workflow2, inputs, opts, true), "allSettled");
|
|
3665
|
+
const allSteps = /* @__PURE__ */ __name(async (ref, inputs, opts, settle) => {
|
|
3471
3666
|
if (opts?.queue !== void 0) {
|
|
3472
3667
|
throw new Error("ctx.all: a flow-control queue is not supported on a step fan");
|
|
3473
3668
|
}
|
|
@@ -3478,7 +3673,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3478
3673
|
};
|
|
3479
3674
|
const positions = inputs.map(() => pos.next());
|
|
3480
3675
|
return host.callRemoteAll(runId, positions, def, inputs, {
|
|
3481
|
-
mode: opts?.mode ?? "waitAll",
|
|
3676
|
+
mode: settle ? "waitAll" : opts?.mode ?? "waitAll",
|
|
3677
|
+
settle,
|
|
3482
3678
|
concurrency: opts?.concurrency,
|
|
3483
3679
|
transport: opts?.transport,
|
|
3484
3680
|
admission: {
|
|
@@ -3796,6 +3992,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3796
3992
|
task: guard("task", task),
|
|
3797
3993
|
child: guard("child", child),
|
|
3798
3994
|
all: guard("all", all),
|
|
3995
|
+
allSettled: guard("allSettled", allSettled),
|
|
3799
3996
|
startChild: guard("startChild", startChild),
|
|
3800
3997
|
breakpoint: guard("breakpoint", breakpoint),
|
|
3801
3998
|
webhook: guardedWebhook,
|
|
@@ -3821,9 +4018,11 @@ function mergeTags(staticTags, runTags) {
|
|
|
3821
4018
|
__name(mergeTags, "mergeTags");
|
|
3822
4019
|
var BREAKPOINT = "breakpoint";
|
|
3823
4020
|
var isBreakpoint = /* @__PURE__ */ __name((cp) => cp.status === "pending" && cp.name.startsWith(BREAKPOINT), "isBreakpoint");
|
|
4021
|
+
var ANNOUNCED_ROUTES_TTL_MS = 5e3;
|
|
3824
4022
|
var VERSION_UNDECLARED_TAG = "version:undeclared";
|
|
3825
4023
|
var versionKey = /* @__PURE__ */ __name((name, version) => `${name}@${version}`, "versionKey");
|
|
3826
4024
|
var STEP_FAN_CHUNK = 200;
|
|
4025
|
+
var RESULT_BATCH_MAX = 256;
|
|
3827
4026
|
function shuffle(items) {
|
|
3828
4027
|
for (let i = items.length - 1; i > 0; i -= 1) {
|
|
3829
4028
|
const j = Math.floor(Math.random() * (i + 1));
|
|
@@ -3853,6 +4052,16 @@ function isNewerVersion(a, b) {
|
|
|
3853
4052
|
}) > 0;
|
|
3854
4053
|
}
|
|
3855
4054
|
__name(isNewerVersion, "isNewerVersion");
|
|
4055
|
+
var SETTLE_ELIGIBLE_STATUSES = [
|
|
4056
|
+
"pending",
|
|
4057
|
+
"running",
|
|
4058
|
+
"suspended",
|
|
4059
|
+
"blocked",
|
|
4060
|
+
"failed",
|
|
4061
|
+
"cancelling"
|
|
4062
|
+
];
|
|
4063
|
+
var SUSPEND_ELIGIBLE_STATUSES = SETTLE_ELIGIBLE_STATUSES.filter((status) => status !== "cancelling");
|
|
4064
|
+
var RECOVERY_BATCH = 500;
|
|
3856
4065
|
var NamespaceMismatch = class NamespaceMismatch2 extends Error {
|
|
3857
4066
|
static {
|
|
3858
4067
|
__name(this, "NamespaceMismatch");
|
|
@@ -3931,6 +4140,8 @@ var WorkflowEngine = class {
|
|
|
3931
4140
|
updateValidators = /* @__PURE__ */ new Map();
|
|
3932
4141
|
/** Runs being cancelled WITH saga compensation — see `cancel({ compensate: true })`. */
|
|
3933
4142
|
cancelRequested = /* @__PURE__ */ new Set();
|
|
4143
|
+
/** {@link announcedRoutes} reads, by group + workflow + version. */
|
|
4144
|
+
announcedRoutesCache = /* @__PURE__ */ new Map();
|
|
3934
4145
|
/**
|
|
3935
4146
|
* Seqs of the dispatched remote steps a turn suspended on, by run, while that turn executes on
|
|
3936
4147
|
* THIS instance — so the turn can re-check them once its lease is released (see
|
|
@@ -3950,8 +4161,22 @@ var WorkflowEngine = class {
|
|
|
3950
4161
|
awaitedStepFans = /* @__PURE__ */ new Map();
|
|
3951
4162
|
/** Step fans whose join barrier this instance opened, by `runId:firstSeq` — see {@link settledHere}. */
|
|
3952
4163
|
stepFansFiredHere = /* @__PURE__ */ new Map();
|
|
4164
|
+
/**
|
|
4165
|
+
* The in-process execution gate, by run id: the execution of the run currently in flight in THIS
|
|
4166
|
+
* process (`tail`), and at most one resume queued behind it (`queued`). See {@link resumeRun}.
|
|
4167
|
+
*
|
|
4168
|
+
* The run lease is keyed by engine instance, so it fences OTHER processes only: to the store, a
|
|
4169
|
+
* second execution in this same process holds the very same lease (and {@link execute} skips
|
|
4170
|
+
* `tryLockRun` when its snapshot says this instance holds it). Without this gate, a resume landing
|
|
4171
|
+
* while this process was still executing the run (a signal, a late step result, a child wake, an
|
|
4172
|
+
* explicit `resume()`) went straight past the lease and ran the body concurrently: a
|
|
4173
|
+
* not-yet-checkpointed step executed twice.
|
|
4174
|
+
*/
|
|
4175
|
+
runGates = /* @__PURE__ */ new Map();
|
|
3953
4176
|
/** The tail of each run's queue of results being handled on this instance — see {@link serializedPerRun}. */
|
|
3954
4177
|
resultChains = /* @__PURE__ */ new Map();
|
|
4178
|
+
/** Each run's results waiting to be settled on this instance — see {@link completeRemoteResult}. */
|
|
4179
|
+
resultQueues = /* @__PURE__ */ new Map();
|
|
3955
4180
|
/** Flow-control admission backend for remote steps (see {@link registerQueue}). */
|
|
3956
4181
|
admission;
|
|
3957
4182
|
/** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
|
|
@@ -4158,6 +4383,7 @@ var WorkflowEngine = class {
|
|
|
4158
4383
|
name,
|
|
4159
4384
|
version,
|
|
4160
4385
|
fn,
|
|
4386
|
+
onError: this.checkedRoutes(name, opts?.onError),
|
|
4161
4387
|
tags: opts?.tags,
|
|
4162
4388
|
singleton: opts?.singleton,
|
|
4163
4389
|
concurrency: opts?.concurrency,
|
|
@@ -4227,6 +4453,7 @@ var WorkflowEngine = class {
|
|
|
4227
4453
|
executionTimeoutMs: opts.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
|
|
4228
4454
|
validateInput: opts.validateInput,
|
|
4229
4455
|
requires: opts.requires,
|
|
4456
|
+
onError: this.checkedRoutes(name, opts.onError),
|
|
4230
4457
|
remote: {
|
|
4231
4458
|
group: opts.group,
|
|
4232
4459
|
executor: opts.executor
|
|
@@ -4414,11 +4641,16 @@ var WorkflowEngine = class {
|
|
|
4414
4641
|
if (registered.singleton) {
|
|
4415
4642
|
await this.singletons.assertCapacity(name, registered.singleton, input);
|
|
4416
4643
|
}
|
|
4644
|
+
const startAtMs = opts?.startAt instanceof Date ? opts.startAt.getTime() : opts?.startAt;
|
|
4645
|
+
const delayed = startAtMs !== void 0 && Number.isFinite(startAtMs) && startAtMs > this.clock();
|
|
4417
4646
|
const run = {
|
|
4418
4647
|
id: runId,
|
|
4419
4648
|
workflow: name,
|
|
4420
4649
|
workflowVersion: registered.version,
|
|
4421
|
-
status: "pending",
|
|
4650
|
+
status: delayed ? "suspended" : "pending",
|
|
4651
|
+
...delayed ? {
|
|
4652
|
+
wakeAt: startAtMs
|
|
4653
|
+
} : {},
|
|
4422
4654
|
namespace: opts?.namespace ?? this.namespace,
|
|
4423
4655
|
// DERIVED from the registration, exactly like `namespace` is derived from this engine — there is
|
|
4424
4656
|
// deliberately no `opts.origin`, so a caller cannot label its run as another package's work.
|
|
@@ -4444,6 +4676,18 @@ var WorkflowEngine = class {
|
|
|
4444
4676
|
error: winner.error
|
|
4445
4677
|
};
|
|
4446
4678
|
}
|
|
4679
|
+
if (delayed) {
|
|
4680
|
+
this.emit({
|
|
4681
|
+
type: "run.suspended",
|
|
4682
|
+
runId,
|
|
4683
|
+
workflow: name,
|
|
4684
|
+
namespace: run.namespace
|
|
4685
|
+
});
|
|
4686
|
+
return {
|
|
4687
|
+
runId,
|
|
4688
|
+
status: "suspended"
|
|
4689
|
+
};
|
|
4690
|
+
}
|
|
4447
4691
|
await this.runDispatcher.dispatch(runId);
|
|
4448
4692
|
if (this.controlPlane) {
|
|
4449
4693
|
void this.controlPlane.publishControl({
|
|
@@ -4481,7 +4725,104 @@ var WorkflowEngine = class {
|
|
|
4481
4725
|
resumeImplicit(runId) {
|
|
4482
4726
|
return this.resumeRun(runId, false);
|
|
4483
4727
|
}
|
|
4484
|
-
|
|
4728
|
+
/**
|
|
4729
|
+
* One execution of a run at a time in this process. A resume that lands while the run executes
|
|
4730
|
+
* here neither joins that execution (the double-run) nor no-ops against its held lease (a lost
|
|
4731
|
+
* wake: its checkpoint may postdate the running turn's snapshot): it is QUEUED, and re-drives the
|
|
4732
|
+
* run once the running execution has settled and released its lease, re-reading the run then
|
|
4733
|
+
* (terminal: echoed, never re-executed). Resumes landing while one is already queued coalesce
|
|
4734
|
+
* into it: it has not started yet, so it will observe every write they made. Their callers get
|
|
4735
|
+
* the queued execution's result, i.e. the run's state after their own wake was applied.
|
|
4736
|
+
*
|
|
4737
|
+
* Across processes, exclusion stays the lease's job: {@link execute} still `tryLockRun`s when it
|
|
4738
|
+
* does not hold the lease.
|
|
4739
|
+
*/
|
|
4740
|
+
resumeRun(runId, allowFailed) {
|
|
4741
|
+
const gate = this.runGates.get(runId);
|
|
4742
|
+
if (!gate) {
|
|
4743
|
+
const tail = this.resumeRunNow(runId, allowFailed);
|
|
4744
|
+
const fresh = {
|
|
4745
|
+
tail
|
|
4746
|
+
};
|
|
4747
|
+
this.runGates.set(runId, fresh);
|
|
4748
|
+
this.closeGateAfter(runId, fresh, tail);
|
|
4749
|
+
return tail;
|
|
4750
|
+
}
|
|
4751
|
+
let queued = gate.queued;
|
|
4752
|
+
if (queued) {
|
|
4753
|
+
queued.allowFailed ||= allowFailed;
|
|
4754
|
+
} else {
|
|
4755
|
+
const entry = {
|
|
4756
|
+
allowFailed
|
|
4757
|
+
};
|
|
4758
|
+
const result = gate.tail.catch(() => void 0).then(() => {
|
|
4759
|
+
if (gate.queued === entry) gate.queued = void 0;
|
|
4760
|
+
return this.resumeRunNow(runId, entry.allowFailed);
|
|
4761
|
+
});
|
|
4762
|
+
entry.result = result;
|
|
4763
|
+
queued = entry;
|
|
4764
|
+
gate.queued = queued;
|
|
4765
|
+
gate.tail = result;
|
|
4766
|
+
this.closeGateAfter(runId, gate, result);
|
|
4767
|
+
void this.track(result).catch(() => void 0);
|
|
4768
|
+
}
|
|
4769
|
+
if (currentWorkflowCtx()?.runId === runId) return this.echoCurrentStatus(runId);
|
|
4770
|
+
return queued.result;
|
|
4771
|
+
}
|
|
4772
|
+
/** Drop `gate` once `tail` settles, unless another execution queued behind it meanwhile. */
|
|
4773
|
+
closeGateAfter(runId, gate, tail) {
|
|
4774
|
+
const close = /* @__PURE__ */ __name(() => {
|
|
4775
|
+
if (this.runGates.get(runId) === gate && gate.tail === tail) this.runGates.delete(runId);
|
|
4776
|
+
}, "close");
|
|
4777
|
+
void tail.then(close, close);
|
|
4778
|
+
}
|
|
4779
|
+
/**
|
|
4780
|
+
* Release a run's lease AFTER its outcome is decided (persisted, or about to be returned). Best
|
|
4781
|
+
* effort, and that matters most on the failure path: the thing that failed the run is frequently
|
|
4782
|
+
* the very store this release talks to (a step that violated a DB constraint leaves Postgres
|
|
4783
|
+
* refusing every further statement on that connection), so the release throws too. A throwing
|
|
4784
|
+
* release in a `finally` REPLACES the run's real outcome with a lease write nobody asked about,
|
|
4785
|
+
* skips the post-turn re-drives after it, and, for a turn driven as a background resume, surfaces
|
|
4786
|
+
* as an unhandled rejection instead of a failed run.
|
|
4787
|
+
*
|
|
4788
|
+
* Letting it go is safe: the lease expires on its own and `recoverIncomplete` re-drives whatever
|
|
4789
|
+
* is still held. A lost release costs one lease window of latency; a masked error costs the
|
|
4790
|
+
* diagnosis.
|
|
4791
|
+
*/
|
|
4792
|
+
async releaseLeaseBestEffort(runId) {
|
|
4793
|
+
try {
|
|
4794
|
+
await this.store.releaseRunLock(runId);
|
|
4795
|
+
} catch (err) {
|
|
4796
|
+
console.warn(`[nestjs-durable] releasing the lease of run ${runId} failed (it expires on its own and recovery re-drives): ${err instanceof Error ? err.message : String(err)}`);
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
/**
|
|
4800
|
+
* The run's persisted state as a {@link RunResult}, without executing anything: the answer to a
|
|
4801
|
+
* resume issued from inside the run's own execution, and to a settle whose compare-and-set lost
|
|
4802
|
+
* (the path that won already emitted and notified). A run that is gone reads as `cancelled`, the
|
|
4803
|
+
* safest terminal reading, never a fabricated success.
|
|
4804
|
+
*/
|
|
4805
|
+
async echoCurrentStatus(runId) {
|
|
4806
|
+
const run = await this.store.getRun(runId);
|
|
4807
|
+
return {
|
|
4808
|
+
runId,
|
|
4809
|
+
status: run?.status ?? "cancelled",
|
|
4810
|
+
output: run?.output,
|
|
4811
|
+
error: run?.error
|
|
4812
|
+
};
|
|
4813
|
+
}
|
|
4814
|
+
/**
|
|
4815
|
+
* The engine's stand-in for {@link StateStore.updateRunIf} on a store without it, called with the
|
|
4816
|
+
* store as `this`: read-then-write, which narrows the race to the gap between the read and the
|
|
4817
|
+
* write (the pre-CAS behavior, now applied to every settle) but cannot close it.
|
|
4818
|
+
*/
|
|
4819
|
+
async updateRunIfFallback(runId, expected, patch) {
|
|
4820
|
+
const current = await this.getRun(runId);
|
|
4821
|
+
if (!current || !expected.includes(current.status)) return false;
|
|
4822
|
+
await this.updateRun(runId, patch);
|
|
4823
|
+
return true;
|
|
4824
|
+
}
|
|
4825
|
+
async resumeRunNow(runId, allowFailed) {
|
|
4485
4826
|
const run = await this.store.getRun(runId);
|
|
4486
4827
|
if (!run) throw new Error(`run ${runId} not found`);
|
|
4487
4828
|
if (run.status === "cancelled" || run.status === "completed" || run.status === "dead" || run.status === "failed" && !allowFailed) {
|
|
@@ -4787,7 +5128,7 @@ var WorkflowEngine = class {
|
|
|
4787
5128
|
async recoverIncomplete(nowMs = this.clock()) {
|
|
4788
5129
|
if (this.draining) return [];
|
|
4789
5130
|
const results = [];
|
|
4790
|
-
for (const listed of await this.
|
|
5131
|
+
for (const listed of await this.orphanedRuns(nowMs)) {
|
|
4791
5132
|
if (this.draining) break;
|
|
4792
5133
|
const acquired = await this.store.tryLockRun(listed.id, this.instanceId, nowMs + this.leaseMs, nowMs);
|
|
4793
5134
|
if (!acquired) continue;
|
|
@@ -4833,6 +5174,21 @@ var WorkflowEngine = class {
|
|
|
4833
5174
|
return results;
|
|
4834
5175
|
}
|
|
4835
5176
|
/**
|
|
5177
|
+
* The runs {@link recoverIncomplete} should try to reclaim: incomplete runs whose lease is free or
|
|
5178
|
+
* expired, never the healthy ones a live turn is renewing. Every worker runs recovery on every
|
|
5179
|
+
* timer tick, so listing every running run and lock-probing each (one doomed UPDATE per healthy
|
|
5180
|
+
* run per worker per tick) was pure database load. The store answers it directly when it can
|
|
5181
|
+
* ({@link StateStore.listOrphanedRuns}, bounded by {@link RECOVERY_BATCH}); else the full list is
|
|
5182
|
+
* filtered here, which still spares the probes. Shuffled, like the due-timer pass, so instances
|
|
5183
|
+
* recovering at once spread over the batch instead of contending for the same leases.
|
|
5184
|
+
*/
|
|
5185
|
+
async orphanedRuns(nowMs) {
|
|
5186
|
+
if (this.store.listOrphanedRuns) {
|
|
5187
|
+
return shuffle(await this.store.listOrphanedRuns(nowMs, RECOVERY_BATCH, this.namespace));
|
|
5188
|
+
}
|
|
5189
|
+
return shuffle((await this.store.listIncompleteRuns(this.namespace)).filter((run) => run.lockedUntil == null || run.lockedUntil <= nowMs));
|
|
5190
|
+
}
|
|
5191
|
+
/**
|
|
4836
5192
|
* The remote step this run dispatched and is still awaiting a result for, if any — the signature
|
|
4837
5193
|
* that tells {@link recoverIncomplete} "this run's work is ON THE TRANSPORT, not in a crashed
|
|
4838
5194
|
* process". A `pending` remote checkpoint is written BEFORE the dispatch and settled by the
|
|
@@ -5004,6 +5360,24 @@ var WorkflowEngine = class {
|
|
|
5004
5360
|
if (resurrecting) {
|
|
5005
5361
|
const isFailureCompletion = /* @__PURE__ */ __name((v) => typeof v === "object" && v !== null && v.ok === false, "isFailureCompletion");
|
|
5006
5362
|
for (const cp of await this.store.listCheckpoints(runId)) {
|
|
5363
|
+
const handlerId = handoffHandlerId(runId, cp);
|
|
5364
|
+
if (handlerId !== void 0) {
|
|
5365
|
+
await this.store.takeSignalWaiter(`child:${handlerId}`);
|
|
5366
|
+
await this.store.saveCheckpoint({
|
|
5367
|
+
...cp,
|
|
5368
|
+
name: retiredHandoffName(handlerId),
|
|
5369
|
+
status: "completed"
|
|
5370
|
+
});
|
|
5371
|
+
const tags = run.tags?.filter((t) => !t.startsWith("onError:"));
|
|
5372
|
+
if (tags && tags.length !== run.tags?.length) {
|
|
5373
|
+
await this.store.updateRun(runId, {
|
|
5374
|
+
tags,
|
|
5375
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
5376
|
+
});
|
|
5377
|
+
run.tags = tags;
|
|
5378
|
+
}
|
|
5379
|
+
continue;
|
|
5380
|
+
}
|
|
5007
5381
|
if (cp.status === "failed") {
|
|
5008
5382
|
await this.store.saveCheckpoint({
|
|
5009
5383
|
...cp,
|
|
@@ -5148,13 +5522,13 @@ var WorkflowEngine = class {
|
|
|
5148
5522
|
* `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
|
|
5149
5523
|
*/
|
|
5150
5524
|
waitForRun(runId, opts) {
|
|
5151
|
-
const
|
|
5525
|
+
const TERMINAL2 = /* @__PURE__ */ new Set([
|
|
5152
5526
|
"completed",
|
|
5153
5527
|
"failed",
|
|
5154
5528
|
"cancelled",
|
|
5155
5529
|
"dead"
|
|
5156
5530
|
]);
|
|
5157
|
-
const isSettled = /* @__PURE__ */ __name((run) => opts?.until === "terminal" ?
|
|
5531
|
+
const isSettled = /* @__PURE__ */ __name((run) => opts?.until === "terminal" ? TERMINAL2.has(run.status) : run.status !== "pending" && run.status !== "running" && run.status !== "cancelling" && !(run.status === "suspended" && run.awaitingDecisionTaskId !== void 0), "isSettled");
|
|
5158
5532
|
const toResult = /* @__PURE__ */ __name((run) => ({
|
|
5159
5533
|
runId,
|
|
5160
5534
|
status: run.status,
|
|
@@ -5409,10 +5783,7 @@ var WorkflowEngine = class {
|
|
|
5409
5783
|
async redriveSettledStepFans(runId, fans) {
|
|
5410
5784
|
for (const fan of fans) {
|
|
5411
5785
|
if (this.stepFansFiredHere.has(`${runId}:${fan.from}`)) continue;
|
|
5412
|
-
|
|
5413
|
-
"pending"
|
|
5414
|
-
]) : (await this.store.listCheckpoints(runId)).filter((c) => c.seq >= fan.from && c.seq <= fan.to && c.status === "pending").length;
|
|
5415
|
-
if (pending > 0) continue;
|
|
5786
|
+
if (await this.stepFanPending(runId, fan.from, fan.to) > 0) continue;
|
|
5416
5787
|
const nowMs = this.clock();
|
|
5417
5788
|
if (!await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs)) {
|
|
5418
5789
|
return;
|
|
@@ -6122,6 +6493,209 @@ var WorkflowEngine = class {
|
|
|
6122
6493
|
* us back here (a remote step finished, a timer fired) goes through `resume` like any TS workflow.
|
|
6123
6494
|
*/
|
|
6124
6495
|
/**
|
|
6496
|
+
* The registered onError routes whose handler workflow nothing can run: not registered on this
|
|
6497
|
+
* engine, and no live worker group serves it. Meant for a boot-time warning: a typo in a handler's
|
|
6498
|
+
* name would otherwise surface only when the first run fails.
|
|
6499
|
+
*/
|
|
6500
|
+
async unservedOnErrorRoutes() {
|
|
6501
|
+
const unserved = [];
|
|
6502
|
+
let groups;
|
|
6503
|
+
for (const registered of this.workflows.values()) {
|
|
6504
|
+
for (const route of registered.onError ?? []) {
|
|
6505
|
+
if (this.latest.has(route.workflow)) continue;
|
|
6506
|
+
groups ??= await this.pool.listWorkerGroups().catch(() => []);
|
|
6507
|
+
const token = sanitizeQueueToken(route.workflow);
|
|
6508
|
+
if (groups.some((g) => g === token || g.startsWith(`${token}@`))) continue;
|
|
6509
|
+
unserved.push({
|
|
6510
|
+
workflow: registered.name,
|
|
6511
|
+
route
|
|
6512
|
+
});
|
|
6513
|
+
}
|
|
6514
|
+
}
|
|
6515
|
+
return unserved;
|
|
6516
|
+
}
|
|
6517
|
+
/** Validate a workflow's {@link OnErrorRoute}s at registration. */
|
|
6518
|
+
checkedRoutes(name, routes) {
|
|
6519
|
+
if (!routes?.length) return void 0;
|
|
6520
|
+
assertRoutes(name, routes);
|
|
6521
|
+
return routes;
|
|
6522
|
+
}
|
|
6523
|
+
/**
|
|
6524
|
+
* Route a failed run to its {@link OnErrorRoute} handler, read from the workflow's local registration
|
|
6525
|
+
* or, for one served by convention, from what its live workers announce. Starts the handler run and
|
|
6526
|
+
* waits for it the way a parent waits for a `ctx.child`: a `child:<handlerId>` waiter plus a
|
|
6527
|
+
* `running` placeholder checkpoint carrying the handler's name and input, so the handler's terminal
|
|
6528
|
+
* state wakes the run (buffered and reconciled like any child). From then on every execution of the
|
|
6529
|
+
* run only follows the handler ({@link followHandoff}); the body never runs again. Undefined when no
|
|
6530
|
+
* route matches: the run fails as usual.
|
|
6531
|
+
*
|
|
6532
|
+
* The placeholder sits past every seq the body used and is left out of {@link remoteHistory}.
|
|
6533
|
+
*/
|
|
6534
|
+
async routeFailure(run, error2) {
|
|
6535
|
+
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion));
|
|
6536
|
+
const routes = registered ? registered.onError : await this.announcedRoutes(run, error2);
|
|
6537
|
+
const route = routeFor(routes, error2);
|
|
6538
|
+
if (!route) return void 0;
|
|
6539
|
+
const checkpoints = await this.handoffCheckpoints(run.id);
|
|
6540
|
+
const live = liveHandoff(run.id, checkpoints);
|
|
6541
|
+
if (live) return this.followHandoff(run, live);
|
|
6542
|
+
const attempt = checkpoints.length;
|
|
6543
|
+
const seq = HANDOFF_SEQ_BASE + attempt;
|
|
6544
|
+
const handlerId = onErrorRunId(run.id, attempt);
|
|
6545
|
+
const handler = {
|
|
6546
|
+
workflow: route.workflow,
|
|
6547
|
+
input: {
|
|
6548
|
+
runId: run.id,
|
|
6549
|
+
workflow: run.workflow,
|
|
6550
|
+
input: run.input,
|
|
6551
|
+
error: error2
|
|
6552
|
+
}
|
|
6553
|
+
};
|
|
6554
|
+
await this.store.putSignalWaiter({
|
|
6555
|
+
token: `child:${handlerId}`,
|
|
6556
|
+
runId: run.id,
|
|
6557
|
+
seq
|
|
6558
|
+
});
|
|
6559
|
+
await this.store.saveCheckpoint({
|
|
6560
|
+
...instantCheckpoint({
|
|
6561
|
+
runId: run.id,
|
|
6562
|
+
seq,
|
|
6563
|
+
name: handoffName(handlerId),
|
|
6564
|
+
kind: "signal",
|
|
6565
|
+
status: "running"
|
|
6566
|
+
}),
|
|
6567
|
+
input: handler
|
|
6568
|
+
});
|
|
6569
|
+
const tag = `onError:${route.code ?? "*"}`;
|
|
6570
|
+
if (!run.tags?.includes(tag)) {
|
|
6571
|
+
const tags = [
|
|
6572
|
+
...run.tags ?? [],
|
|
6573
|
+
tag
|
|
6574
|
+
];
|
|
6575
|
+
await this.store.updateRun(run.id, {
|
|
6576
|
+
tags,
|
|
6577
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6578
|
+
});
|
|
6579
|
+
run.tags = tags;
|
|
6580
|
+
}
|
|
6581
|
+
this.emit({
|
|
6582
|
+
type: "run.routed",
|
|
6583
|
+
runId: run.id,
|
|
6584
|
+
workflow: run.workflow,
|
|
6585
|
+
namespace: run.namespace,
|
|
6586
|
+
name: route.workflow,
|
|
6587
|
+
error: error2
|
|
6588
|
+
});
|
|
6589
|
+
this.startChildDeferred(handler.workflow, handler.input, handlerId, {
|
|
6590
|
+
namespace: run.namespace
|
|
6591
|
+
});
|
|
6592
|
+
return {
|
|
6593
|
+
kind: "suspended"
|
|
6594
|
+
};
|
|
6595
|
+
}
|
|
6596
|
+
/** A run's handoff checkpoints, live and retired: the only ones at {@link HANDOFF_SEQ_BASE} and up. */
|
|
6597
|
+
async handoffCheckpoints(runId) {
|
|
6598
|
+
const handoffs = await this.store.listCheckpointsByNamePrefix?.(runId, [
|
|
6599
|
+
`signal:child:${runId}.onError.`,
|
|
6600
|
+
`onError:retried:${runId}.onError.`
|
|
6601
|
+
]);
|
|
6602
|
+
return (handoffs ?? await this.store.listCheckpoints(runId)).filter((cp) => cp.seq >= HANDOFF_SEQ_BASE);
|
|
6603
|
+
}
|
|
6604
|
+
/**
|
|
6605
|
+
* Where a run that handed its failure off stands: the handler's outcome once it settled (from the
|
|
6606
|
+
* wake's checkpoint, or off the handler run when the wake never arrived), else still waiting. Starts
|
|
6607
|
+
* the handler if the turn that handed off crashed before its deferred start ran.
|
|
6608
|
+
*/
|
|
6609
|
+
async followHandoff(run, handoff) {
|
|
6610
|
+
if (handoff.status === "completed") {
|
|
6611
|
+
const outcome = handoff.output;
|
|
6612
|
+
return outcome.ok ? {
|
|
6613
|
+
kind: "completed",
|
|
6614
|
+
output: outcome.value
|
|
6615
|
+
} : {
|
|
6616
|
+
kind: "failed",
|
|
6617
|
+
error: completionError(outcome)
|
|
6618
|
+
};
|
|
6619
|
+
}
|
|
6620
|
+
const handlerId = handoffHandlerId(run.id, handoff);
|
|
6621
|
+
const handlerRun = await this.store.getRun(handlerId);
|
|
6622
|
+
if (handlerRun?.status === "completed") return {
|
|
6623
|
+
kind: "completed",
|
|
6624
|
+
output: handlerRun.output
|
|
6625
|
+
};
|
|
6626
|
+
if (handlerRun && TERMINAL_RUN_STATUSES.includes(handlerRun.status)) {
|
|
6627
|
+
return {
|
|
6628
|
+
kind: "failed",
|
|
6629
|
+
error: handlerRun.error ?? {
|
|
6630
|
+
message: `onError handler ${handlerId} ${handlerRun.status}`
|
|
6631
|
+
}
|
|
6632
|
+
};
|
|
6633
|
+
}
|
|
6634
|
+
if (!handlerRun) {
|
|
6635
|
+
const handler = handoff.input;
|
|
6636
|
+
this.startChildDeferred(handler.workflow, handler.input, handlerId, {
|
|
6637
|
+
namespace: run.namespace
|
|
6638
|
+
});
|
|
6639
|
+
}
|
|
6640
|
+
return {
|
|
6641
|
+
kind: "suspended"
|
|
6642
|
+
};
|
|
6643
|
+
}
|
|
6644
|
+
/**
|
|
6645
|
+
* The onError routes the live workers announce for a workflow the engine serves by convention: the
|
|
6646
|
+
* one set they agree on, or none when they state none or cannot be read. Workers that announce
|
|
6647
|
+
* different sets (a rollout in progress) route nothing, and the run's failure is reported as a
|
|
6648
|
+
* `run.unrouted` event. Announcers of another version of the workflow are ignored. The read is
|
|
6649
|
+
* cached for {@link ANNOUNCED_ROUTES_TTL_MS}, so a burst of failures costs one descriptor read.
|
|
6650
|
+
*/
|
|
6651
|
+
async announcedRoutes(run, error2) {
|
|
6652
|
+
const group = tenantGroup(sanitizeQueueToken(run.workflow), run.namespace);
|
|
6653
|
+
const key = `${group}
|
|
6654
|
+
${run.workflow}
|
|
6655
|
+
${run.workflowVersion}`;
|
|
6656
|
+
const now = this.clock();
|
|
6657
|
+
let cached = this.announcedRoutesCache.get(key);
|
|
6658
|
+
if (!cached || now - cached.at > ANNOUNCED_ROUTES_TTL_MS) {
|
|
6659
|
+
cached = {
|
|
6660
|
+
at: now,
|
|
6661
|
+
sets: await this.readAnnouncedRoutes(run, group)
|
|
6662
|
+
};
|
|
6663
|
+
this.announcedRoutesCache.set(key, cached);
|
|
6664
|
+
}
|
|
6665
|
+
if (cached.sets.length > 1) {
|
|
6666
|
+
this.emit({
|
|
6667
|
+
type: "run.unrouted",
|
|
6668
|
+
runId: run.id,
|
|
6669
|
+
workflow: run.workflow,
|
|
6670
|
+
namespace: run.namespace,
|
|
6671
|
+
error: error2,
|
|
6672
|
+
reason: `live workers announce ${cached.sets.length} different onError route sets`
|
|
6673
|
+
});
|
|
6674
|
+
return void 0;
|
|
6675
|
+
}
|
|
6676
|
+
return cached.sets[0];
|
|
6677
|
+
}
|
|
6678
|
+
/** Every distinct onError route set the live workers of `group` announce for the run's workflow. */
|
|
6679
|
+
async readAnnouncedRoutes(run, group) {
|
|
6680
|
+
const transport = await this.pool.transportWithLiveGroup(group);
|
|
6681
|
+
if (!transport?.readWorkerDescriptors) return [];
|
|
6682
|
+
let descriptors;
|
|
6683
|
+
try {
|
|
6684
|
+
descriptors = await transport.readWorkerDescriptors(group);
|
|
6685
|
+
} catch {
|
|
6686
|
+
return [];
|
|
6687
|
+
}
|
|
6688
|
+
const sets = /* @__PURE__ */ new Map();
|
|
6689
|
+
for (const announced of aggregateAnnouncements(descriptors)) {
|
|
6690
|
+
if (announced.name !== run.workflow) continue;
|
|
6691
|
+
if (announced.version !== void 0 && announced.version !== run.workflowVersion) continue;
|
|
6692
|
+
for (const routes of announced.onError) sets.set(JSON.stringify(routes), routes);
|
|
6693
|
+
}
|
|
6694
|
+
return [
|
|
6695
|
+
...sets.values()
|
|
6696
|
+
];
|
|
6697
|
+
}
|
|
6698
|
+
/**
|
|
6125
6699
|
* Apply a terminal/suspended transition once: persist the new run status, emit the matching
|
|
6126
6700
|
* lifecycle event, and (on completion/failure) wake a waiting parent. Both the TS and remote
|
|
6127
6701
|
* executors funnel their completed/failed/suspended outcomes through here so a status update can
|
|
@@ -6131,12 +6705,13 @@ var WorkflowEngine = class {
|
|
|
6131
6705
|
async settleRun(run, outcome) {
|
|
6132
6706
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
6133
6707
|
if (outcome.kind === "completed") {
|
|
6134
|
-
await this.store.
|
|
6708
|
+
const applied2 = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SETTLE_ELIGIBLE_STATUSES, {
|
|
6135
6709
|
status: "completed",
|
|
6136
6710
|
output: outcome.output,
|
|
6137
6711
|
error: void 0,
|
|
6138
6712
|
updatedAt
|
|
6139
6713
|
});
|
|
6714
|
+
if (!applied2) return this.echoCurrentStatus(run.id);
|
|
6140
6715
|
this.emit({
|
|
6141
6716
|
type: "run.completed",
|
|
6142
6717
|
runId: run.id,
|
|
@@ -6155,42 +6730,36 @@ var WorkflowEngine = class {
|
|
|
6155
6730
|
};
|
|
6156
6731
|
}
|
|
6157
6732
|
if (outcome.kind === "failed") {
|
|
6158
|
-
await this.
|
|
6733
|
+
const handed = await this.routeFailure(run, outcome.error);
|
|
6734
|
+
if (handed && handed.kind !== "failed") return this.settleRun(run, handed);
|
|
6735
|
+
const error2 = handed?.error ?? outcome.error;
|
|
6736
|
+
const applied2 = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SETTLE_ELIGIBLE_STATUSES, {
|
|
6159
6737
|
status: "failed",
|
|
6160
|
-
error:
|
|
6738
|
+
error: error2,
|
|
6161
6739
|
updatedAt
|
|
6162
6740
|
});
|
|
6741
|
+
if (!applied2) return this.echoCurrentStatus(run.id);
|
|
6163
6742
|
this.emit({
|
|
6164
6743
|
type: "run.failed",
|
|
6165
6744
|
runId: run.id,
|
|
6166
6745
|
workflow: run.workflow,
|
|
6167
6746
|
namespace: run.namespace,
|
|
6168
|
-
error:
|
|
6169
|
-
});
|
|
6170
|
-
void this.notifyParent(run.id, {
|
|
6171
|
-
ok: false,
|
|
6172
|
-
error: outcome.error.message
|
|
6747
|
+
error: error2
|
|
6173
6748
|
});
|
|
6749
|
+
void this.notifyParent(run.id, failedCompletion(error2));
|
|
6174
6750
|
return {
|
|
6175
6751
|
runId: run.id,
|
|
6176
6752
|
status: "failed",
|
|
6177
|
-
error:
|
|
6753
|
+
error: error2
|
|
6178
6754
|
};
|
|
6179
6755
|
}
|
|
6180
6756
|
if (outcome.kind === "blocked") return this.parkBlocked(run, outcome.blocked);
|
|
6181
|
-
const
|
|
6182
|
-
if (latest?.status === "cancelled") {
|
|
6183
|
-
return {
|
|
6184
|
-
runId: run.id,
|
|
6185
|
-
status: "cancelled",
|
|
6186
|
-
error: latest.error
|
|
6187
|
-
};
|
|
6188
|
-
}
|
|
6189
|
-
await this.store.updateRun(run.id, {
|
|
6757
|
+
const applied = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SUSPEND_ELIGIBLE_STATUSES, {
|
|
6190
6758
|
status: "suspended",
|
|
6191
6759
|
wakeAt: this.reconcileWakeAt(outcome.wakeAt),
|
|
6192
6760
|
updatedAt
|
|
6193
6761
|
});
|
|
6762
|
+
if (!applied) return this.echoCurrentStatus(run.id);
|
|
6194
6763
|
this.emit({
|
|
6195
6764
|
type: "run.suspended",
|
|
6196
6765
|
runId: run.id,
|
|
@@ -6237,7 +6806,7 @@ var WorkflowEngine = class {
|
|
|
6237
6806
|
from: this.instanceId
|
|
6238
6807
|
}).catch(() => void 0);
|
|
6239
6808
|
}
|
|
6240
|
-
await this.
|
|
6809
|
+
await this.releaseLeaseBestEffort(run.id);
|
|
6241
6810
|
return {
|
|
6242
6811
|
runId: run.id,
|
|
6243
6812
|
status: "cancelled",
|
|
@@ -6263,7 +6832,14 @@ var WorkflowEngine = class {
|
|
|
6263
6832
|
}
|
|
6264
6833
|
const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
|
|
6265
6834
|
if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
|
|
6266
|
-
const
|
|
6835
|
+
const checkpoints = await this.store.listCheckpoints(run.id);
|
|
6836
|
+
const handoff = liveHandoff(run.id, checkpoints);
|
|
6837
|
+
if (handoff) {
|
|
6838
|
+
const result = await this.settleRun(run, await this.followHandoff(run, handoff));
|
|
6839
|
+
await this.releaseLeaseBestEffort(run.id);
|
|
6840
|
+
return result;
|
|
6841
|
+
}
|
|
6842
|
+
const history = await this.remoteHistory(run.id, checkpoints);
|
|
6267
6843
|
if (remote.executor.dispatch) {
|
|
6268
6844
|
const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
|
|
6269
6845
|
const silenceMs = this.remoteAdvanceSilenceMs;
|
|
@@ -6298,7 +6874,7 @@ var WorkflowEngine = class {
|
|
|
6298
6874
|
decision = silenceMs != null ? await this.awaitWithLivenessDeadline(run.id, advance, silenceMs, () => new RemoteWorkflowTimeout(run.id, silenceMs)) : await advance;
|
|
6299
6875
|
} catch (err) {
|
|
6300
6876
|
if (err instanceof RemoteWorkflowTimeout) {
|
|
6301
|
-
await this.
|
|
6877
|
+
await this.releaseLeaseBestEffort(run.id);
|
|
6302
6878
|
return {
|
|
6303
6879
|
runId: run.id,
|
|
6304
6880
|
status: "running"
|
|
@@ -6587,8 +7163,8 @@ var WorkflowEngine = class {
|
|
|
6587
7163
|
}
|
|
6588
7164
|
}
|
|
6589
7165
|
/** The run's resolved durable ops as replay inputs: completed/failed steps + elapsed timers. */
|
|
6590
|
-
async remoteHistory(runId) {
|
|
6591
|
-
const checkpoints = await this.store.listCheckpoints(runId);
|
|
7166
|
+
async remoteHistory(runId, listed) {
|
|
7167
|
+
const checkpoints = listed ?? await this.store.listCheckpoints(runId);
|
|
6592
7168
|
const kindOf = {
|
|
6593
7169
|
remote: "call",
|
|
6594
7170
|
local: "step",
|
|
@@ -6598,15 +7174,14 @@ var WorkflowEngine = class {
|
|
|
6598
7174
|
const events = [];
|
|
6599
7175
|
for (const cp of checkpoints) {
|
|
6600
7176
|
if (cp.status === "completed" || cp.status === "failed") {
|
|
7177
|
+
if (cp.seq >= HANDOFF_SEQ_BASE) continue;
|
|
6601
7178
|
if (cp.kind === "signal" && cp.name.startsWith("signal:child:")) {
|
|
6602
7179
|
const completion = cp.output;
|
|
6603
7180
|
events.push({
|
|
6604
7181
|
seq: cp.seq,
|
|
6605
7182
|
kind: "child",
|
|
6606
7183
|
output: completion?.ok ? completion.value : void 0,
|
|
6607
|
-
error: completion && completion.ok === false ?
|
|
6608
|
-
message: completion.error
|
|
6609
|
-
} : void 0
|
|
7184
|
+
error: completion && completion.ok === false ? completionError(completion) : void 0
|
|
6610
7185
|
});
|
|
6611
7186
|
continue;
|
|
6612
7187
|
}
|
|
@@ -6887,6 +7462,10 @@ var WorkflowEngine = class {
|
|
|
6887
7462
|
this.awaitedStepFans.set(run.id, awaitedFans);
|
|
6888
7463
|
const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow, registered?.searchAttributesSchema);
|
|
6889
7464
|
try {
|
|
7465
|
+
const handoff = liveHandoff(run.id, snapshot);
|
|
7466
|
+
if (handoff && !this.cancelRequested.has(run.id)) {
|
|
7467
|
+
return await this.settleRun(run, await this.followHandoff(run, handoff));
|
|
7468
|
+
}
|
|
6890
7469
|
const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
|
|
6891
7470
|
if (this.cancelRequested.has(run.id)) {
|
|
6892
7471
|
return await this.compensateAndCancel(run, compensations, replay);
|
|
@@ -6949,7 +7528,8 @@ var WorkflowEngine = class {
|
|
|
6949
7528
|
}
|
|
6950
7529
|
const error2 = {
|
|
6951
7530
|
message: err instanceof Error ? err.message : String(err),
|
|
6952
|
-
stack: err instanceof Error ? err.stack : void 0
|
|
7531
|
+
stack: err instanceof Error ? err.stack : void 0,
|
|
7532
|
+
...codeAndDetails(err)
|
|
6953
7533
|
};
|
|
6954
7534
|
const pending = await this.unwindCompensations(run, compensations, replay);
|
|
6955
7535
|
if (pending) return await this.settleRun(run, {
|
|
@@ -6965,7 +7545,7 @@ var WorkflowEngine = class {
|
|
|
6965
7545
|
this.awaitedRemoteSteps.delete(run.id);
|
|
6966
7546
|
}
|
|
6967
7547
|
if (this.awaitedStepFans.get(run.id) === awaitedFans) this.awaitedStepFans.delete(run.id);
|
|
6968
|
-
await this.
|
|
7548
|
+
await this.releaseLeaseBestEffort(run.id);
|
|
6969
7549
|
if (suspendedOnFan) {
|
|
6970
7550
|
void this.redriveSettledChildren(run.id, suspendedOnFan).catch(() => void 0);
|
|
6971
7551
|
}
|
|
@@ -7336,27 +7916,21 @@ var WorkflowEngine = class {
|
|
|
7336
7916
|
* own `finally` also releases (the run-execution path) is safe.
|
|
7337
7917
|
*/
|
|
7338
7918
|
async parkBlocked(run, blocked) {
|
|
7339
|
-
const current = await this.store.getRun(run.id);
|
|
7340
|
-
if (current && (current.status === "cancelled" || current.status === "completed")) {
|
|
7341
|
-
await this.store.releaseRunLock(run.id);
|
|
7342
|
-
return {
|
|
7343
|
-
runId: run.id,
|
|
7344
|
-
status: current.status,
|
|
7345
|
-
output: current.output,
|
|
7346
|
-
error: current.error
|
|
7347
|
-
};
|
|
7348
|
-
}
|
|
7349
7919
|
const error2 = {
|
|
7350
7920
|
message: blocked.reason,
|
|
7351
7921
|
code: blocked.code,
|
|
7352
7922
|
retryable: true
|
|
7353
7923
|
};
|
|
7354
|
-
await this.store.
|
|
7924
|
+
const applied = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SUSPEND_ELIGIBLE_STATUSES, {
|
|
7355
7925
|
status: "blocked",
|
|
7356
7926
|
error: error2,
|
|
7357
7927
|
wakeAt: this.clock() + this.blockedPollMs,
|
|
7358
7928
|
updatedAt: /* @__PURE__ */ new Date()
|
|
7359
7929
|
});
|
|
7930
|
+
if (!applied) {
|
|
7931
|
+
await this.store.releaseRunLock(run.id);
|
|
7932
|
+
return this.echoCurrentStatus(run.id);
|
|
7933
|
+
}
|
|
7360
7934
|
this.emit({
|
|
7361
7935
|
type: "run.blocked",
|
|
7362
7936
|
runId: run.id,
|
|
@@ -7585,7 +8159,8 @@ var WorkflowEngine = class {
|
|
|
7585
8159
|
const failure = {
|
|
7586
8160
|
index: i,
|
|
7587
8161
|
id: stepId(runId, seq),
|
|
7588
|
-
error: err.message
|
|
8162
|
+
error: err.message,
|
|
8163
|
+
...codeAndDetails(err)
|
|
7589
8164
|
};
|
|
7590
8165
|
if (opts.mode === "failFast") throw new GatherError([
|
|
7591
8166
|
failure
|
|
@@ -7607,6 +8182,24 @@ var WorkflowEngine = class {
|
|
|
7607
8182
|
});
|
|
7608
8183
|
throw new WorkflowSuspended(wakeAt);
|
|
7609
8184
|
}
|
|
8185
|
+
if (opts.settle) {
|
|
8186
|
+
const failed = new Map(failures.map((f) => [
|
|
8187
|
+
f.index,
|
|
8188
|
+
f
|
|
8189
|
+
]));
|
|
8190
|
+
return Array.from(outputs, (value, i) => {
|
|
8191
|
+
const f = failed.get(i);
|
|
8192
|
+
if (!f) return {
|
|
8193
|
+
status: "fulfilled",
|
|
8194
|
+
value
|
|
8195
|
+
};
|
|
8196
|
+
const { index: _index, ...rejected } = f;
|
|
8197
|
+
return {
|
|
8198
|
+
status: "rejected",
|
|
8199
|
+
...rejected
|
|
8200
|
+
};
|
|
8201
|
+
});
|
|
8202
|
+
}
|
|
7610
8203
|
if (failures.length > 0) throw new GatherError(failures, inputs.length);
|
|
7611
8204
|
return outputs;
|
|
7612
8205
|
}
|
|
@@ -7678,6 +8271,25 @@ var WorkflowEngine = class {
|
|
|
7678
8271
|
await this.store.saveCheckpoint(checkpoint);
|
|
7679
8272
|
return true;
|
|
7680
8273
|
}
|
|
8274
|
+
/**
|
|
8275
|
+
* {@link savePendingCheckpoint} for checkpoints at distinct seqs of one run: one
|
|
8276
|
+
* {@link StateStore.saveCheckpointsIfPending} call when the store has it, else one write each.
|
|
8277
|
+
* Never rejects as a whole: a write that failed is reported as that entry's error, and the batch
|
|
8278
|
+
* call rejecting means it wrote nothing, so the entries are then written one by one.
|
|
8279
|
+
*/
|
|
8280
|
+
async savePendingCheckpoints(checkpoints) {
|
|
8281
|
+
if (checkpoints.length > 1 && this.store.saveCheckpointsIfPending) {
|
|
8282
|
+
try {
|
|
8283
|
+
const wrote = await this.store.saveCheckpointsIfPending(checkpoints);
|
|
8284
|
+
return checkpoints.map((_, i) => wrote[i] === true);
|
|
8285
|
+
} catch {
|
|
8286
|
+
}
|
|
8287
|
+
}
|
|
8288
|
+
const settled = await Promise.allSettled(checkpoints.map((cp) => this.savePendingCheckpoint(cp)));
|
|
8289
|
+
return settled.map((s) => s.status === "fulfilled" ? s.value : {
|
|
8290
|
+
error: s.reason
|
|
8291
|
+
});
|
|
8292
|
+
}
|
|
7681
8293
|
/** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
|
|
7682
8294
|
* dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
|
|
7683
8295
|
* {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
|
|
@@ -7716,57 +8328,161 @@ var WorkflowEngine = class {
|
|
|
7716
8328
|
* result (the dispatching one may be gone), so the run is crash/scale-safe. Resolves whether the
|
|
7717
8329
|
* run should now be resumed: not for a duplicate or late delivery (the checkpoint isn't `pending`),
|
|
7718
8330
|
* a run already finished, or a step fan whose barrier is still closed.
|
|
8331
|
+
*
|
|
8332
|
+
* Results of one run are settled in batches: the first starts a batch, and the ones that arrive
|
|
8333
|
+
* while it is being written form the next. Under load a wide fan's results are written by one
|
|
8334
|
+
* {@link StateStore.saveCheckpointsIfPending} call per batch, and the fan's barrier is asked once
|
|
8335
|
+
* per batch, not once per result. A result resolves only once its own write has committed, so the
|
|
8336
|
+
* transport acknowledges nothing that is not durable yet.
|
|
8337
|
+
*/
|
|
8338
|
+
completeRemoteResult(result) {
|
|
8339
|
+
return new Promise((resolve, reject) => {
|
|
8340
|
+
const entry = {
|
|
8341
|
+
result,
|
|
8342
|
+
resolve,
|
|
8343
|
+
reject
|
|
8344
|
+
};
|
|
8345
|
+
const queued = this.resultQueues.get(result.runId);
|
|
8346
|
+
if (queued) {
|
|
8347
|
+
queued.push(entry);
|
|
8348
|
+
return;
|
|
8349
|
+
}
|
|
8350
|
+
const queue = [
|
|
8351
|
+
entry
|
|
8352
|
+
];
|
|
8353
|
+
this.resultQueues.set(result.runId, queue);
|
|
8354
|
+
void this.drainResultQueue(result.runId, queue);
|
|
8355
|
+
});
|
|
8356
|
+
}
|
|
8357
|
+
/** Settle `queue` (a run's queued results) batch by batch until it is empty, then retire it. */
|
|
8358
|
+
async drainResultQueue(runId, queue) {
|
|
8359
|
+
await Promise.resolve();
|
|
8360
|
+
while (queue.length > 0) {
|
|
8361
|
+
const batch = queue.splice(0, RESULT_BATCH_MAX);
|
|
8362
|
+
let outcomes;
|
|
8363
|
+
try {
|
|
8364
|
+
outcomes = await this.completeRemoteResults(runId, batch.map((q) => q.result));
|
|
8365
|
+
} catch (err) {
|
|
8366
|
+
outcomes = batch.map(() => ({
|
|
8367
|
+
error: err
|
|
8368
|
+
}));
|
|
8369
|
+
}
|
|
8370
|
+
batch.forEach((q, i) => {
|
|
8371
|
+
const outcome = outcomes[i] ?? false;
|
|
8372
|
+
if (typeof outcome === "boolean") q.resolve(outcome);
|
|
8373
|
+
else q.reject(outcome.error);
|
|
8374
|
+
});
|
|
8375
|
+
}
|
|
8376
|
+
this.resultQueues.delete(runId);
|
|
8377
|
+
}
|
|
8378
|
+
/**
|
|
8379
|
+
* Settle a batch of one run's step results. At most one of them resolves `true` (resume the run):
|
|
8380
|
+
* that resume replays every write of the batch, so a second would only replay it again. A result
|
|
8381
|
+
* whose read or write failed resolves its error, and the transport redelivers just that one.
|
|
7719
8382
|
*/
|
|
7720
|
-
async
|
|
7721
|
-
const
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
8383
|
+
async completeRemoteResults(runId, results) {
|
|
8384
|
+
const outcomes = results.map(() => false);
|
|
8385
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8386
|
+
const live = results.flatMap((r, i) => {
|
|
8387
|
+
if (seen.has(r.seq)) return [];
|
|
8388
|
+
seen.add(r.seq);
|
|
8389
|
+
return [
|
|
8390
|
+
i
|
|
8391
|
+
];
|
|
8392
|
+
});
|
|
8393
|
+
const reads = await Promise.allSettled(live.map((i) => this.store.getCheckpoint(runId, results[i].seq)));
|
|
8394
|
+
const pending = [];
|
|
8395
|
+
reads.forEach((read, k) => {
|
|
8396
|
+
const index = live[k];
|
|
8397
|
+
if (read.status === "rejected") outcomes[index] = {
|
|
8398
|
+
error: read.reason
|
|
8399
|
+
};
|
|
8400
|
+
else if (read.value?.status === "pending") pending.push({
|
|
8401
|
+
index,
|
|
8402
|
+
cp: read.value
|
|
8403
|
+
});
|
|
8404
|
+
});
|
|
8405
|
+
if (pending.length === 0) return outcomes;
|
|
8406
|
+
for (const { cp } of pending) void this.releaseQueueSlot(cp.stepId).catch(() => void 0);
|
|
8407
|
+
const run = await this.store.getRun(runId);
|
|
8408
|
+
if (run && (run.status === "cancelled" || run.status === "completed")) return outcomes;
|
|
7726
8409
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
7727
|
-
const
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
8410
|
+
const writes = pending.map(({ index, cp }) => {
|
|
8411
|
+
const result = results[index];
|
|
8412
|
+
return {
|
|
8413
|
+
...cp,
|
|
8414
|
+
status: result.status,
|
|
8415
|
+
output: result.status === "completed" ? result.output : cp.output,
|
|
8416
|
+
error: result.error,
|
|
8417
|
+
events: result.events ?? cp.events,
|
|
8418
|
+
startedAt: result.startedAt ? new Date(result.startedAt) : cp.startedAt,
|
|
8419
|
+
finishedAt
|
|
8420
|
+
};
|
|
7736
8421
|
});
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
queueMs: startedAt.getTime() - cp.enqueuedAt.getTime(),
|
|
7748
|
-
durationMs: finishedAt.getTime() - startedAt.getTime()
|
|
8422
|
+
const claims = await this.savePendingCheckpoints(writes);
|
|
8423
|
+
const settled = [];
|
|
8424
|
+
claims.forEach((claim, k) => {
|
|
8425
|
+
const { index, cp } = pending[k];
|
|
8426
|
+
if (claim === true) settled.push({
|
|
8427
|
+
index,
|
|
8428
|
+
cp,
|
|
8429
|
+
result: results[index]
|
|
8430
|
+
});
|
|
8431
|
+
else if (claim !== false) outcomes[index] = claim;
|
|
7749
8432
|
});
|
|
7750
|
-
|
|
8433
|
+
for (const { cp, result } of settled) {
|
|
8434
|
+
this.noteSettledHere(`${runId}:${result.seq}`);
|
|
8435
|
+
const startedAt = result.startedAt ? new Date(result.startedAt) : cp.startedAt;
|
|
8436
|
+
this.emit({
|
|
8437
|
+
type: result.status === "completed" ? "step.completed" : "step.failed",
|
|
8438
|
+
runId,
|
|
8439
|
+
seq: result.seq,
|
|
8440
|
+
name: cp.name,
|
|
8441
|
+
kind: cp.kind,
|
|
8442
|
+
output: result.output,
|
|
8443
|
+
error: result.error,
|
|
8444
|
+
queueMs: startedAt.getTime() - cp.enqueuedAt.getTime(),
|
|
8445
|
+
durationMs: finishedAt.getTime() - startedAt.getTime()
|
|
8446
|
+
});
|
|
8447
|
+
}
|
|
8448
|
+
const barriers = /* @__PURE__ */ new Map();
|
|
8449
|
+
let carrier;
|
|
8450
|
+
for (const { index, cp, result } of settled) {
|
|
8451
|
+
if (!await this.stepFanStillRunning(cp, result, barriers)) carrier = index;
|
|
8452
|
+
}
|
|
8453
|
+
if (carrier !== void 0) outcomes[carrier] = true;
|
|
8454
|
+
return outcomes;
|
|
7751
8455
|
}
|
|
7752
8456
|
/**
|
|
7753
8457
|
* The join barrier of a step fan: a result for one of its steps resumes the parent only once no
|
|
7754
8458
|
* step of the fan is still `pending` (a settled wave), or on a failure (failFast surfaces it, and a
|
|
7755
8459
|
* retry needs the parent to schedule it). Without it every result would replay the whole parent,
|
|
7756
|
-
* which makes a fan of N steps cost O(N²) checkpoint reads.
|
|
8460
|
+
* which makes a fan of N steps cost O(N²) checkpoint reads. `barriers` holds the answer per fan
|
|
8461
|
+
* for the rest of a batch. A barrier that cannot be read counts as open: a spare resume replays and
|
|
8462
|
+
* re-suspends, a lost one strands the run.
|
|
7757
8463
|
*/
|
|
7758
|
-
async stepFanStillRunning(cp, result) {
|
|
8464
|
+
async stepFanStillRunning(cp, result, barriers) {
|
|
7759
8465
|
const fan = parseStepFanGroup(cp.parallelGroup);
|
|
7760
8466
|
if (!fan) return false;
|
|
7761
8467
|
if (result.status === "failed") return false;
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
8468
|
+
let closed = barriers.get(fan.firstSeq);
|
|
8469
|
+
if (!closed) {
|
|
8470
|
+
closed = this.stepFanPending(result.runId, fan.firstSeq, fan.firstSeq + fan.size - 1).then((pending) => pending > 0, () => false);
|
|
8471
|
+
barriers.set(fan.firstSeq, closed);
|
|
8472
|
+
}
|
|
8473
|
+
if (await closed) return true;
|
|
7767
8474
|
this.noteRecent(this.stepFansFiredHere, `${result.runId}:${fan.firstSeq}`);
|
|
7768
8475
|
return false;
|
|
7769
8476
|
}
|
|
8477
|
+
/** How many steps of a step fan's seq range are still `pending`. */
|
|
8478
|
+
async stepFanPending(runId, from, to) {
|
|
8479
|
+
if (this.store.countCheckpoints) {
|
|
8480
|
+
return this.store.countCheckpoints(runId, from, to, [
|
|
8481
|
+
"pending"
|
|
8482
|
+
]);
|
|
8483
|
+
}
|
|
8484
|
+
return (await this.store.listCheckpoints(runId)).filter((c) => c.seq >= from && c.seq <= to && c.status === "pending").length;
|
|
8485
|
+
}
|
|
7770
8486
|
/**
|
|
7771
8487
|
* Apply a remote workflow-turn {@link WorkflowDecision} durably — runs on WHATEVER instance receives
|
|
7772
8488
|
* it over the broker (the dispatching one may be gone, or a point-to-point broker handed it to a
|
|
@@ -8213,14 +8929,339 @@ function summarizeTimeline(timeline, over) {
|
|
|
8213
8929
|
__name(summarizeTimeline, "summarizeTimeline");
|
|
8214
8930
|
function pageFan(timeline, group, query) {
|
|
8215
8931
|
const matching = timeline.filter((cp) => cp.parallelGroup === group && (!query.failedOnly || isFailedMember(cp)));
|
|
8216
|
-
const
|
|
8217
|
-
const limit = query.limit !== void 0 && Number.isFinite(query.limit) ? Math.min(FAN_PAGE_MAX, Math.max(1, Math.floor(query.limit))) : 100;
|
|
8932
|
+
const { offset, limit } = fanPageBounds(query);
|
|
8218
8933
|
return {
|
|
8219
8934
|
items: matching.slice(offset, offset + limit),
|
|
8220
8935
|
total: matching.length
|
|
8221
8936
|
};
|
|
8222
8937
|
}
|
|
8223
8938
|
__name(pageFan, "pageFan");
|
|
8939
|
+
function fanPageBounds(query) {
|
|
8940
|
+
const offset = query.offset !== void 0 && Number.isFinite(query.offset) ? Math.max(0, Math.floor(query.offset)) : 0;
|
|
8941
|
+
const limit = query.limit !== void 0 && Number.isFinite(query.limit) ? Math.min(FAN_PAGE_MAX, Math.max(1, Math.floor(query.limit))) : 100;
|
|
8942
|
+
return {
|
|
8943
|
+
offset,
|
|
8944
|
+
limit
|
|
8945
|
+
};
|
|
8946
|
+
}
|
|
8947
|
+
__name(fanPageBounds, "fanPageBounds");
|
|
8948
|
+
|
|
8949
|
+
// src/run-detail-store.ts
|
|
8950
|
+
function aggregatesCheckpoints(store) {
|
|
8951
|
+
return store.checkpointStats !== void 0 && store.queryCheckpoints !== void 0;
|
|
8952
|
+
}
|
|
8953
|
+
__name(aggregatesCheckpoints, "aggregatesCheckpoints");
|
|
8954
|
+
function checkpointStatsOf(checkpoints, opts) {
|
|
8955
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
8956
|
+
for (const cp of checkpoints) {
|
|
8957
|
+
const group = cp.parallelGroup ?? null;
|
|
8958
|
+
if (opts?.group !== void 0 && group !== opts.group) continue;
|
|
8959
|
+
const nameKind = checkpointNameKind(cp.name);
|
|
8960
|
+
const key = JSON.stringify([
|
|
8961
|
+
group,
|
|
8962
|
+
cp.status,
|
|
8963
|
+
nameKind
|
|
8964
|
+
]);
|
|
8965
|
+
const enqueuedAt = cp.enqueuedAt ?? cp.startedAt;
|
|
8966
|
+
const bucket = buckets.get(key);
|
|
8967
|
+
if (!bucket) {
|
|
8968
|
+
buckets.set(key, {
|
|
8969
|
+
group,
|
|
8970
|
+
status: cp.status,
|
|
8971
|
+
nameKind,
|
|
8972
|
+
count: 1,
|
|
8973
|
+
minSeq: cp.seq,
|
|
8974
|
+
maxSeq: cp.seq,
|
|
8975
|
+
minName: cp.name,
|
|
8976
|
+
maxName: cp.name,
|
|
8977
|
+
minEnqueuedAt: enqueuedAt,
|
|
8978
|
+
minStartedAt: cp.startedAt,
|
|
8979
|
+
maxStartedAt: cp.startedAt,
|
|
8980
|
+
maxFinishedAt: cp.finishedAt,
|
|
8981
|
+
sumAttempts: cp.attempts
|
|
8982
|
+
});
|
|
8983
|
+
continue;
|
|
8984
|
+
}
|
|
8985
|
+
bucket.count += 1;
|
|
8986
|
+
bucket.minSeq = Math.min(bucket.minSeq, cp.seq);
|
|
8987
|
+
bucket.maxSeq = Math.max(bucket.maxSeq, cp.seq);
|
|
8988
|
+
if (cp.name < bucket.minName) bucket.minName = cp.name;
|
|
8989
|
+
if (cp.name > bucket.maxName) bucket.maxName = cp.name;
|
|
8990
|
+
if (enqueuedAt < bucket.minEnqueuedAt) bucket.minEnqueuedAt = enqueuedAt;
|
|
8991
|
+
if (cp.startedAt < bucket.minStartedAt) bucket.minStartedAt = cp.startedAt;
|
|
8992
|
+
if (cp.startedAt > bucket.maxStartedAt) bucket.maxStartedAt = cp.startedAt;
|
|
8993
|
+
if (cp.finishedAt > bucket.maxFinishedAt) bucket.maxFinishedAt = cp.finishedAt;
|
|
8994
|
+
bucket.sumAttempts += cp.attempts;
|
|
8995
|
+
}
|
|
8996
|
+
return [
|
|
8997
|
+
...buckets.values()
|
|
8998
|
+
];
|
|
8999
|
+
}
|
|
9000
|
+
__name(checkpointStatsOf, "checkpointStatsOf");
|
|
9001
|
+
function queryCheckpointsOf(checkpoints, query) {
|
|
9002
|
+
const excluded = new Set(query.excludeGroups ?? []);
|
|
9003
|
+
const matching = checkpoints.filter((cp) => (query.group === void 0 || cp.parallelGroup === query.group) && (cp.parallelGroup === void 0 || !excluded.has(cp.parallelGroup)) && (query.statuses === void 0 || query.statuses.includes(cp.status)) && (query.namePrefixes === void 0 || query.namePrefixes.some((prefix) => cp.name.startsWith(prefix)))).sort((a, b) => a.seq - b.seq);
|
|
9004
|
+
const offset = query.offset ?? 0;
|
|
9005
|
+
return matching.slice(offset, query.limit === void 0 ? void 0 : offset + query.limit);
|
|
9006
|
+
}
|
|
9007
|
+
__name(queryCheckpointsOf, "queryCheckpointsOf");
|
|
9008
|
+
var TERMINAL = /* @__PURE__ */ __name((status) => status !== "pending" && status !== "running", "TERMINAL");
|
|
9009
|
+
function isWide(group, rows, over) {
|
|
9010
|
+
return rows > over || (parseFanGroup(group)?.size ?? 0) > over;
|
|
9011
|
+
}
|
|
9012
|
+
__name(isWide, "isWide");
|
|
9013
|
+
function mergeBySeq(a, b) {
|
|
9014
|
+
return [
|
|
9015
|
+
...a,
|
|
9016
|
+
...b
|
|
9017
|
+
].sort((x, y) => x.seq - y.seq);
|
|
9018
|
+
}
|
|
9019
|
+
__name(mergeBySeq, "mergeBySeq");
|
|
9020
|
+
async function failedChildren(store, runId, group, buckets) {
|
|
9021
|
+
if (!buckets.some((b) => b.nameKind === "child" && b.status === "completed")) {
|
|
9022
|
+
return {
|
|
9023
|
+
completed: [],
|
|
9024
|
+
failed: []
|
|
9025
|
+
};
|
|
9026
|
+
}
|
|
9027
|
+
const completed = await store.queryCheckpoints(runId, {
|
|
9028
|
+
group,
|
|
9029
|
+
statuses: [
|
|
9030
|
+
"completed"
|
|
9031
|
+
],
|
|
9032
|
+
namePrefixes: [
|
|
9033
|
+
"signal:child:"
|
|
9034
|
+
]
|
|
9035
|
+
});
|
|
9036
|
+
return {
|
|
9037
|
+
completed,
|
|
9038
|
+
failed: completed.filter(isFailedMember)
|
|
9039
|
+
};
|
|
9040
|
+
}
|
|
9041
|
+
__name(failedChildren, "failedChildren");
|
|
9042
|
+
async function summarizeFanFromStats(store, runId, group, buckets, folded) {
|
|
9043
|
+
const counts = {};
|
|
9044
|
+
const add = /* @__PURE__ */ __name((status, n) => {
|
|
9045
|
+
counts[status] = (counts[status] ?? 0) + n;
|
|
9046
|
+
}, "add");
|
|
9047
|
+
let size = 0;
|
|
9048
|
+
let startedAt;
|
|
9049
|
+
let finishedAt;
|
|
9050
|
+
let firstSeq = Number.POSITIVE_INFINITY;
|
|
9051
|
+
let lastSeq = Number.NEGATIVE_INFINITY;
|
|
9052
|
+
let minName;
|
|
9053
|
+
let maxName;
|
|
9054
|
+
let childRuns = false;
|
|
9055
|
+
for (const b of buckets) {
|
|
9056
|
+
firstSeq = Math.min(firstSeq, b.minSeq);
|
|
9057
|
+
lastSeq = Math.max(lastSeq, b.maxSeq);
|
|
9058
|
+
if (minName === void 0 || b.minName < minName) minName = b.minName;
|
|
9059
|
+
if (maxName === void 0 || b.maxName > maxName) maxName = b.maxName;
|
|
9060
|
+
if (b.nameKind === "child" || b.nameKind === "folded") childRuns = true;
|
|
9061
|
+
if (TERMINAL(b.status)) {
|
|
9062
|
+
if (!startedAt || b.minStartedAt < startedAt) startedAt = b.minStartedAt;
|
|
9063
|
+
if (!finishedAt || b.maxFinishedAt > finishedAt) finishedAt = b.maxFinishedAt;
|
|
9064
|
+
} else if (!startedAt || b.minEnqueuedAt < startedAt) {
|
|
9065
|
+
startedAt = b.minEnqueuedAt;
|
|
9066
|
+
}
|
|
9067
|
+
if (b.nameKind === "folded") continue;
|
|
9068
|
+
if (b.nameKind === "child" && b.status === "completed") continue;
|
|
9069
|
+
add(b.status, b.count);
|
|
9070
|
+
size += b.count;
|
|
9071
|
+
}
|
|
9072
|
+
for (const cp of folded) {
|
|
9073
|
+
const n = foldedCount(cp) ?? 1;
|
|
9074
|
+
add(isFailedMember(cp) ? "failed" : cp.status, n);
|
|
9075
|
+
size += n;
|
|
9076
|
+
}
|
|
9077
|
+
const children = await failedChildren(store, runId, group, buckets);
|
|
9078
|
+
const failedChildSet = new Set(children.failed);
|
|
9079
|
+
for (const cp of children.completed) add(failedChildSet.has(cp) ? "failed" : "completed", 1);
|
|
9080
|
+
size += children.completed.length;
|
|
9081
|
+
const failedStatusRows = buckets.some((b) => b.status === "failed") ? await store.queryCheckpoints(runId, {
|
|
9082
|
+
group,
|
|
9083
|
+
statuses: [
|
|
9084
|
+
"failed"
|
|
9085
|
+
],
|
|
9086
|
+
limit: FAN_SUMMARY_FAILURES
|
|
9087
|
+
}) : [];
|
|
9088
|
+
const failures = mergeBySeq(failedStatusRows, children.failed).slice(0, FAN_SUMMARY_FAILURES);
|
|
9089
|
+
const parsed = parseFanGroup(group);
|
|
9090
|
+
const total = parsed?.size;
|
|
9091
|
+
return {
|
|
9092
|
+
group,
|
|
9093
|
+
label: childRuns ? "child runs" : minName === maxName ? minName ?? "" : "steps",
|
|
9094
|
+
firstSeq,
|
|
9095
|
+
lastSeq,
|
|
9096
|
+
size,
|
|
9097
|
+
total,
|
|
9098
|
+
notStarted: total === void 0 ? void 0 : Math.max(0, total - size),
|
|
9099
|
+
failFast: parsed?.failFast ?? false,
|
|
9100
|
+
counts,
|
|
9101
|
+
startedAt,
|
|
9102
|
+
finishedAt,
|
|
9103
|
+
failures
|
|
9104
|
+
};
|
|
9105
|
+
}
|
|
9106
|
+
__name(summarizeFanFromStats, "summarizeFanFromStats");
|
|
9107
|
+
async function firstChildIds(store, runId) {
|
|
9108
|
+
const ids = [];
|
|
9109
|
+
for (let offset = 0; ids.length < SUMMARY_CHILD_LIMIT; offset += SUMMARY_CHILD_LIMIT) {
|
|
9110
|
+
const page = await store.queryCheckpoints(runId, {
|
|
9111
|
+
namePrefixes: CHILD_EDGE_PREFIXES,
|
|
9112
|
+
offset,
|
|
9113
|
+
limit: SUMMARY_CHILD_LIMIT
|
|
9114
|
+
});
|
|
9115
|
+
ids.push(...childIdsOf(page));
|
|
9116
|
+
if (page.length < SUMMARY_CHILD_LIMIT) break;
|
|
9117
|
+
}
|
|
9118
|
+
return ids.slice(0, SUMMARY_CHILD_LIMIT);
|
|
9119
|
+
}
|
|
9120
|
+
__name(firstChildIds, "firstChildIds");
|
|
9121
|
+
async function summarizeRunFromStore(store, runId, over, stats) {
|
|
9122
|
+
const all = stats ?? await store.checkpointStats(runId);
|
|
9123
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
9124
|
+
let edgeRows = 0;
|
|
9125
|
+
let hasFolded = false;
|
|
9126
|
+
for (const b of all) {
|
|
9127
|
+
if (b.nameKind === "child" || b.nameKind === "spawn") edgeRows += b.count;
|
|
9128
|
+
if (b.nameKind === "folded") hasFolded = true;
|
|
9129
|
+
if (b.group === null) continue;
|
|
9130
|
+
const buckets = byGroup.get(b.group);
|
|
9131
|
+
if (buckets) buckets.push(b);
|
|
9132
|
+
else byGroup.set(b.group, [
|
|
9133
|
+
b
|
|
9134
|
+
]);
|
|
9135
|
+
}
|
|
9136
|
+
const wide = [
|
|
9137
|
+
...byGroup
|
|
9138
|
+
].filter(([group, buckets]) => isWide(group, buckets.reduce((n, b) => n + b.count, 0), over));
|
|
9139
|
+
const [timeline, folded, children] = await Promise.all([
|
|
9140
|
+
store.queryCheckpoints(runId, {
|
|
9141
|
+
excludeGroups: wide.map(([group]) => group)
|
|
9142
|
+
}),
|
|
9143
|
+
hasFolded ? store.queryCheckpoints(runId, {
|
|
9144
|
+
namePrefixes: [
|
|
9145
|
+
FOLDED_PREFIX
|
|
9146
|
+
]
|
|
9147
|
+
}) : [],
|
|
9148
|
+
edgeRows > 0 || hasFolded ? firstChildIds(store, runId) : []
|
|
9149
|
+
]);
|
|
9150
|
+
const fans = await Promise.all(wide.map(([group, buckets]) => summarizeFanFromStats(store, runId, group, buckets, folded.filter((cp) => cp.parallelGroup === group))));
|
|
9151
|
+
const childCount = folded.reduce((n, cp) => n + childIdsOf([
|
|
9152
|
+
cp
|
|
9153
|
+
]).length, edgeRows);
|
|
9154
|
+
return {
|
|
9155
|
+
timeline,
|
|
9156
|
+
fans: fans.sort((a, b) => a.firstSeq - b.firstSeq),
|
|
9157
|
+
children,
|
|
9158
|
+
childCount
|
|
9159
|
+
};
|
|
9160
|
+
}
|
|
9161
|
+
__name(summarizeRunFromStore, "summarizeRunFromStore");
|
|
9162
|
+
async function pageFanFromStore(store, runId, group, query, stats) {
|
|
9163
|
+
const buckets = stats ?? await store.checkpointStats(runId, {
|
|
9164
|
+
group
|
|
9165
|
+
});
|
|
9166
|
+
const { offset, limit } = fanPageBounds(query);
|
|
9167
|
+
if (!query.failedOnly) {
|
|
9168
|
+
const total2 = buckets.reduce((n, b) => n + b.count, 0);
|
|
9169
|
+
const items = total2 > offset ? await store.queryCheckpoints(runId, {
|
|
9170
|
+
group,
|
|
9171
|
+
offset,
|
|
9172
|
+
limit
|
|
9173
|
+
}) : [];
|
|
9174
|
+
return {
|
|
9175
|
+
items,
|
|
9176
|
+
total: total2
|
|
9177
|
+
};
|
|
9178
|
+
}
|
|
9179
|
+
const failedStatus = buckets.filter((b) => b.status === "failed").reduce((n, b) => n + b.count, 0);
|
|
9180
|
+
const children = await failedChildren(store, runId, group, buckets);
|
|
9181
|
+
const total = failedStatus + children.failed.length;
|
|
9182
|
+
if (total <= offset) return {
|
|
9183
|
+
items: [],
|
|
9184
|
+
total
|
|
9185
|
+
};
|
|
9186
|
+
if (children.failed.length === 0) {
|
|
9187
|
+
return {
|
|
9188
|
+
items: await store.queryCheckpoints(runId, {
|
|
9189
|
+
group,
|
|
9190
|
+
statuses: [
|
|
9191
|
+
"failed"
|
|
9192
|
+
],
|
|
9193
|
+
offset,
|
|
9194
|
+
limit
|
|
9195
|
+
}),
|
|
9196
|
+
total
|
|
9197
|
+
};
|
|
9198
|
+
}
|
|
9199
|
+
const failedRows = await store.queryCheckpoints(runId, {
|
|
9200
|
+
group,
|
|
9201
|
+
statuses: [
|
|
9202
|
+
"failed"
|
|
9203
|
+
],
|
|
9204
|
+
limit: offset + limit
|
|
9205
|
+
});
|
|
9206
|
+
return {
|
|
9207
|
+
items: mergeBySeq(failedRows, children.failed).slice(offset, offset + limit),
|
|
9208
|
+
total
|
|
9209
|
+
};
|
|
9210
|
+
}
|
|
9211
|
+
__name(pageFanFromStore, "pageFanFromStore");
|
|
9212
|
+
function hash(text) {
|
|
9213
|
+
let h1 = 3735928559;
|
|
9214
|
+
let h2 = 1103547991;
|
|
9215
|
+
for (let i = 0; i < text.length; i++) {
|
|
9216
|
+
const ch = text.charCodeAt(i);
|
|
9217
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
9218
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
9219
|
+
}
|
|
9220
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
9221
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
9222
|
+
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16);
|
|
9223
|
+
}
|
|
9224
|
+
__name(hash, "hash");
|
|
9225
|
+
function statsFingerprint(stats) {
|
|
9226
|
+
return stats.map((b) => [
|
|
9227
|
+
b.group ?? "",
|
|
9228
|
+
b.status,
|
|
9229
|
+
b.nameKind,
|
|
9230
|
+
b.count,
|
|
9231
|
+
b.minSeq,
|
|
9232
|
+
b.maxSeq,
|
|
9233
|
+
b.minName,
|
|
9234
|
+
b.maxName,
|
|
9235
|
+
b.minEnqueuedAt.getTime(),
|
|
9236
|
+
b.minStartedAt.getTime(),
|
|
9237
|
+
b.maxStartedAt.getTime(),
|
|
9238
|
+
b.maxFinishedAt.getTime(),
|
|
9239
|
+
b.sumAttempts
|
|
9240
|
+
].join("")).sort().join("");
|
|
9241
|
+
}
|
|
9242
|
+
__name(statsFingerprint, "statsFingerprint");
|
|
9243
|
+
function runDetailEtag(run, stats, over) {
|
|
9244
|
+
return `"rd-${hash(`${over}${JSON.stringify(run)}${statsFingerprint(stats)}`)}"`;
|
|
9245
|
+
}
|
|
9246
|
+
__name(runDetailEtag, "runDetailEtag");
|
|
9247
|
+
function fanPageEtag(group, stats, query) {
|
|
9248
|
+
const { offset, limit } = fanPageBounds(query);
|
|
9249
|
+
const key = [
|
|
9250
|
+
group,
|
|
9251
|
+
offset,
|
|
9252
|
+
limit,
|
|
9253
|
+
query.failedOnly === true
|
|
9254
|
+
].join("");
|
|
9255
|
+
return `"fp-${hash(`${key}${statsFingerprint(stats)}`)}"`;
|
|
9256
|
+
}
|
|
9257
|
+
__name(fanPageEtag, "fanPageEtag");
|
|
9258
|
+
function etagMatches(ifNoneMatch, etag) {
|
|
9259
|
+
if (!ifNoneMatch) return false;
|
|
9260
|
+
const bare = /* @__PURE__ */ __name((tag) => tag.trim().replace(/^W\//, ""), "bare");
|
|
9261
|
+
const target = bare(etag);
|
|
9262
|
+
return ifNoneMatch.split(",").some((tag) => tag.trim() === "*" || bare(tag) === target);
|
|
9263
|
+
}
|
|
9264
|
+
__name(etagMatches, "etagMatches");
|
|
8224
9265
|
|
|
8225
9266
|
// src/codec-state-store.ts
|
|
8226
9267
|
var CodecStateStore = class {
|
|
@@ -8247,14 +9288,28 @@ var CodecStateStore = class {
|
|
|
8247
9288
|
/** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
|
|
8248
9289
|
* engine's per-run fallback still kicks in through the wrapper. */
|
|
8249
9290
|
deleteRuns;
|
|
9291
|
+
/** Decodes the runs like {@link listIncompleteRuns}. Bound only when the inner store has it, so
|
|
9292
|
+
* the engine's list-and-filter fallback still kicks in through the wrapper. */
|
|
9293
|
+
listOrphanedRuns;
|
|
9294
|
+
/** Encodes the patch like {@link updateRun}. Bound only when the inner store has it, so the
|
|
9295
|
+
* engine's read-then-write fallback still kicks in through the wrapper. */
|
|
9296
|
+
updateRunIf;
|
|
8250
9297
|
/** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
|
|
8251
9298
|
* plain-save fallback still kicks in through the wrapper. */
|
|
8252
9299
|
saveCheckpointIfPending;
|
|
9300
|
+
/** Encodes each checkpoint like {@link saveCheckpoint}. Bound only when the inner store has it. */
|
|
9301
|
+
saveCheckpointsIfPending;
|
|
8253
9302
|
/** Forwarded verbatim — a count reads no payload. Bound only when the inner store has it. */
|
|
8254
9303
|
countCheckpoints;
|
|
8255
9304
|
/** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
|
|
8256
9305
|
* when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
|
|
8257
9306
|
pruneTerminalRuns;
|
|
9307
|
+
/** Forwarded verbatim — the buckets aggregate names, statuses and timestamps, never a payload.
|
|
9308
|
+
* Bound only when the inner store aggregates, so the run detail's fallback still applies. */
|
|
9309
|
+
checkpointStats;
|
|
9310
|
+
/** Filters on unencoded columns, then decodes the rows like {@link listCheckpoints}. Bound only
|
|
9311
|
+
* when the inner store has it. */
|
|
9312
|
+
queryCheckpoints;
|
|
8258
9313
|
/** Forwarded verbatim — a count reads no payload. Bound only when the inner store counts, so the
|
|
8259
9314
|
* engine's facet/listing fallback still applies through the wrapper. */
|
|
8260
9315
|
countRuns;
|
|
@@ -8279,10 +9334,20 @@ var CodecStateStore = class {
|
|
|
8279
9334
|
if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
|
|
8280
9335
|
const deleteRuns = inner.deleteRuns;
|
|
8281
9336
|
if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
|
|
9337
|
+
const orphans = inner.listOrphanedRuns;
|
|
9338
|
+
if (orphans) this.listOrphanedRuns = async (nowMs, limit, namespace) => (await orphans.call(inner, nowMs, limit, namespace)).map((r) => this.decRun(r));
|
|
9339
|
+
const updateIf = inner.updateRunIf;
|
|
9340
|
+
if (updateIf) this.updateRunIf = (runId, expected, patch) => updateIf.call(inner, runId, expected, this.encRunPatch(patch));
|
|
8282
9341
|
const saveIfPending = inner.saveCheckpointIfPending;
|
|
8283
9342
|
if (saveIfPending) this.saveCheckpointIfPending = (cp) => saveIfPending.call(inner, this.encCp(cp));
|
|
9343
|
+
const saveManyIfPending = inner.saveCheckpointsIfPending;
|
|
9344
|
+
if (saveManyIfPending) this.saveCheckpointsIfPending = (cps) => saveManyIfPending.call(inner, cps.map((cp) => this.encCp(cp)));
|
|
8284
9345
|
const countCps = inner.countCheckpoints;
|
|
8285
9346
|
if (countCps) this.countCheckpoints = (runId, from, to, statuses) => countCps.call(inner, runId, from, to, statuses);
|
|
9347
|
+
const stats = inner.checkpointStats;
|
|
9348
|
+
if (stats) this.checkpointStats = (runId, opts) => stats.call(inner, runId, opts);
|
|
9349
|
+
const queryCps = inner.queryCheckpoints;
|
|
9350
|
+
if (queryCps) this.queryCheckpoints = async (runId, query) => (await queryCps.call(inner, runId, query)).map((c) => this.decCp(c));
|
|
8286
9351
|
const prune = inner.pruneTerminalRuns;
|
|
8287
9352
|
if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
|
|
8288
9353
|
const count = inner.countRuns;
|
|
@@ -8379,12 +9444,15 @@ var CodecStateStore = class {
|
|
|
8379
9444
|
return this.inner.createRun(this.encRun(run));
|
|
8380
9445
|
}
|
|
8381
9446
|
updateRun(runId, patch) {
|
|
9447
|
+
return this.inner.updateRun(runId, this.encRunPatch(patch));
|
|
9448
|
+
}
|
|
9449
|
+
encRunPatch(patch) {
|
|
8382
9450
|
const next = {
|
|
8383
9451
|
...patch
|
|
8384
9452
|
};
|
|
8385
9453
|
if ("input" in patch) next.input = this.enc(patch.input);
|
|
8386
9454
|
if ("output" in patch) next.output = this.enc(patch.output);
|
|
8387
|
-
return
|
|
9455
|
+
return next;
|
|
8388
9456
|
}
|
|
8389
9457
|
async getRun(runId) {
|
|
8390
9458
|
return this.decRun(await this.inner.getRun(runId));
|
|
@@ -8490,8 +9558,11 @@ function collectMetrics(engine) {
|
|
|
8490
9558
|
started: 0,
|
|
8491
9559
|
completed: 0,
|
|
8492
9560
|
failed: 0,
|
|
8493
|
-
suspended: 0
|
|
9561
|
+
suspended: 0,
|
|
9562
|
+
routed: 0,
|
|
9563
|
+
unrouted: 0
|
|
8494
9564
|
},
|
|
9565
|
+
routedByCode: {},
|
|
8495
9566
|
steps: {
|
|
8496
9567
|
started: 0,
|
|
8497
9568
|
completed: 0,
|
|
@@ -8533,6 +9604,15 @@ function collectMetrics(engine) {
|
|
|
8533
9604
|
case "run.suspended":
|
|
8534
9605
|
m.runs.suspended += 1;
|
|
8535
9606
|
break;
|
|
9607
|
+
case "run.routed": {
|
|
9608
|
+
m.runs.routed += 1;
|
|
9609
|
+
const code = e.error?.code ?? "*";
|
|
9610
|
+
m.routedByCode[code] = (m.routedByCode[code] ?? 0) + 1;
|
|
9611
|
+
break;
|
|
9612
|
+
}
|
|
9613
|
+
case "run.unrouted":
|
|
9614
|
+
m.runs.unrouted += 1;
|
|
9615
|
+
break;
|
|
8536
9616
|
case "step.started":
|
|
8537
9617
|
m.steps.started += 1;
|
|
8538
9618
|
break;
|
|
@@ -8559,6 +9639,8 @@ function collectMetrics(engine) {
|
|
|
8559
9639
|
for (const [k, v] of Object.entries(m.steps)) lines.push(`durable_steps_total{event="${k}"} ${v}`);
|
|
8560
9640
|
lines.push("# TYPE durable_runs_by_workflow_total counter");
|
|
8561
9641
|
for (const [name, v] of Object.entries(m.byWorkflow)) for (const [event, n] of Object.entries(v)) lines.push(`durable_runs_by_workflow_total{workflow="${name}",event="${event}"} ${n}`);
|
|
9642
|
+
lines.push("# TYPE durable_runs_routed_total counter");
|
|
9643
|
+
for (const [code, n] of Object.entries(m.routedByCode)) lines.push(`durable_runs_routed_total{code="${code}"} ${n}`);
|
|
8562
9644
|
lines.push("# TYPE durable_step_duration_ms_sum counter");
|
|
8563
9645
|
lines.push(`durable_step_duration_ms_sum ${m.stepDuration.sumMs}`);
|
|
8564
9646
|
lines.push("# TYPE durable_step_duration_count counter");
|
|
@@ -8729,6 +9811,11 @@ var InMemoryStateStore = class {
|
|
|
8729
9811
|
* so this lets the benchmark contrast "fetch N, scan in JS" against "fetch the k that match".
|
|
8730
9812
|
*/
|
|
8731
9813
|
lastCheckpointRowsRead = 0;
|
|
9814
|
+
/**
|
|
9815
|
+
* Test/observability hook: checkpoint rows materialized since construction, across every
|
|
9816
|
+
* checkpoint read that returns rows — what a run-detail benchmark compares between paths.
|
|
9817
|
+
*/
|
|
9818
|
+
checkpointRowsRead = 0;
|
|
8732
9819
|
key(runId, seq) {
|
|
8733
9820
|
return `${runId}:${seq}`;
|
|
8734
9821
|
}
|
|
@@ -8774,6 +9861,14 @@ var InMemoryStateStore = class {
|
|
|
8774
9861
|
this.reindexAttributes(run.id, run.searchAttributes);
|
|
8775
9862
|
}
|
|
8776
9863
|
async updateRun(runId, inputPatch) {
|
|
9864
|
+
this.applyRunPatch(runId, inputPatch);
|
|
9865
|
+
}
|
|
9866
|
+
updateRunIf(runId, expectedStatuses, patch) {
|
|
9867
|
+
const existing = this.runs.get(runId);
|
|
9868
|
+
if (!existing || !expectedStatuses.includes(existing.status)) return Promise.resolve(false);
|
|
9869
|
+
return this.updateRun(runId, patch).then(() => true);
|
|
9870
|
+
}
|
|
9871
|
+
applyRunPatch(runId, inputPatch) {
|
|
8777
9872
|
const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
|
|
8778
9873
|
...inputPatch,
|
|
8779
9874
|
tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
@@ -8854,6 +9949,23 @@ var InMemoryStateStore = class {
|
|
|
8854
9949
|
});
|
|
8855
9950
|
return true;
|
|
8856
9951
|
}
|
|
9952
|
+
async saveCheckpointsIfPending(checkpoints) {
|
|
9953
|
+
return checkpoints.map((checkpoint) => {
|
|
9954
|
+
const key = this.key(checkpoint.runId, checkpoint.seq);
|
|
9955
|
+
if (this.checkpoints.get(key)?.status !== "pending") return false;
|
|
9956
|
+
this.checkpoints.set(key, {
|
|
9957
|
+
...checkpoint
|
|
9958
|
+
});
|
|
9959
|
+
return true;
|
|
9960
|
+
});
|
|
9961
|
+
}
|
|
9962
|
+
async listOrphanedRuns(nowMs, limit, namespace) {
|
|
9963
|
+
return [
|
|
9964
|
+
...this.runs.values()
|
|
9965
|
+
].filter((r) => (r.status === "running" || r.status === "cancelling") && (r.lockedUntil == null || r.lockedUntil <= nowMs) && (namespace === void 0 || r.namespace === namespace)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, Math.max(0, limit)).map((r) => ({
|
|
9966
|
+
...r
|
|
9967
|
+
}));
|
|
9968
|
+
}
|
|
8857
9969
|
async listIncompleteRuns(namespace) {
|
|
8858
9970
|
return [
|
|
8859
9971
|
...this.runs.values()
|
|
@@ -9133,6 +10245,23 @@ var InMemoryStateStore = class {
|
|
|
9133
10245
|
...cp
|
|
9134
10246
|
}));
|
|
9135
10247
|
this.lastCheckpointRowsRead = rows.length;
|
|
10248
|
+
this.checkpointRowsRead += rows.length;
|
|
10249
|
+
return rows;
|
|
10250
|
+
}
|
|
10251
|
+
runCheckpoints(runId) {
|
|
10252
|
+
return [
|
|
10253
|
+
...this.checkpoints.values()
|
|
10254
|
+
].filter((cp) => cp.runId === runId);
|
|
10255
|
+
}
|
|
10256
|
+
async checkpointStats(runId, opts) {
|
|
10257
|
+
return checkpointStatsOf(this.runCheckpoints(runId), opts);
|
|
10258
|
+
}
|
|
10259
|
+
async queryCheckpoints(runId, query) {
|
|
10260
|
+
const rows = queryCheckpointsOf(this.runCheckpoints(runId), query).map((cp) => ({
|
|
10261
|
+
...cp
|
|
10262
|
+
}));
|
|
10263
|
+
this.lastCheckpointRowsRead = rows.length;
|
|
10264
|
+
this.checkpointRowsRead += rows.length;
|
|
9136
10265
|
return rows;
|
|
9137
10266
|
}
|
|
9138
10267
|
async getLatestCheckpointByName(runId, name) {
|
|
@@ -9160,6 +10289,7 @@ var InMemoryStateStore = class {
|
|
|
9160
10289
|
...cp
|
|
9161
10290
|
}));
|
|
9162
10291
|
this.lastCheckpointRowsRead = rows.length;
|
|
10292
|
+
this.checkpointRowsRead += rows.length;
|
|
9163
10293
|
return rows;
|
|
9164
10294
|
}
|
|
9165
10295
|
};
|
|
@@ -9211,6 +10341,7 @@ var InMemoryTransport = class {
|
|
|
9211
10341
|
};
|
|
9212
10342
|
// Annotate the CommonJS export names for ESM import in node:
|
|
9213
10343
|
0 && (module.exports = {
|
|
10344
|
+
CHILD_EDGE_PREFIXES,
|
|
9214
10345
|
CONTROL_FLOW_SIGNAL,
|
|
9215
10346
|
CURRENT_PROTOCOL_VERSION,
|
|
9216
10347
|
CodecStateStore,
|
|
@@ -9231,6 +10362,7 @@ var InMemoryTransport = class {
|
|
|
9231
10362
|
FAN_SUMMARY_FAILURES,
|
|
9232
10363
|
FatalError,
|
|
9233
10364
|
GatherError,
|
|
10365
|
+
HANDOFF_SEQ_BASE,
|
|
9234
10366
|
InMemoryAdmissionBackend,
|
|
9235
10367
|
InMemoryStateStore,
|
|
9236
10368
|
InMemoryTransport,
|
|
@@ -9265,9 +10397,11 @@ var InMemoryTransport = class {
|
|
|
9265
10397
|
WorkflowEngine,
|
|
9266
10398
|
WorkflowSuspended,
|
|
9267
10399
|
aggregateAnnouncements,
|
|
10400
|
+
aggregatesCheckpoints,
|
|
9268
10401
|
announcementsOf,
|
|
9269
10402
|
applyAttributeQuery,
|
|
9270
10403
|
assertConcurrency,
|
|
10404
|
+
assertRoutes,
|
|
9271
10405
|
attributeColumnFor,
|
|
9272
10406
|
attributeOperand,
|
|
9273
10407
|
attributePredicateOperands,
|
|
@@ -9277,10 +10411,14 @@ var InMemoryTransport = class {
|
|
|
9277
10411
|
buildWorkflowDirectory,
|
|
9278
10412
|
canRoute,
|
|
9279
10413
|
capableWorkers,
|
|
10414
|
+
checkpointNameKind,
|
|
10415
|
+
checkpointStatsOf,
|
|
9280
10416
|
childIdsOf,
|
|
9281
10417
|
classifyWaiterToken,
|
|
9282
10418
|
clearDurableExecutionWrappers,
|
|
9283
10419
|
clearTerminalSingletonAdmission,
|
|
10420
|
+
codeAndDetails,
|
|
10421
|
+
codeOf,
|
|
9284
10422
|
collectMetrics,
|
|
9285
10423
|
concurrencyTag,
|
|
9286
10424
|
controlPlaneDescriptor,
|
|
@@ -9293,13 +10431,20 @@ var InMemoryTransport = class {
|
|
|
9293
10431
|
debug,
|
|
9294
10432
|
describeWorker,
|
|
9295
10433
|
descriptorHash,
|
|
10434
|
+
detailsOf,
|
|
9296
10435
|
error,
|
|
10436
|
+
etagMatches,
|
|
9297
10437
|
eventMatchOf,
|
|
9298
10438
|
eventMatches,
|
|
9299
10439
|
eventNameOf,
|
|
9300
10440
|
eventPrefix,
|
|
9301
10441
|
eventToken,
|
|
9302
10442
|
facetOrigin,
|
|
10443
|
+
fanPageBounds,
|
|
10444
|
+
fanPageEtag,
|
|
10445
|
+
foldedCount,
|
|
10446
|
+
handoffHandlerId,
|
|
10447
|
+
handoffName,
|
|
9303
10448
|
hasDurableExecutionWrappers,
|
|
9304
10449
|
heartbeatStatus,
|
|
9305
10450
|
indexWaitersByRun,
|
|
@@ -9311,6 +10456,7 @@ var InMemoryTransport = class {
|
|
|
9311
10456
|
isNonEmptyRunScope,
|
|
9312
10457
|
isWorkflowControlFlowSignal,
|
|
9313
10458
|
listSignalWaitersOfRuns,
|
|
10459
|
+
liveHandoff,
|
|
9314
10460
|
log,
|
|
9315
10461
|
matchesAttributes,
|
|
9316
10462
|
mergeRunFacetRows,
|
|
@@ -9321,17 +10467,23 @@ var InMemoryTransport = class {
|
|
|
9321
10467
|
normalizeAttributeRows,
|
|
9322
10468
|
normalizeDescriptor,
|
|
9323
10469
|
observedAnnouncements,
|
|
10470
|
+
onErrorRunId,
|
|
9324
10471
|
pageFan,
|
|
10472
|
+
pageFanFromStore,
|
|
9325
10473
|
parentRunIdOf,
|
|
9326
10474
|
parseDuration,
|
|
9327
10475
|
partitionOfToken,
|
|
9328
10476
|
planDispatch,
|
|
9329
10477
|
prevCronFireMs,
|
|
10478
|
+
queryCheckpointsOf,
|
|
9330
10479
|
readSearchAttributes,
|
|
9331
10480
|
requiredCapabilities,
|
|
9332
10481
|
resolveConcurrency,
|
|
9333
10482
|
resolveRouting,
|
|
9334
10483
|
resolveRunWaiting,
|
|
10484
|
+
retiredHandoffName,
|
|
10485
|
+
routeFor,
|
|
10486
|
+
runDetailEtag,
|
|
9335
10487
|
runDurableExecution,
|
|
9336
10488
|
runInStepLogger,
|
|
9337
10489
|
runInStepScope,
|
|
@@ -9352,6 +10504,7 @@ var InMemoryTransport = class {
|
|
|
9352
10504
|
subEvent,
|
|
9353
10505
|
subProcess,
|
|
9354
10506
|
summarizeFan,
|
|
10507
|
+
summarizeRunFromStore,
|
|
9355
10508
|
summarizeTimeline,
|
|
9356
10509
|
tenantGroup,
|
|
9357
10510
|
useDurableExecution,
|