@dudousxd/nestjs-durable-core 0.77.0 → 0.79.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 +1200 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +590 -120
- package/dist/index.d.ts +590 -120
- package/dist/index.js +1170 -84
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -265,10 +265,16 @@ var FatalError = class extends Error {
|
|
|
265
265
|
__name(this, "FatalError");
|
|
266
266
|
}
|
|
267
267
|
code;
|
|
268
|
-
|
|
268
|
+
/** Structured, JSON-serializable detail (e.g. validation issues). It travels with the failure to
|
|
269
|
+
* the run's error, a parent's `allSettled` item and an onError handler's input. */
|
|
270
|
+
details;
|
|
271
|
+
/** Read by the step result builders, so a dispatched step's retry policy honours it too. */
|
|
272
|
+
retryable = false;
|
|
273
|
+
constructor(message, code, details) {
|
|
269
274
|
super(message);
|
|
270
275
|
this.name = "FatalError";
|
|
271
276
|
this.code = code;
|
|
277
|
+
this.details = details;
|
|
272
278
|
}
|
|
273
279
|
};
|
|
274
280
|
var ConcurrencyLimitError = class extends Error {
|
|
@@ -346,6 +352,8 @@ var NonDeterminismError = class extends Error {
|
|
|
346
352
|
static {
|
|
347
353
|
__name(this, "NonDeterminismError");
|
|
348
354
|
}
|
|
355
|
+
/** Marks the failure as a broken deploy, which an `onError` route never takes. */
|
|
356
|
+
code = "non_determinism";
|
|
349
357
|
runId;
|
|
350
358
|
seq;
|
|
351
359
|
constructor(runId, seq, expected, recorded) {
|
|
@@ -757,6 +765,9 @@ async function runStepHandler(task, handler) {
|
|
|
757
765
|
...typeof e?.code === "string" ? {
|
|
758
766
|
code: e.code
|
|
759
767
|
} : {},
|
|
768
|
+
...e?.details !== void 0 ? {
|
|
769
|
+
details: e.details
|
|
770
|
+
} : {},
|
|
760
771
|
...typeof e?.retryable === "boolean" ? {
|
|
761
772
|
retryable: e.retryable
|
|
762
773
|
} : {}
|
|
@@ -833,11 +844,36 @@ __name(isFailedCompletion, "isFailedCompletion");
|
|
|
833
844
|
function unwrapCompletion(payload, label) {
|
|
834
845
|
const c = payload;
|
|
835
846
|
if (c && typeof c === "object" && "ok" in c && c.ok === false) {
|
|
836
|
-
throw new FatalError(`${label} failed: ${c.error}
|
|
847
|
+
throw new FatalError(`${label} failed: ${c.error}`, c.code, c.details);
|
|
837
848
|
}
|
|
838
849
|
return c?.value;
|
|
839
850
|
}
|
|
840
851
|
__name(unwrapCompletion, "unwrapCompletion");
|
|
852
|
+
function failedCompletion(error2) {
|
|
853
|
+
return {
|
|
854
|
+
ok: false,
|
|
855
|
+
error: error2.message,
|
|
856
|
+
...error2.code !== void 0 ? {
|
|
857
|
+
code: error2.code
|
|
858
|
+
} : {},
|
|
859
|
+
...error2.details !== void 0 ? {
|
|
860
|
+
details: error2.details
|
|
861
|
+
} : {}
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
__name(failedCompletion, "failedCompletion");
|
|
865
|
+
function completionError(c) {
|
|
866
|
+
return {
|
|
867
|
+
message: c.error,
|
|
868
|
+
...c.code !== void 0 ? {
|
|
869
|
+
code: c.code
|
|
870
|
+
} : {},
|
|
871
|
+
...c.details !== void 0 ? {
|
|
872
|
+
details: c.details
|
|
873
|
+
} : {}
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
__name(completionError, "completionError");
|
|
841
877
|
|
|
842
878
|
// src/concurrency.ts
|
|
843
879
|
var DEFAULT_CONCURRENCY_STATUSES = [
|
|
@@ -1083,8 +1119,9 @@ function allChildId(runId, firstSeq, index, wave) {
|
|
|
1083
1119
|
return `${allBase(runId, firstSeq, wave)}${index}`;
|
|
1084
1120
|
}
|
|
1085
1121
|
__name(allChildId, "allChildId");
|
|
1086
|
-
function allGroup(firstSeq, mode) {
|
|
1087
|
-
|
|
1122
|
+
function allGroup(firstSeq, size, mode) {
|
|
1123
|
+
const group = `all:${firstSeq}:n${size}`;
|
|
1124
|
+
return mode === "failFast" ? `${group}:failFast` : group;
|
|
1088
1125
|
}
|
|
1089
1126
|
__name(allGroup, "allGroup");
|
|
1090
1127
|
function stepFanGroup(firstSeq, size, mode) {
|
|
@@ -1093,14 +1130,26 @@ function stepFanGroup(firstSeq, size, mode) {
|
|
|
1093
1130
|
}
|
|
1094
1131
|
__name(stepFanGroup, "stepFanGroup");
|
|
1095
1132
|
function parseStepFanGroup(group) {
|
|
1096
|
-
const
|
|
1097
|
-
if (
|
|
1133
|
+
const fan = parseFanGroup(group);
|
|
1134
|
+
if (fan?.kind !== "steps" || fan.size === void 0) return void 0;
|
|
1098
1135
|
return {
|
|
1099
|
-
firstSeq:
|
|
1100
|
-
size:
|
|
1136
|
+
firstSeq: fan.firstSeq,
|
|
1137
|
+
size: fan.size
|
|
1101
1138
|
};
|
|
1102
1139
|
}
|
|
1103
1140
|
__name(parseStepFanGroup, "parseStepFanGroup");
|
|
1141
|
+
function parseFanGroup(group) {
|
|
1142
|
+
const m = group ? /^(all|steps):(\d+)(?::n?(\d+))?(:failFast)?$/.exec(group) : null;
|
|
1143
|
+
if (!m) return void 0;
|
|
1144
|
+
if (m[1] === "steps" && m[3] === void 0) return void 0;
|
|
1145
|
+
return {
|
|
1146
|
+
kind: m[1],
|
|
1147
|
+
firstSeq: Number(m[2]),
|
|
1148
|
+
size: m[3] === void 0 ? void 0 : Number(m[3]),
|
|
1149
|
+
failFast: m[4] !== void 0
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
__name(parseFanGroup, "parseFanGroup");
|
|
1104
1153
|
function allWait(runId, firstSeq, wave) {
|
|
1105
1154
|
const base = allBase(runId, firstSeq, wave);
|
|
1106
1155
|
return {
|
|
@@ -1145,12 +1194,12 @@ function allWaves(size, concurrency) {
|
|
|
1145
1194
|
};
|
|
1146
1195
|
}
|
|
1147
1196
|
__name(allWaves, "allWaves");
|
|
1148
|
-
var ENGINE_CHILD_ID = /^(.+)\.(?:all\.\d+\.(?:w\d+\.)?\d+|child\.\d+)$/;
|
|
1197
|
+
var ENGINE_CHILD_ID = /^(.+)\.(?:all\.\d+\.(?:w\d+\.)?\d+|child\.\d+|onError\.\d+)$/;
|
|
1149
1198
|
function parentRunIdOf(runId) {
|
|
1150
1199
|
return ENGINE_CHILD_ID.exec(runId)?.[1];
|
|
1151
1200
|
}
|
|
1152
1201
|
__name(parentRunIdOf, "parentRunIdOf");
|
|
1153
|
-
var GROUP = /^all:(\d+)(:failFast)?$/;
|
|
1202
|
+
var GROUP = /^all:(\d+)(?::n\d+)?(:failFast)?$/;
|
|
1154
1203
|
var REST = /^(?:w(\d+)\.)?\d+$/;
|
|
1155
1204
|
function fanOfWaiter(waiter) {
|
|
1156
1205
|
const group = waiter.parallelGroup ? GROUP.exec(waiter.parallelGroup) : null;
|
|
@@ -1230,7 +1279,11 @@ function canonicalizeRegistrations(regs) {
|
|
|
1230
1279
|
requires: r.requires ? [
|
|
1231
1280
|
...new Set(r.requires)
|
|
1232
1281
|
].sort() : null,
|
|
1233
|
-
origin: r.origin ?? null
|
|
1282
|
+
origin: r.origin ?? null,
|
|
1283
|
+
// Conditional, so a registration that states no routes keeps the hash it had before routes existed.
|
|
1284
|
+
...r.onError !== void 0 ? {
|
|
1285
|
+
onError: canonicalRoutes(r.onError)
|
|
1286
|
+
} : {}
|
|
1234
1287
|
}));
|
|
1235
1288
|
const byJson = /* @__PURE__ */ new Map();
|
|
1236
1289
|
for (const entry of canonical) byJson.set(JSON.stringify(entry), entry);
|
|
@@ -1239,6 +1292,20 @@ function canonicalizeRegistrations(regs) {
|
|
|
1239
1292
|
].sort().map((k) => byJson.get(k));
|
|
1240
1293
|
}
|
|
1241
1294
|
__name(canonicalizeRegistrations, "canonicalizeRegistrations");
|
|
1295
|
+
function canonicalRoutes(routes) {
|
|
1296
|
+
const byJson = /* @__PURE__ */ new Map();
|
|
1297
|
+
for (const r of routes) {
|
|
1298
|
+
const entry = {
|
|
1299
|
+
code: r.code ?? null,
|
|
1300
|
+
workflow: r.workflow
|
|
1301
|
+
};
|
|
1302
|
+
byJson.set(JSON.stringify(entry), entry);
|
|
1303
|
+
}
|
|
1304
|
+
return [
|
|
1305
|
+
...byJson.keys()
|
|
1306
|
+
].sort().map((k) => byJson.get(k));
|
|
1307
|
+
}
|
|
1308
|
+
__name(canonicalRoutes, "canonicalRoutes");
|
|
1242
1309
|
function canonicalizeForHash(d) {
|
|
1243
1310
|
const set = /* @__PURE__ */ __name((xs) => [
|
|
1244
1311
|
...new Set(xs)
|
|
@@ -1273,13 +1340,13 @@ function fnv1a64Hex(input) {
|
|
|
1273
1340
|
const OFFSET = 0xcbf29ce484222325n;
|
|
1274
1341
|
const PRIME = 0x100000001b3n;
|
|
1275
1342
|
const MASK = 0xffffffffffffffffn;
|
|
1276
|
-
let
|
|
1343
|
+
let hash2 = OFFSET;
|
|
1277
1344
|
for (let i = 0; i < input.length; i += 1) {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1345
|
+
hash2 ^= BigInt(input.charCodeAt(i) & 255);
|
|
1346
|
+
hash2 ^= BigInt(input.charCodeAt(i) >> 8 & 255);
|
|
1347
|
+
hash2 = hash2 * PRIME & MASK;
|
|
1281
1348
|
}
|
|
1282
|
-
return
|
|
1349
|
+
return hash2.toString(16).padStart(16, "0");
|
|
1283
1350
|
}
|
|
1284
1351
|
__name(fnv1a64Hex, "fnv1a64Hex");
|
|
1285
1352
|
function descriptorHash(descriptor) {
|
|
@@ -1395,6 +1462,20 @@ function aggregateAnnouncements(descriptors) {
|
|
|
1395
1462
|
const requires = [
|
|
1396
1463
|
...requiresByJson.values()
|
|
1397
1464
|
].sort((x, y) => x.join(",").localeCompare(y.join(",")));
|
|
1465
|
+
const routesByJson = /* @__PURE__ */ new Map();
|
|
1466
|
+
for (const a of announcements) {
|
|
1467
|
+
if (a.onError === void 0) continue;
|
|
1468
|
+
const canonical = canonicalRoutes(a.onError).map((r) => r.code === null ? {
|
|
1469
|
+
workflow: r.workflow
|
|
1470
|
+
} : {
|
|
1471
|
+
code: r.code,
|
|
1472
|
+
workflow: r.workflow
|
|
1473
|
+
});
|
|
1474
|
+
routesByJson.set(JSON.stringify(canonical), canonical);
|
|
1475
|
+
}
|
|
1476
|
+
const onError = [
|
|
1477
|
+
...routesByJson.keys()
|
|
1478
|
+
].sort().map((k) => routesByJson.get(k) ?? []);
|
|
1398
1479
|
const disagreements = [];
|
|
1399
1480
|
if (groups.length > 1) disagreements.push({
|
|
1400
1481
|
axis: "group",
|
|
@@ -1410,6 +1491,14 @@ function aggregateAnnouncements(descriptors) {
|
|
|
1410
1491
|
values: requires.map((r) => r.join(","))
|
|
1411
1492
|
});
|
|
1412
1493
|
}
|
|
1494
|
+
if (onError.length > 1) {
|
|
1495
|
+
disagreements.push({
|
|
1496
|
+
axis: "onError",
|
|
1497
|
+
values: [
|
|
1498
|
+
...routesByJson.keys()
|
|
1499
|
+
].sort()
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1413
1502
|
const first = announcements[0];
|
|
1414
1503
|
if (!first) continue;
|
|
1415
1504
|
out.push({
|
|
@@ -1422,6 +1511,7 @@ function aggregateAnnouncements(descriptors) {
|
|
|
1422
1511
|
groups,
|
|
1423
1512
|
origins,
|
|
1424
1513
|
requires,
|
|
1514
|
+
onError,
|
|
1425
1515
|
runtimes: [
|
|
1426
1516
|
...new Set(announcements.map((a) => a.runtime))
|
|
1427
1517
|
].sort(),
|
|
@@ -1455,6 +1545,7 @@ function observedAnnouncements(heartbeats, covered) {
|
|
|
1455
1545
|
],
|
|
1456
1546
|
origins: [],
|
|
1457
1547
|
requires: [],
|
|
1548
|
+
onError: [],
|
|
1458
1549
|
runtimes: [],
|
|
1459
1550
|
instances: [
|
|
1460
1551
|
...instances
|
|
@@ -1813,6 +1904,13 @@ function planDispatch(requires, liveDescriptors, cp, token) {
|
|
|
1813
1904
|
__name(planDispatch, "planDispatch");
|
|
1814
1905
|
|
|
1815
1906
|
// src/interfaces.ts
|
|
1907
|
+
function checkpointNameKind(name) {
|
|
1908
|
+
if (name.startsWith("signal:child:")) return "child";
|
|
1909
|
+
if (name.startsWith("fan:")) return "folded";
|
|
1910
|
+
if (name.startsWith("spawn:")) return "spawn";
|
|
1911
|
+
return "step";
|
|
1912
|
+
}
|
|
1913
|
+
__name(checkpointNameKind, "checkpointNameKind");
|
|
1816
1914
|
var TERMINAL_RUN_STATUSES = [
|
|
1817
1915
|
"completed",
|
|
1818
1916
|
"failed",
|
|
@@ -1825,6 +1923,71 @@ function isNonEmptyRunScope(scope) {
|
|
|
1825
1923
|
}
|
|
1826
1924
|
__name(isNonEmptyRunScope, "isNonEmptyRunScope");
|
|
1827
1925
|
|
|
1926
|
+
// src/on-error.ts
|
|
1927
|
+
var UNROUTABLE_CODES = /* @__PURE__ */ new Set([
|
|
1928
|
+
"non_determinism",
|
|
1929
|
+
"nested_workflow_call"
|
|
1930
|
+
]);
|
|
1931
|
+
var HANDOFF_SEQ_BASE = 1e9;
|
|
1932
|
+
var onErrorRunId = /* @__PURE__ */ __name((runId, attempt) => `${runId}.onError.${attempt}`, "onErrorRunId");
|
|
1933
|
+
var handoffName = /* @__PURE__ */ __name((handlerId) => `signal:child:${handlerId}`, "handoffName");
|
|
1934
|
+
var retiredHandoffName = /* @__PURE__ */ __name((handlerId) => `onError:retried:${handlerId}`, "retiredHandoffName");
|
|
1935
|
+
function handoffHandlerId(runId, cp) {
|
|
1936
|
+
const prefix = `signal:child:${runId}.onError.`;
|
|
1937
|
+
return cp.seq >= HANDOFF_SEQ_BASE && cp.name.startsWith(prefix) ? cp.name.slice("signal:child:".length) : void 0;
|
|
1938
|
+
}
|
|
1939
|
+
__name(handoffHandlerId, "handoffHandlerId");
|
|
1940
|
+
function liveHandoff(runId, checkpoints) {
|
|
1941
|
+
let live;
|
|
1942
|
+
for (const cp of checkpoints) {
|
|
1943
|
+
if (handoffHandlerId(runId, cp) !== void 0 && (!live || cp.seq > live.seq)) live = cp;
|
|
1944
|
+
}
|
|
1945
|
+
return live;
|
|
1946
|
+
}
|
|
1947
|
+
__name(liveHandoff, "liveHandoff");
|
|
1948
|
+
function routeFor(routes, error2) {
|
|
1949
|
+
if (!routes?.length || error2.code && UNROUTABLE_CODES.has(error2.code)) return void 0;
|
|
1950
|
+
return (error2.code ? routes.find((r) => r.code === error2.code) : void 0) ?? routes.find((r) => r.code === void 0);
|
|
1951
|
+
}
|
|
1952
|
+
__name(routeFor, "routeFor");
|
|
1953
|
+
function assertRoutes(workflow, routes) {
|
|
1954
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1955
|
+
for (const r of routes) {
|
|
1956
|
+
if (seen.has(r.code)) {
|
|
1957
|
+
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}"`);
|
|
1958
|
+
}
|
|
1959
|
+
seen.add(r.code);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
__name(assertRoutes, "assertRoutes");
|
|
1963
|
+
function codeOf(err) {
|
|
1964
|
+
const e = err;
|
|
1965
|
+
return [
|
|
1966
|
+
e?.code,
|
|
1967
|
+
e?.stepError?.code,
|
|
1968
|
+
e?.error?.code
|
|
1969
|
+
].find((c) => typeof c === "string");
|
|
1970
|
+
}
|
|
1971
|
+
__name(codeOf, "codeOf");
|
|
1972
|
+
function detailsOf(err) {
|
|
1973
|
+
const e = err;
|
|
1974
|
+
return e?.details ?? e?.stepError?.details ?? e?.error?.details;
|
|
1975
|
+
}
|
|
1976
|
+
__name(detailsOf, "detailsOf");
|
|
1977
|
+
function codeAndDetails(err) {
|
|
1978
|
+
const code = codeOf(err);
|
|
1979
|
+
const details = detailsOf(err);
|
|
1980
|
+
return {
|
|
1981
|
+
...code !== void 0 ? {
|
|
1982
|
+
code
|
|
1983
|
+
} : {},
|
|
1984
|
+
...details !== void 0 ? {
|
|
1985
|
+
details
|
|
1986
|
+
} : {}
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
__name(codeAndDetails, "codeAndDetails");
|
|
1990
|
+
|
|
1828
1991
|
// src/tenant-group.ts
|
|
1829
1992
|
function tenantGroup(baseGroup, tenant) {
|
|
1830
1993
|
return tenant !== void 0 && tenant !== "" && tenant !== "default" ? `${baseGroup}@${tenant}` : baseGroup;
|
|
@@ -2884,7 +3047,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
2884
3047
|
kind: "local",
|
|
2885
3048
|
error: {
|
|
2886
3049
|
message: err instanceof Error ? err.message : String(err),
|
|
2887
|
-
stack: err instanceof Error ? err.stack : void 0
|
|
3050
|
+
stack: err instanceof Error ? err.stack : void 0,
|
|
3051
|
+
...codeAndDetails(err)
|
|
2888
3052
|
},
|
|
2889
3053
|
events,
|
|
2890
3054
|
attempts: attempt,
|
|
@@ -3140,17 +3304,17 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3140
3304
|
}
|
|
3141
3305
|
]));
|
|
3142
3306
|
}, "child");
|
|
3143
|
-
const
|
|
3307
|
+
const fan = /* @__PURE__ */ __name(async (workflow2, inputs, opts, settle) => {
|
|
3144
3308
|
if (inputs.length === 0) return [];
|
|
3145
3309
|
if (stepNameOf(workflow2) !== void 0) {
|
|
3146
|
-
return allSteps(workflow2, inputs, opts);
|
|
3310
|
+
return allSteps(workflow2, inputs, opts, settle);
|
|
3147
3311
|
}
|
|
3148
|
-
const mode = opts?.mode ?? "waitAll";
|
|
3312
|
+
const mode = settle ? "waitAll" : opts?.mode ?? "waitAll";
|
|
3149
3313
|
const name = workflowName(workflow2);
|
|
3150
3314
|
const waves = allWaves(inputs.length, opts?.concurrency);
|
|
3151
3315
|
const positions = inputs.map(() => pos.next());
|
|
3152
3316
|
const firstSeq = positions[0];
|
|
3153
|
-
const group = allGroup(firstSeq, mode);
|
|
3317
|
+
const group = allGroup(firstSeq, inputs.length, mode);
|
|
3154
3318
|
const waveOf = /* @__PURE__ */ __name((i) => waves ? Math.floor(i / waves.waveSize) : void 0, "waveOf");
|
|
3155
3319
|
const id = /* @__PURE__ */ __name((i) => allChildId(runId, firstSeq, i, waveOf(i)), "id");
|
|
3156
3320
|
const { checkpoints: existing, folded } = await readFan(positions, [
|
|
@@ -3267,24 +3431,45 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3267
3431
|
throw new WorkflowSuspended(void 0, fanJoin(active.map((k) => allWait(runId, firstSeq, waves ? k : void 0))));
|
|
3268
3432
|
}
|
|
3269
3433
|
const outputs = [];
|
|
3434
|
+
const results = [];
|
|
3270
3435
|
const failures = [];
|
|
3271
3436
|
for (let i = 0; i < inputs.length; i += 1) {
|
|
3272
3437
|
const c = existing[i]?.output;
|
|
3273
3438
|
if (c && typeof c === "object" && "ok" in c && c.ok === false) {
|
|
3439
|
+
const { message, ...rest } = completionError({
|
|
3440
|
+
...c,
|
|
3441
|
+
error: c.error ?? "unknown"
|
|
3442
|
+
});
|
|
3443
|
+
const failure = {
|
|
3444
|
+
id: id(i),
|
|
3445
|
+
error: message,
|
|
3446
|
+
...rest
|
|
3447
|
+
};
|
|
3274
3448
|
failures.push({
|
|
3275
3449
|
index: i,
|
|
3276
|
-
|
|
3277
|
-
|
|
3450
|
+
...failure
|
|
3451
|
+
});
|
|
3452
|
+
results.push({
|
|
3453
|
+
status: "rejected",
|
|
3454
|
+
...failure
|
|
3278
3455
|
});
|
|
3279
3456
|
outputs.push(void 0);
|
|
3280
3457
|
} else {
|
|
3281
|
-
|
|
3458
|
+
const value = c?.value;
|
|
3459
|
+
results.push({
|
|
3460
|
+
status: "fulfilled",
|
|
3461
|
+
value
|
|
3462
|
+
});
|
|
3463
|
+
outputs.push(value);
|
|
3282
3464
|
}
|
|
3283
3465
|
}
|
|
3466
|
+
if (settle) return results;
|
|
3284
3467
|
if (failures.length > 0) throw new GatherError(failures, inputs.length);
|
|
3285
3468
|
return outputs;
|
|
3286
|
-
}, "
|
|
3287
|
-
const
|
|
3469
|
+
}, "fan");
|
|
3470
|
+
const all = /* @__PURE__ */ __name((workflow2, inputs, opts) => fan(workflow2, inputs, opts, false), "all");
|
|
3471
|
+
const allSettled = /* @__PURE__ */ __name((workflow2, inputs, opts) => fan(workflow2, inputs, opts, true), "allSettled");
|
|
3472
|
+
const allSteps = /* @__PURE__ */ __name(async (ref, inputs, opts, settle) => {
|
|
3288
3473
|
if (opts?.queue !== void 0) {
|
|
3289
3474
|
throw new Error("ctx.all: a flow-control queue is not supported on a step fan");
|
|
3290
3475
|
}
|
|
@@ -3295,7 +3480,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3295
3480
|
};
|
|
3296
3481
|
const positions = inputs.map(() => pos.next());
|
|
3297
3482
|
return host.callRemoteAll(runId, positions, def, inputs, {
|
|
3298
|
-
mode: opts?.mode ?? "waitAll",
|
|
3483
|
+
mode: settle ? "waitAll" : opts?.mode ?? "waitAll",
|
|
3484
|
+
settle,
|
|
3299
3485
|
concurrency: opts?.concurrency,
|
|
3300
3486
|
transport: opts?.transport,
|
|
3301
3487
|
admission: {
|
|
@@ -3613,6 +3799,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3613
3799
|
task: guard("task", task),
|
|
3614
3800
|
child: guard("child", child),
|
|
3615
3801
|
all: guard("all", all),
|
|
3802
|
+
allSettled: guard("allSettled", allSettled),
|
|
3616
3803
|
startChild: guard("startChild", startChild),
|
|
3617
3804
|
breakpoint: guard("breakpoint", breakpoint),
|
|
3618
3805
|
webhook: guardedWebhook,
|
|
@@ -3638,9 +3825,11 @@ function mergeTags(staticTags, runTags) {
|
|
|
3638
3825
|
__name(mergeTags, "mergeTags");
|
|
3639
3826
|
var BREAKPOINT = "breakpoint";
|
|
3640
3827
|
var isBreakpoint = /* @__PURE__ */ __name((cp) => cp.status === "pending" && cp.name.startsWith(BREAKPOINT), "isBreakpoint");
|
|
3828
|
+
var ANNOUNCED_ROUTES_TTL_MS = 5e3;
|
|
3641
3829
|
var VERSION_UNDECLARED_TAG = "version:undeclared";
|
|
3642
3830
|
var versionKey = /* @__PURE__ */ __name((name, version) => `${name}@${version}`, "versionKey");
|
|
3643
3831
|
var STEP_FAN_CHUNK = 200;
|
|
3832
|
+
var RESULT_BATCH_MAX = 256;
|
|
3644
3833
|
function shuffle(items) {
|
|
3645
3834
|
for (let i = items.length - 1; i > 0; i -= 1) {
|
|
3646
3835
|
const j = Math.floor(Math.random() * (i + 1));
|
|
@@ -3748,6 +3937,8 @@ var WorkflowEngine = class {
|
|
|
3748
3937
|
updateValidators = /* @__PURE__ */ new Map();
|
|
3749
3938
|
/** Runs being cancelled WITH saga compensation — see `cancel({ compensate: true })`. */
|
|
3750
3939
|
cancelRequested = /* @__PURE__ */ new Set();
|
|
3940
|
+
/** {@link announcedRoutes} reads, by group + workflow + version. */
|
|
3941
|
+
announcedRoutesCache = /* @__PURE__ */ new Map();
|
|
3751
3942
|
/**
|
|
3752
3943
|
* Seqs of the dispatched remote steps a turn suspended on, by run, while that turn executes on
|
|
3753
3944
|
* THIS instance — so the turn can re-check them once its lease is released (see
|
|
@@ -3769,6 +3960,8 @@ var WorkflowEngine = class {
|
|
|
3769
3960
|
stepFansFiredHere = /* @__PURE__ */ new Map();
|
|
3770
3961
|
/** The tail of each run's queue of results being handled on this instance — see {@link serializedPerRun}. */
|
|
3771
3962
|
resultChains = /* @__PURE__ */ new Map();
|
|
3963
|
+
/** Each run's results waiting to be settled on this instance — see {@link completeRemoteResult}. */
|
|
3964
|
+
resultQueues = /* @__PURE__ */ new Map();
|
|
3772
3965
|
/** Flow-control admission backend for remote steps (see {@link registerQueue}). */
|
|
3773
3966
|
admission;
|
|
3774
3967
|
/** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
|
|
@@ -3975,6 +4168,7 @@ var WorkflowEngine = class {
|
|
|
3975
4168
|
name,
|
|
3976
4169
|
version,
|
|
3977
4170
|
fn,
|
|
4171
|
+
onError: this.checkedRoutes(name, opts?.onError),
|
|
3978
4172
|
tags: opts?.tags,
|
|
3979
4173
|
singleton: opts?.singleton,
|
|
3980
4174
|
concurrency: opts?.concurrency,
|
|
@@ -4044,6 +4238,7 @@ var WorkflowEngine = class {
|
|
|
4044
4238
|
executionTimeoutMs: opts.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
|
|
4045
4239
|
validateInput: opts.validateInput,
|
|
4046
4240
|
requires: opts.requires,
|
|
4241
|
+
onError: this.checkedRoutes(name, opts.onError),
|
|
4047
4242
|
remote: {
|
|
4048
4243
|
group: opts.group,
|
|
4049
4244
|
executor: opts.executor
|
|
@@ -4821,6 +5016,24 @@ var WorkflowEngine = class {
|
|
|
4821
5016
|
if (resurrecting) {
|
|
4822
5017
|
const isFailureCompletion = /* @__PURE__ */ __name((v) => typeof v === "object" && v !== null && v.ok === false, "isFailureCompletion");
|
|
4823
5018
|
for (const cp of await this.store.listCheckpoints(runId)) {
|
|
5019
|
+
const handlerId = handoffHandlerId(runId, cp);
|
|
5020
|
+
if (handlerId !== void 0) {
|
|
5021
|
+
await this.store.takeSignalWaiter(`child:${handlerId}`);
|
|
5022
|
+
await this.store.saveCheckpoint({
|
|
5023
|
+
...cp,
|
|
5024
|
+
name: retiredHandoffName(handlerId),
|
|
5025
|
+
status: "completed"
|
|
5026
|
+
});
|
|
5027
|
+
const tags = run.tags?.filter((t) => !t.startsWith("onError:"));
|
|
5028
|
+
if (tags && tags.length !== run.tags?.length) {
|
|
5029
|
+
await this.store.updateRun(runId, {
|
|
5030
|
+
tags,
|
|
5031
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
5032
|
+
});
|
|
5033
|
+
run.tags = tags;
|
|
5034
|
+
}
|
|
5035
|
+
continue;
|
|
5036
|
+
}
|
|
4824
5037
|
if (cp.status === "failed") {
|
|
4825
5038
|
await this.store.saveCheckpoint({
|
|
4826
5039
|
...cp,
|
|
@@ -4965,13 +5178,13 @@ var WorkflowEngine = class {
|
|
|
4965
5178
|
* `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
|
|
4966
5179
|
*/
|
|
4967
5180
|
waitForRun(runId, opts) {
|
|
4968
|
-
const
|
|
5181
|
+
const TERMINAL2 = /* @__PURE__ */ new Set([
|
|
4969
5182
|
"completed",
|
|
4970
5183
|
"failed",
|
|
4971
5184
|
"cancelled",
|
|
4972
5185
|
"dead"
|
|
4973
5186
|
]);
|
|
4974
|
-
const isSettled = /* @__PURE__ */ __name((run) => opts?.until === "terminal" ?
|
|
5187
|
+
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");
|
|
4975
5188
|
const toResult = /* @__PURE__ */ __name((run) => ({
|
|
4976
5189
|
runId,
|
|
4977
5190
|
status: run.status,
|
|
@@ -5226,10 +5439,7 @@ var WorkflowEngine = class {
|
|
|
5226
5439
|
async redriveSettledStepFans(runId, fans) {
|
|
5227
5440
|
for (const fan of fans) {
|
|
5228
5441
|
if (this.stepFansFiredHere.has(`${runId}:${fan.from}`)) continue;
|
|
5229
|
-
|
|
5230
|
-
"pending"
|
|
5231
|
-
]) : (await this.store.listCheckpoints(runId)).filter((c) => c.seq >= fan.from && c.seq <= fan.to && c.status === "pending").length;
|
|
5232
|
-
if (pending > 0) continue;
|
|
5442
|
+
if (await this.stepFanPending(runId, fan.from, fan.to) > 0) continue;
|
|
5233
5443
|
const nowMs = this.clock();
|
|
5234
5444
|
if (!await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs)) {
|
|
5235
5445
|
return;
|
|
@@ -5939,6 +6149,209 @@ var WorkflowEngine = class {
|
|
|
5939
6149
|
* us back here (a remote step finished, a timer fired) goes through `resume` like any TS workflow.
|
|
5940
6150
|
*/
|
|
5941
6151
|
/**
|
|
6152
|
+
* The registered onError routes whose handler workflow nothing can run: not registered on this
|
|
6153
|
+
* engine, and no live worker group serves it. Meant for a boot-time warning: a typo in a handler's
|
|
6154
|
+
* name would otherwise surface only when the first run fails.
|
|
6155
|
+
*/
|
|
6156
|
+
async unservedOnErrorRoutes() {
|
|
6157
|
+
const unserved = [];
|
|
6158
|
+
let groups;
|
|
6159
|
+
for (const registered of this.workflows.values()) {
|
|
6160
|
+
for (const route of registered.onError ?? []) {
|
|
6161
|
+
if (this.latest.has(route.workflow)) continue;
|
|
6162
|
+
groups ??= await this.pool.listWorkerGroups().catch(() => []);
|
|
6163
|
+
const token = sanitizeQueueToken(route.workflow);
|
|
6164
|
+
if (groups.some((g) => g === token || g.startsWith(`${token}@`))) continue;
|
|
6165
|
+
unserved.push({
|
|
6166
|
+
workflow: registered.name,
|
|
6167
|
+
route
|
|
6168
|
+
});
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
6171
|
+
return unserved;
|
|
6172
|
+
}
|
|
6173
|
+
/** Validate a workflow's {@link OnErrorRoute}s at registration. */
|
|
6174
|
+
checkedRoutes(name, routes) {
|
|
6175
|
+
if (!routes?.length) return void 0;
|
|
6176
|
+
assertRoutes(name, routes);
|
|
6177
|
+
return routes;
|
|
6178
|
+
}
|
|
6179
|
+
/**
|
|
6180
|
+
* Route a failed run to its {@link OnErrorRoute} handler, read from the workflow's local registration
|
|
6181
|
+
* or, for one served by convention, from what its live workers announce. Starts the handler run and
|
|
6182
|
+
* waits for it the way a parent waits for a `ctx.child`: a `child:<handlerId>` waiter plus a
|
|
6183
|
+
* `running` placeholder checkpoint carrying the handler's name and input, so the handler's terminal
|
|
6184
|
+
* state wakes the run (buffered and reconciled like any child). From then on every execution of the
|
|
6185
|
+
* run only follows the handler ({@link followHandoff}); the body never runs again. Undefined when no
|
|
6186
|
+
* route matches: the run fails as usual.
|
|
6187
|
+
*
|
|
6188
|
+
* The placeholder sits past every seq the body used and is left out of {@link remoteHistory}.
|
|
6189
|
+
*/
|
|
6190
|
+
async routeFailure(run, error2) {
|
|
6191
|
+
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion));
|
|
6192
|
+
const routes = registered ? registered.onError : await this.announcedRoutes(run, error2);
|
|
6193
|
+
const route = routeFor(routes, error2);
|
|
6194
|
+
if (!route) return void 0;
|
|
6195
|
+
const checkpoints = await this.handoffCheckpoints(run.id);
|
|
6196
|
+
const live = liveHandoff(run.id, checkpoints);
|
|
6197
|
+
if (live) return this.followHandoff(run, live);
|
|
6198
|
+
const attempt = checkpoints.length;
|
|
6199
|
+
const seq = HANDOFF_SEQ_BASE + attempt;
|
|
6200
|
+
const handlerId = onErrorRunId(run.id, attempt);
|
|
6201
|
+
const handler = {
|
|
6202
|
+
workflow: route.workflow,
|
|
6203
|
+
input: {
|
|
6204
|
+
runId: run.id,
|
|
6205
|
+
workflow: run.workflow,
|
|
6206
|
+
input: run.input,
|
|
6207
|
+
error: error2
|
|
6208
|
+
}
|
|
6209
|
+
};
|
|
6210
|
+
await this.store.putSignalWaiter({
|
|
6211
|
+
token: `child:${handlerId}`,
|
|
6212
|
+
runId: run.id,
|
|
6213
|
+
seq
|
|
6214
|
+
});
|
|
6215
|
+
await this.store.saveCheckpoint({
|
|
6216
|
+
...instantCheckpoint({
|
|
6217
|
+
runId: run.id,
|
|
6218
|
+
seq,
|
|
6219
|
+
name: handoffName(handlerId),
|
|
6220
|
+
kind: "signal",
|
|
6221
|
+
status: "running"
|
|
6222
|
+
}),
|
|
6223
|
+
input: handler
|
|
6224
|
+
});
|
|
6225
|
+
const tag = `onError:${route.code ?? "*"}`;
|
|
6226
|
+
if (!run.tags?.includes(tag)) {
|
|
6227
|
+
const tags = [
|
|
6228
|
+
...run.tags ?? [],
|
|
6229
|
+
tag
|
|
6230
|
+
];
|
|
6231
|
+
await this.store.updateRun(run.id, {
|
|
6232
|
+
tags,
|
|
6233
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6234
|
+
});
|
|
6235
|
+
run.tags = tags;
|
|
6236
|
+
}
|
|
6237
|
+
this.emit({
|
|
6238
|
+
type: "run.routed",
|
|
6239
|
+
runId: run.id,
|
|
6240
|
+
workflow: run.workflow,
|
|
6241
|
+
namespace: run.namespace,
|
|
6242
|
+
name: route.workflow,
|
|
6243
|
+
error: error2
|
|
6244
|
+
});
|
|
6245
|
+
this.startChildDeferred(handler.workflow, handler.input, handlerId, {
|
|
6246
|
+
namespace: run.namespace
|
|
6247
|
+
});
|
|
6248
|
+
return {
|
|
6249
|
+
kind: "suspended"
|
|
6250
|
+
};
|
|
6251
|
+
}
|
|
6252
|
+
/** A run's handoff checkpoints, live and retired: the only ones at {@link HANDOFF_SEQ_BASE} and up. */
|
|
6253
|
+
async handoffCheckpoints(runId) {
|
|
6254
|
+
const handoffs = await this.store.listCheckpointsByNamePrefix?.(runId, [
|
|
6255
|
+
`signal:child:${runId}.onError.`,
|
|
6256
|
+
`onError:retried:${runId}.onError.`
|
|
6257
|
+
]);
|
|
6258
|
+
return (handoffs ?? await this.store.listCheckpoints(runId)).filter((cp) => cp.seq >= HANDOFF_SEQ_BASE);
|
|
6259
|
+
}
|
|
6260
|
+
/**
|
|
6261
|
+
* Where a run that handed its failure off stands: the handler's outcome once it settled (from the
|
|
6262
|
+
* wake's checkpoint, or off the handler run when the wake never arrived), else still waiting. Starts
|
|
6263
|
+
* the handler if the turn that handed off crashed before its deferred start ran.
|
|
6264
|
+
*/
|
|
6265
|
+
async followHandoff(run, handoff) {
|
|
6266
|
+
if (handoff.status === "completed") {
|
|
6267
|
+
const outcome = handoff.output;
|
|
6268
|
+
return outcome.ok ? {
|
|
6269
|
+
kind: "completed",
|
|
6270
|
+
output: outcome.value
|
|
6271
|
+
} : {
|
|
6272
|
+
kind: "failed",
|
|
6273
|
+
error: completionError(outcome)
|
|
6274
|
+
};
|
|
6275
|
+
}
|
|
6276
|
+
const handlerId = handoffHandlerId(run.id, handoff);
|
|
6277
|
+
const handlerRun = await this.store.getRun(handlerId);
|
|
6278
|
+
if (handlerRun?.status === "completed") return {
|
|
6279
|
+
kind: "completed",
|
|
6280
|
+
output: handlerRun.output
|
|
6281
|
+
};
|
|
6282
|
+
if (handlerRun && TERMINAL_RUN_STATUSES.includes(handlerRun.status)) {
|
|
6283
|
+
return {
|
|
6284
|
+
kind: "failed",
|
|
6285
|
+
error: handlerRun.error ?? {
|
|
6286
|
+
message: `onError handler ${handlerId} ${handlerRun.status}`
|
|
6287
|
+
}
|
|
6288
|
+
};
|
|
6289
|
+
}
|
|
6290
|
+
if (!handlerRun) {
|
|
6291
|
+
const handler = handoff.input;
|
|
6292
|
+
this.startChildDeferred(handler.workflow, handler.input, handlerId, {
|
|
6293
|
+
namespace: run.namespace
|
|
6294
|
+
});
|
|
6295
|
+
}
|
|
6296
|
+
return {
|
|
6297
|
+
kind: "suspended"
|
|
6298
|
+
};
|
|
6299
|
+
}
|
|
6300
|
+
/**
|
|
6301
|
+
* The onError routes the live workers announce for a workflow the engine serves by convention: the
|
|
6302
|
+
* one set they agree on, or none when they state none or cannot be read. Workers that announce
|
|
6303
|
+
* different sets (a rollout in progress) route nothing, and the run's failure is reported as a
|
|
6304
|
+
* `run.unrouted` event. Announcers of another version of the workflow are ignored. The read is
|
|
6305
|
+
* cached for {@link ANNOUNCED_ROUTES_TTL_MS}, so a burst of failures costs one descriptor read.
|
|
6306
|
+
*/
|
|
6307
|
+
async announcedRoutes(run, error2) {
|
|
6308
|
+
const group = tenantGroup(sanitizeQueueToken(run.workflow), run.namespace);
|
|
6309
|
+
const key = `${group}
|
|
6310
|
+
${run.workflow}
|
|
6311
|
+
${run.workflowVersion}`;
|
|
6312
|
+
const now = this.clock();
|
|
6313
|
+
let cached = this.announcedRoutesCache.get(key);
|
|
6314
|
+
if (!cached || now - cached.at > ANNOUNCED_ROUTES_TTL_MS) {
|
|
6315
|
+
cached = {
|
|
6316
|
+
at: now,
|
|
6317
|
+
sets: await this.readAnnouncedRoutes(run, group)
|
|
6318
|
+
};
|
|
6319
|
+
this.announcedRoutesCache.set(key, cached);
|
|
6320
|
+
}
|
|
6321
|
+
if (cached.sets.length > 1) {
|
|
6322
|
+
this.emit({
|
|
6323
|
+
type: "run.unrouted",
|
|
6324
|
+
runId: run.id,
|
|
6325
|
+
workflow: run.workflow,
|
|
6326
|
+
namespace: run.namespace,
|
|
6327
|
+
error: error2,
|
|
6328
|
+
reason: `live workers announce ${cached.sets.length} different onError route sets`
|
|
6329
|
+
});
|
|
6330
|
+
return void 0;
|
|
6331
|
+
}
|
|
6332
|
+
return cached.sets[0];
|
|
6333
|
+
}
|
|
6334
|
+
/** Every distinct onError route set the live workers of `group` announce for the run's workflow. */
|
|
6335
|
+
async readAnnouncedRoutes(run, group) {
|
|
6336
|
+
const transport = await this.pool.transportWithLiveGroup(group);
|
|
6337
|
+
if (!transport?.readWorkerDescriptors) return [];
|
|
6338
|
+
let descriptors;
|
|
6339
|
+
try {
|
|
6340
|
+
descriptors = await transport.readWorkerDescriptors(group);
|
|
6341
|
+
} catch {
|
|
6342
|
+
return [];
|
|
6343
|
+
}
|
|
6344
|
+
const sets = /* @__PURE__ */ new Map();
|
|
6345
|
+
for (const announced of aggregateAnnouncements(descriptors)) {
|
|
6346
|
+
if (announced.name !== run.workflow) continue;
|
|
6347
|
+
if (announced.version !== void 0 && announced.version !== run.workflowVersion) continue;
|
|
6348
|
+
for (const routes of announced.onError) sets.set(JSON.stringify(routes), routes);
|
|
6349
|
+
}
|
|
6350
|
+
return [
|
|
6351
|
+
...sets.values()
|
|
6352
|
+
];
|
|
6353
|
+
}
|
|
6354
|
+
/**
|
|
5942
6355
|
* Apply a terminal/suspended transition once: persist the new run status, emit the matching
|
|
5943
6356
|
* lifecycle event, and (on completion/failure) wake a waiting parent. Both the TS and remote
|
|
5944
6357
|
* executors funnel their completed/failed/suspended outcomes through here so a status update can
|
|
@@ -5972,9 +6385,12 @@ var WorkflowEngine = class {
|
|
|
5972
6385
|
};
|
|
5973
6386
|
}
|
|
5974
6387
|
if (outcome.kind === "failed") {
|
|
6388
|
+
const handed = await this.routeFailure(run, outcome.error);
|
|
6389
|
+
if (handed && handed.kind !== "failed") return this.settleRun(run, handed);
|
|
6390
|
+
const error2 = handed?.error ?? outcome.error;
|
|
5975
6391
|
await this.store.updateRun(run.id, {
|
|
5976
6392
|
status: "failed",
|
|
5977
|
-
error:
|
|
6393
|
+
error: error2,
|
|
5978
6394
|
updatedAt
|
|
5979
6395
|
});
|
|
5980
6396
|
this.emit({
|
|
@@ -5982,16 +6398,13 @@ var WorkflowEngine = class {
|
|
|
5982
6398
|
runId: run.id,
|
|
5983
6399
|
workflow: run.workflow,
|
|
5984
6400
|
namespace: run.namespace,
|
|
5985
|
-
error:
|
|
5986
|
-
});
|
|
5987
|
-
void this.notifyParent(run.id, {
|
|
5988
|
-
ok: false,
|
|
5989
|
-
error: outcome.error.message
|
|
6401
|
+
error: error2
|
|
5990
6402
|
});
|
|
6403
|
+
void this.notifyParent(run.id, failedCompletion(error2));
|
|
5991
6404
|
return {
|
|
5992
6405
|
runId: run.id,
|
|
5993
6406
|
status: "failed",
|
|
5994
|
-
error:
|
|
6407
|
+
error: error2
|
|
5995
6408
|
};
|
|
5996
6409
|
}
|
|
5997
6410
|
if (outcome.kind === "blocked") return this.parkBlocked(run, outcome.blocked);
|
|
@@ -6080,7 +6493,14 @@ var WorkflowEngine = class {
|
|
|
6080
6493
|
}
|
|
6081
6494
|
const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
|
|
6082
6495
|
if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
|
|
6083
|
-
const
|
|
6496
|
+
const checkpoints = await this.store.listCheckpoints(run.id);
|
|
6497
|
+
const handoff = liveHandoff(run.id, checkpoints);
|
|
6498
|
+
if (handoff) {
|
|
6499
|
+
const result = await this.settleRun(run, await this.followHandoff(run, handoff));
|
|
6500
|
+
await this.store.releaseRunLock(run.id);
|
|
6501
|
+
return result;
|
|
6502
|
+
}
|
|
6503
|
+
const history = await this.remoteHistory(run.id, checkpoints);
|
|
6084
6504
|
if (remote.executor.dispatch) {
|
|
6085
6505
|
const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
|
|
6086
6506
|
const silenceMs = this.remoteAdvanceSilenceMs;
|
|
@@ -6404,8 +6824,8 @@ var WorkflowEngine = class {
|
|
|
6404
6824
|
}
|
|
6405
6825
|
}
|
|
6406
6826
|
/** The run's resolved durable ops as replay inputs: completed/failed steps + elapsed timers. */
|
|
6407
|
-
async remoteHistory(runId) {
|
|
6408
|
-
const checkpoints = await this.store.listCheckpoints(runId);
|
|
6827
|
+
async remoteHistory(runId, listed) {
|
|
6828
|
+
const checkpoints = listed ?? await this.store.listCheckpoints(runId);
|
|
6409
6829
|
const kindOf = {
|
|
6410
6830
|
remote: "call",
|
|
6411
6831
|
local: "step",
|
|
@@ -6415,15 +6835,14 @@ var WorkflowEngine = class {
|
|
|
6415
6835
|
const events = [];
|
|
6416
6836
|
for (const cp of checkpoints) {
|
|
6417
6837
|
if (cp.status === "completed" || cp.status === "failed") {
|
|
6838
|
+
if (cp.seq >= HANDOFF_SEQ_BASE) continue;
|
|
6418
6839
|
if (cp.kind === "signal" && cp.name.startsWith("signal:child:")) {
|
|
6419
6840
|
const completion = cp.output;
|
|
6420
6841
|
events.push({
|
|
6421
6842
|
seq: cp.seq,
|
|
6422
6843
|
kind: "child",
|
|
6423
6844
|
output: completion?.ok ? completion.value : void 0,
|
|
6424
|
-
error: completion && completion.ok === false ?
|
|
6425
|
-
message: completion.error
|
|
6426
|
-
} : void 0
|
|
6845
|
+
error: completion && completion.ok === false ? completionError(completion) : void 0
|
|
6427
6846
|
});
|
|
6428
6847
|
continue;
|
|
6429
6848
|
}
|
|
@@ -6704,6 +7123,10 @@ var WorkflowEngine = class {
|
|
|
6704
7123
|
this.awaitedStepFans.set(run.id, awaitedFans);
|
|
6705
7124
|
const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow, registered?.searchAttributesSchema);
|
|
6706
7125
|
try {
|
|
7126
|
+
const handoff = liveHandoff(run.id, snapshot);
|
|
7127
|
+
if (handoff && !this.cancelRequested.has(run.id)) {
|
|
7128
|
+
return await this.settleRun(run, await this.followHandoff(run, handoff));
|
|
7129
|
+
}
|
|
6707
7130
|
const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
|
|
6708
7131
|
if (this.cancelRequested.has(run.id)) {
|
|
6709
7132
|
return await this.compensateAndCancel(run, compensations, replay);
|
|
@@ -6766,7 +7189,8 @@ var WorkflowEngine = class {
|
|
|
6766
7189
|
}
|
|
6767
7190
|
const error2 = {
|
|
6768
7191
|
message: err instanceof Error ? err.message : String(err),
|
|
6769
|
-
stack: err instanceof Error ? err.stack : void 0
|
|
7192
|
+
stack: err instanceof Error ? err.stack : void 0,
|
|
7193
|
+
...codeAndDetails(err)
|
|
6770
7194
|
};
|
|
6771
7195
|
const pending = await this.unwindCompensations(run, compensations, replay);
|
|
6772
7196
|
if (pending) return await this.settleRun(run, {
|
|
@@ -7402,7 +7826,8 @@ var WorkflowEngine = class {
|
|
|
7402
7826
|
const failure = {
|
|
7403
7827
|
index: i,
|
|
7404
7828
|
id: stepId(runId, seq),
|
|
7405
|
-
error: err.message
|
|
7829
|
+
error: err.message,
|
|
7830
|
+
...codeAndDetails(err)
|
|
7406
7831
|
};
|
|
7407
7832
|
if (opts.mode === "failFast") throw new GatherError([
|
|
7408
7833
|
failure
|
|
@@ -7424,6 +7849,24 @@ var WorkflowEngine = class {
|
|
|
7424
7849
|
});
|
|
7425
7850
|
throw new WorkflowSuspended(wakeAt);
|
|
7426
7851
|
}
|
|
7852
|
+
if (opts.settle) {
|
|
7853
|
+
const failed = new Map(failures.map((f) => [
|
|
7854
|
+
f.index,
|
|
7855
|
+
f
|
|
7856
|
+
]));
|
|
7857
|
+
return Array.from(outputs, (value, i) => {
|
|
7858
|
+
const f = failed.get(i);
|
|
7859
|
+
if (!f) return {
|
|
7860
|
+
status: "fulfilled",
|
|
7861
|
+
value
|
|
7862
|
+
};
|
|
7863
|
+
const { index: _index, ...rejected } = f;
|
|
7864
|
+
return {
|
|
7865
|
+
status: "rejected",
|
|
7866
|
+
...rejected
|
|
7867
|
+
};
|
|
7868
|
+
});
|
|
7869
|
+
}
|
|
7427
7870
|
if (failures.length > 0) throw new GatherError(failures, inputs.length);
|
|
7428
7871
|
return outputs;
|
|
7429
7872
|
}
|
|
@@ -7495,6 +7938,25 @@ var WorkflowEngine = class {
|
|
|
7495
7938
|
await this.store.saveCheckpoint(checkpoint);
|
|
7496
7939
|
return true;
|
|
7497
7940
|
}
|
|
7941
|
+
/**
|
|
7942
|
+
* {@link savePendingCheckpoint} for checkpoints at distinct seqs of one run: one
|
|
7943
|
+
* {@link StateStore.saveCheckpointsIfPending} call when the store has it, else one write each.
|
|
7944
|
+
* Never rejects as a whole: a write that failed is reported as that entry's error, and the batch
|
|
7945
|
+
* call rejecting means it wrote nothing, so the entries are then written one by one.
|
|
7946
|
+
*/
|
|
7947
|
+
async savePendingCheckpoints(checkpoints) {
|
|
7948
|
+
if (checkpoints.length > 1 && this.store.saveCheckpointsIfPending) {
|
|
7949
|
+
try {
|
|
7950
|
+
const wrote = await this.store.saveCheckpointsIfPending(checkpoints);
|
|
7951
|
+
return checkpoints.map((_, i) => wrote[i] === true);
|
|
7952
|
+
} catch {
|
|
7953
|
+
}
|
|
7954
|
+
}
|
|
7955
|
+
const settled = await Promise.allSettled(checkpoints.map((cp) => this.savePendingCheckpoint(cp)));
|
|
7956
|
+
return settled.map((s) => s.status === "fulfilled" ? s.value : {
|
|
7957
|
+
error: s.reason
|
|
7958
|
+
});
|
|
7959
|
+
}
|
|
7498
7960
|
/** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
|
|
7499
7961
|
* dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
|
|
7500
7962
|
* {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
|
|
@@ -7533,57 +7995,161 @@ var WorkflowEngine = class {
|
|
|
7533
7995
|
* result (the dispatching one may be gone), so the run is crash/scale-safe. Resolves whether the
|
|
7534
7996
|
* run should now be resumed: not for a duplicate or late delivery (the checkpoint isn't `pending`),
|
|
7535
7997
|
* a run already finished, or a step fan whose barrier is still closed.
|
|
7998
|
+
*
|
|
7999
|
+
* Results of one run are settled in batches: the first starts a batch, and the ones that arrive
|
|
8000
|
+
* while it is being written form the next. Under load a wide fan's results are written by one
|
|
8001
|
+
* {@link StateStore.saveCheckpointsIfPending} call per batch, and the fan's barrier is asked once
|
|
8002
|
+
* per batch, not once per result. A result resolves only once its own write has committed, so the
|
|
8003
|
+
* transport acknowledges nothing that is not durable yet.
|
|
7536
8004
|
*/
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
8005
|
+
completeRemoteResult(result) {
|
|
8006
|
+
return new Promise((resolve, reject) => {
|
|
8007
|
+
const entry = {
|
|
8008
|
+
result,
|
|
8009
|
+
resolve,
|
|
8010
|
+
reject
|
|
8011
|
+
};
|
|
8012
|
+
const queued = this.resultQueues.get(result.runId);
|
|
8013
|
+
if (queued) {
|
|
8014
|
+
queued.push(entry);
|
|
8015
|
+
return;
|
|
8016
|
+
}
|
|
8017
|
+
const queue = [
|
|
8018
|
+
entry
|
|
8019
|
+
];
|
|
8020
|
+
this.resultQueues.set(result.runId, queue);
|
|
8021
|
+
void this.drainResultQueue(result.runId, queue);
|
|
8022
|
+
});
|
|
8023
|
+
}
|
|
8024
|
+
/** Settle `queue` (a run's queued results) batch by batch until it is empty, then retire it. */
|
|
8025
|
+
async drainResultQueue(runId, queue) {
|
|
8026
|
+
await Promise.resolve();
|
|
8027
|
+
while (queue.length > 0) {
|
|
8028
|
+
const batch = queue.splice(0, RESULT_BATCH_MAX);
|
|
8029
|
+
let outcomes;
|
|
8030
|
+
try {
|
|
8031
|
+
outcomes = await this.completeRemoteResults(runId, batch.map((q) => q.result));
|
|
8032
|
+
} catch (err) {
|
|
8033
|
+
outcomes = batch.map(() => ({
|
|
8034
|
+
error: err
|
|
8035
|
+
}));
|
|
8036
|
+
}
|
|
8037
|
+
batch.forEach((q, i) => {
|
|
8038
|
+
const outcome = outcomes[i] ?? false;
|
|
8039
|
+
if (typeof outcome === "boolean") q.resolve(outcome);
|
|
8040
|
+
else q.reject(outcome.error);
|
|
8041
|
+
});
|
|
8042
|
+
}
|
|
8043
|
+
this.resultQueues.delete(runId);
|
|
8044
|
+
}
|
|
8045
|
+
/**
|
|
8046
|
+
* Settle a batch of one run's step results. At most one of them resolves `true` (resume the run):
|
|
8047
|
+
* that resume replays every write of the batch, so a second would only replay it again. A result
|
|
8048
|
+
* whose read or write failed resolves its error, and the transport redelivers just that one.
|
|
8049
|
+
*/
|
|
8050
|
+
async completeRemoteResults(runId, results) {
|
|
8051
|
+
const outcomes = results.map(() => false);
|
|
8052
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8053
|
+
const live = results.flatMap((r, i) => {
|
|
8054
|
+
if (seen.has(r.seq)) return [];
|
|
8055
|
+
seen.add(r.seq);
|
|
8056
|
+
return [
|
|
8057
|
+
i
|
|
8058
|
+
];
|
|
8059
|
+
});
|
|
8060
|
+
const reads = await Promise.allSettled(live.map((i) => this.store.getCheckpoint(runId, results[i].seq)));
|
|
8061
|
+
const pending = [];
|
|
8062
|
+
reads.forEach((read, k) => {
|
|
8063
|
+
const index = live[k];
|
|
8064
|
+
if (read.status === "rejected") outcomes[index] = {
|
|
8065
|
+
error: read.reason
|
|
8066
|
+
};
|
|
8067
|
+
else if (read.value?.status === "pending") pending.push({
|
|
8068
|
+
index,
|
|
8069
|
+
cp: read.value
|
|
8070
|
+
});
|
|
8071
|
+
});
|
|
8072
|
+
if (pending.length === 0) return outcomes;
|
|
8073
|
+
for (const { cp } of pending) void this.releaseQueueSlot(cp.stepId).catch(() => void 0);
|
|
8074
|
+
const run = await this.store.getRun(runId);
|
|
8075
|
+
if (run && (run.status === "cancelled" || run.status === "completed")) return outcomes;
|
|
7543
8076
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
7544
|
-
const
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
8077
|
+
const writes = pending.map(({ index, cp }) => {
|
|
8078
|
+
const result = results[index];
|
|
8079
|
+
return {
|
|
8080
|
+
...cp,
|
|
8081
|
+
status: result.status,
|
|
8082
|
+
output: result.status === "completed" ? result.output : cp.output,
|
|
8083
|
+
error: result.error,
|
|
8084
|
+
events: result.events ?? cp.events,
|
|
8085
|
+
startedAt: result.startedAt ? new Date(result.startedAt) : cp.startedAt,
|
|
8086
|
+
finishedAt
|
|
8087
|
+
};
|
|
7553
8088
|
});
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
queueMs: startedAt.getTime() - cp.enqueuedAt.getTime(),
|
|
7565
|
-
durationMs: finishedAt.getTime() - startedAt.getTime()
|
|
8089
|
+
const claims = await this.savePendingCheckpoints(writes);
|
|
8090
|
+
const settled = [];
|
|
8091
|
+
claims.forEach((claim, k) => {
|
|
8092
|
+
const { index, cp } = pending[k];
|
|
8093
|
+
if (claim === true) settled.push({
|
|
8094
|
+
index,
|
|
8095
|
+
cp,
|
|
8096
|
+
result: results[index]
|
|
8097
|
+
});
|
|
8098
|
+
else if (claim !== false) outcomes[index] = claim;
|
|
7566
8099
|
});
|
|
7567
|
-
|
|
8100
|
+
for (const { cp, result } of settled) {
|
|
8101
|
+
this.noteSettledHere(`${runId}:${result.seq}`);
|
|
8102
|
+
const startedAt = result.startedAt ? new Date(result.startedAt) : cp.startedAt;
|
|
8103
|
+
this.emit({
|
|
8104
|
+
type: result.status === "completed" ? "step.completed" : "step.failed",
|
|
8105
|
+
runId,
|
|
8106
|
+
seq: result.seq,
|
|
8107
|
+
name: cp.name,
|
|
8108
|
+
kind: cp.kind,
|
|
8109
|
+
output: result.output,
|
|
8110
|
+
error: result.error,
|
|
8111
|
+
queueMs: startedAt.getTime() - cp.enqueuedAt.getTime(),
|
|
8112
|
+
durationMs: finishedAt.getTime() - startedAt.getTime()
|
|
8113
|
+
});
|
|
8114
|
+
}
|
|
8115
|
+
const barriers = /* @__PURE__ */ new Map();
|
|
8116
|
+
let carrier;
|
|
8117
|
+
for (const { index, cp, result } of settled) {
|
|
8118
|
+
if (!await this.stepFanStillRunning(cp, result, barriers)) carrier = index;
|
|
8119
|
+
}
|
|
8120
|
+
if (carrier !== void 0) outcomes[carrier] = true;
|
|
8121
|
+
return outcomes;
|
|
7568
8122
|
}
|
|
7569
8123
|
/**
|
|
7570
8124
|
* The join barrier of a step fan: a result for one of its steps resumes the parent only once no
|
|
7571
8125
|
* step of the fan is still `pending` (a settled wave), or on a failure (failFast surfaces it, and a
|
|
7572
8126
|
* retry needs the parent to schedule it). Without it every result would replay the whole parent,
|
|
7573
|
-
* which makes a fan of N steps cost O(N²) checkpoint reads.
|
|
8127
|
+
* which makes a fan of N steps cost O(N²) checkpoint reads. `barriers` holds the answer per fan
|
|
8128
|
+
* for the rest of a batch. A barrier that cannot be read counts as open: a spare resume replays and
|
|
8129
|
+
* re-suspends, a lost one strands the run.
|
|
7574
8130
|
*/
|
|
7575
|
-
async stepFanStillRunning(cp, result) {
|
|
8131
|
+
async stepFanStillRunning(cp, result, barriers) {
|
|
7576
8132
|
const fan = parseStepFanGroup(cp.parallelGroup);
|
|
7577
8133
|
if (!fan) return false;
|
|
7578
8134
|
if (result.status === "failed") return false;
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
8135
|
+
let closed = barriers.get(fan.firstSeq);
|
|
8136
|
+
if (!closed) {
|
|
8137
|
+
closed = this.stepFanPending(result.runId, fan.firstSeq, fan.firstSeq + fan.size - 1).then((pending) => pending > 0, () => false);
|
|
8138
|
+
barriers.set(fan.firstSeq, closed);
|
|
8139
|
+
}
|
|
8140
|
+
if (await closed) return true;
|
|
7584
8141
|
this.noteRecent(this.stepFansFiredHere, `${result.runId}:${fan.firstSeq}`);
|
|
7585
8142
|
return false;
|
|
7586
8143
|
}
|
|
8144
|
+
/** How many steps of a step fan's seq range are still `pending`. */
|
|
8145
|
+
async stepFanPending(runId, from, to) {
|
|
8146
|
+
if (this.store.countCheckpoints) {
|
|
8147
|
+
return this.store.countCheckpoints(runId, from, to, [
|
|
8148
|
+
"pending"
|
|
8149
|
+
]);
|
|
8150
|
+
}
|
|
8151
|
+
return (await this.store.listCheckpoints(runId)).filter((c) => c.seq >= from && c.seq <= to && c.status === "pending").length;
|
|
8152
|
+
}
|
|
7587
8153
|
/**
|
|
7588
8154
|
* Apply a remote workflow-turn {@link WorkflowDecision} durably — runs on WHATEVER instance receives
|
|
7589
8155
|
* it over the broker (the dispatching one may be gone, or a point-to-point broker handed it to a
|
|
@@ -7935,6 +8501,435 @@ var RunGateway = class {
|
|
|
7935
8501
|
}
|
|
7936
8502
|
};
|
|
7937
8503
|
|
|
8504
|
+
// src/run-detail-summary.ts
|
|
8505
|
+
var FAN_SUMMARY_FAILURES = 20;
|
|
8506
|
+
var SUMMARY_CHILD_LIMIT = 200;
|
|
8507
|
+
var FAN_PAGE_MAX = 500;
|
|
8508
|
+
function foldedCount(cp) {
|
|
8509
|
+
if (!cp.name.startsWith(FOLDED_PREFIX)) return void 0;
|
|
8510
|
+
const values = cp.output?.values;
|
|
8511
|
+
return Array.isArray(values) ? values.length : 0;
|
|
8512
|
+
}
|
|
8513
|
+
__name(foldedCount, "foldedCount");
|
|
8514
|
+
function isFailedMember(cp) {
|
|
8515
|
+
if (cp.status === "failed") return true;
|
|
8516
|
+
if (!cp.name.startsWith("signal:child:") || cp.status !== "completed") return false;
|
|
8517
|
+
return cp.output?.ok === false;
|
|
8518
|
+
}
|
|
8519
|
+
__name(isFailedMember, "isFailedMember");
|
|
8520
|
+
function fanLabel(members) {
|
|
8521
|
+
if (members.some((cp) => cp.name.startsWith("signal:child:") || foldedCount(cp) !== void 0)) {
|
|
8522
|
+
return "child runs";
|
|
8523
|
+
}
|
|
8524
|
+
const first = members[0]?.name ?? "";
|
|
8525
|
+
return members.every((cp) => cp.name === first) ? first : "steps";
|
|
8526
|
+
}
|
|
8527
|
+
__name(fanLabel, "fanLabel");
|
|
8528
|
+
function summarizeFan(group, members) {
|
|
8529
|
+
const counts = {};
|
|
8530
|
+
const failures = [];
|
|
8531
|
+
let size = 0;
|
|
8532
|
+
let startedAt;
|
|
8533
|
+
let finishedAt;
|
|
8534
|
+
let firstSeq = Number.POSITIVE_INFINITY;
|
|
8535
|
+
let lastSeq = Number.NEGATIVE_INFINITY;
|
|
8536
|
+
for (const cp of members) {
|
|
8537
|
+
firstSeq = Math.min(firstSeq, cp.seq);
|
|
8538
|
+
lastSeq = Math.max(lastSeq, cp.seq);
|
|
8539
|
+
const folded = foldedCount(cp);
|
|
8540
|
+
const n = folded ?? 1;
|
|
8541
|
+
size += n;
|
|
8542
|
+
const status = isFailedMember(cp) ? "failed" : cp.status;
|
|
8543
|
+
counts[status] = (counts[status] ?? 0) + n;
|
|
8544
|
+
if (status === "failed" && failures.length < FAN_SUMMARY_FAILURES) failures.push(cp);
|
|
8545
|
+
if (cp.status !== "pending" && cp.status !== "running") {
|
|
8546
|
+
if (!startedAt || cp.startedAt < startedAt) startedAt = cp.startedAt;
|
|
8547
|
+
if (!finishedAt || cp.finishedAt > finishedAt) finishedAt = cp.finishedAt;
|
|
8548
|
+
} else if (!startedAt || cp.enqueuedAt < startedAt) {
|
|
8549
|
+
startedAt = cp.enqueuedAt;
|
|
8550
|
+
}
|
|
8551
|
+
}
|
|
8552
|
+
const parsed = parseFanGroup(group);
|
|
8553
|
+
const total = parsed?.size;
|
|
8554
|
+
return {
|
|
8555
|
+
group,
|
|
8556
|
+
label: fanLabel(members),
|
|
8557
|
+
firstSeq,
|
|
8558
|
+
lastSeq,
|
|
8559
|
+
size,
|
|
8560
|
+
total,
|
|
8561
|
+
notStarted: total === void 0 ? void 0 : Math.max(0, total - size),
|
|
8562
|
+
failFast: parsed?.failFast ?? false,
|
|
8563
|
+
counts,
|
|
8564
|
+
startedAt,
|
|
8565
|
+
finishedAt,
|
|
8566
|
+
failures
|
|
8567
|
+
};
|
|
8568
|
+
}
|
|
8569
|
+
__name(summarizeFan, "summarizeFan");
|
|
8570
|
+
function summarizeTimeline(timeline, over) {
|
|
8571
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
8572
|
+
for (const cp of timeline) {
|
|
8573
|
+
if (cp.parallelGroup === void 0) continue;
|
|
8574
|
+
const members = byGroup.get(cp.parallelGroup);
|
|
8575
|
+
if (members) members.push(cp);
|
|
8576
|
+
else byGroup.set(cp.parallelGroup, [
|
|
8577
|
+
cp
|
|
8578
|
+
]);
|
|
8579
|
+
}
|
|
8580
|
+
const summarized = /* @__PURE__ */ new Set();
|
|
8581
|
+
const fans = [];
|
|
8582
|
+
for (const [group, members] of byGroup) {
|
|
8583
|
+
if (members.length <= over && (parseFanGroup(group)?.size ?? 0) <= over) continue;
|
|
8584
|
+
summarized.add(group);
|
|
8585
|
+
fans.push(summarizeFan(group, members));
|
|
8586
|
+
}
|
|
8587
|
+
if (fans.length === 0) return {
|
|
8588
|
+
timeline,
|
|
8589
|
+
fans
|
|
8590
|
+
};
|
|
8591
|
+
return {
|
|
8592
|
+
timeline: timeline.filter((cp) => cp.parallelGroup === void 0 || !summarized.has(cp.parallelGroup)),
|
|
8593
|
+
fans: fans.sort((a, b) => a.firstSeq - b.firstSeq)
|
|
8594
|
+
};
|
|
8595
|
+
}
|
|
8596
|
+
__name(summarizeTimeline, "summarizeTimeline");
|
|
8597
|
+
function pageFan(timeline, group, query) {
|
|
8598
|
+
const matching = timeline.filter((cp) => cp.parallelGroup === group && (!query.failedOnly || isFailedMember(cp)));
|
|
8599
|
+
const { offset, limit } = fanPageBounds(query);
|
|
8600
|
+
return {
|
|
8601
|
+
items: matching.slice(offset, offset + limit),
|
|
8602
|
+
total: matching.length
|
|
8603
|
+
};
|
|
8604
|
+
}
|
|
8605
|
+
__name(pageFan, "pageFan");
|
|
8606
|
+
function fanPageBounds(query) {
|
|
8607
|
+
const offset = query.offset !== void 0 && Number.isFinite(query.offset) ? Math.max(0, Math.floor(query.offset)) : 0;
|
|
8608
|
+
const limit = query.limit !== void 0 && Number.isFinite(query.limit) ? Math.min(FAN_PAGE_MAX, Math.max(1, Math.floor(query.limit))) : 100;
|
|
8609
|
+
return {
|
|
8610
|
+
offset,
|
|
8611
|
+
limit
|
|
8612
|
+
};
|
|
8613
|
+
}
|
|
8614
|
+
__name(fanPageBounds, "fanPageBounds");
|
|
8615
|
+
|
|
8616
|
+
// src/run-detail-store.ts
|
|
8617
|
+
function aggregatesCheckpoints(store) {
|
|
8618
|
+
return store.checkpointStats !== void 0 && store.queryCheckpoints !== void 0;
|
|
8619
|
+
}
|
|
8620
|
+
__name(aggregatesCheckpoints, "aggregatesCheckpoints");
|
|
8621
|
+
function checkpointStatsOf(checkpoints, opts) {
|
|
8622
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
8623
|
+
for (const cp of checkpoints) {
|
|
8624
|
+
const group = cp.parallelGroup ?? null;
|
|
8625
|
+
if (opts?.group !== void 0 && group !== opts.group) continue;
|
|
8626
|
+
const nameKind = checkpointNameKind(cp.name);
|
|
8627
|
+
const key = JSON.stringify([
|
|
8628
|
+
group,
|
|
8629
|
+
cp.status,
|
|
8630
|
+
nameKind
|
|
8631
|
+
]);
|
|
8632
|
+
const enqueuedAt = cp.enqueuedAt ?? cp.startedAt;
|
|
8633
|
+
const bucket = buckets.get(key);
|
|
8634
|
+
if (!bucket) {
|
|
8635
|
+
buckets.set(key, {
|
|
8636
|
+
group,
|
|
8637
|
+
status: cp.status,
|
|
8638
|
+
nameKind,
|
|
8639
|
+
count: 1,
|
|
8640
|
+
minSeq: cp.seq,
|
|
8641
|
+
maxSeq: cp.seq,
|
|
8642
|
+
minName: cp.name,
|
|
8643
|
+
maxName: cp.name,
|
|
8644
|
+
minEnqueuedAt: enqueuedAt,
|
|
8645
|
+
minStartedAt: cp.startedAt,
|
|
8646
|
+
maxStartedAt: cp.startedAt,
|
|
8647
|
+
maxFinishedAt: cp.finishedAt,
|
|
8648
|
+
sumAttempts: cp.attempts
|
|
8649
|
+
});
|
|
8650
|
+
continue;
|
|
8651
|
+
}
|
|
8652
|
+
bucket.count += 1;
|
|
8653
|
+
bucket.minSeq = Math.min(bucket.minSeq, cp.seq);
|
|
8654
|
+
bucket.maxSeq = Math.max(bucket.maxSeq, cp.seq);
|
|
8655
|
+
if (cp.name < bucket.minName) bucket.minName = cp.name;
|
|
8656
|
+
if (cp.name > bucket.maxName) bucket.maxName = cp.name;
|
|
8657
|
+
if (enqueuedAt < bucket.minEnqueuedAt) bucket.minEnqueuedAt = enqueuedAt;
|
|
8658
|
+
if (cp.startedAt < bucket.minStartedAt) bucket.minStartedAt = cp.startedAt;
|
|
8659
|
+
if (cp.startedAt > bucket.maxStartedAt) bucket.maxStartedAt = cp.startedAt;
|
|
8660
|
+
if (cp.finishedAt > bucket.maxFinishedAt) bucket.maxFinishedAt = cp.finishedAt;
|
|
8661
|
+
bucket.sumAttempts += cp.attempts;
|
|
8662
|
+
}
|
|
8663
|
+
return [
|
|
8664
|
+
...buckets.values()
|
|
8665
|
+
];
|
|
8666
|
+
}
|
|
8667
|
+
__name(checkpointStatsOf, "checkpointStatsOf");
|
|
8668
|
+
function queryCheckpointsOf(checkpoints, query) {
|
|
8669
|
+
const excluded = new Set(query.excludeGroups ?? []);
|
|
8670
|
+
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);
|
|
8671
|
+
const offset = query.offset ?? 0;
|
|
8672
|
+
return matching.slice(offset, query.limit === void 0 ? void 0 : offset + query.limit);
|
|
8673
|
+
}
|
|
8674
|
+
__name(queryCheckpointsOf, "queryCheckpointsOf");
|
|
8675
|
+
var TERMINAL = /* @__PURE__ */ __name((status) => status !== "pending" && status !== "running", "TERMINAL");
|
|
8676
|
+
function isWide(group, rows, over) {
|
|
8677
|
+
return rows > over || (parseFanGroup(group)?.size ?? 0) > over;
|
|
8678
|
+
}
|
|
8679
|
+
__name(isWide, "isWide");
|
|
8680
|
+
function mergeBySeq(a, b) {
|
|
8681
|
+
return [
|
|
8682
|
+
...a,
|
|
8683
|
+
...b
|
|
8684
|
+
].sort((x, y) => x.seq - y.seq);
|
|
8685
|
+
}
|
|
8686
|
+
__name(mergeBySeq, "mergeBySeq");
|
|
8687
|
+
async function failedChildren(store, runId, group, buckets) {
|
|
8688
|
+
if (!buckets.some((b) => b.nameKind === "child" && b.status === "completed")) {
|
|
8689
|
+
return {
|
|
8690
|
+
completed: [],
|
|
8691
|
+
failed: []
|
|
8692
|
+
};
|
|
8693
|
+
}
|
|
8694
|
+
const completed = await store.queryCheckpoints(runId, {
|
|
8695
|
+
group,
|
|
8696
|
+
statuses: [
|
|
8697
|
+
"completed"
|
|
8698
|
+
],
|
|
8699
|
+
namePrefixes: [
|
|
8700
|
+
"signal:child:"
|
|
8701
|
+
]
|
|
8702
|
+
});
|
|
8703
|
+
return {
|
|
8704
|
+
completed,
|
|
8705
|
+
failed: completed.filter(isFailedMember)
|
|
8706
|
+
};
|
|
8707
|
+
}
|
|
8708
|
+
__name(failedChildren, "failedChildren");
|
|
8709
|
+
async function summarizeFanFromStats(store, runId, group, buckets, folded) {
|
|
8710
|
+
const counts = {};
|
|
8711
|
+
const add = /* @__PURE__ */ __name((status, n) => {
|
|
8712
|
+
counts[status] = (counts[status] ?? 0) + n;
|
|
8713
|
+
}, "add");
|
|
8714
|
+
let size = 0;
|
|
8715
|
+
let startedAt;
|
|
8716
|
+
let finishedAt;
|
|
8717
|
+
let firstSeq = Number.POSITIVE_INFINITY;
|
|
8718
|
+
let lastSeq = Number.NEGATIVE_INFINITY;
|
|
8719
|
+
let minName;
|
|
8720
|
+
let maxName;
|
|
8721
|
+
let childRuns = false;
|
|
8722
|
+
for (const b of buckets) {
|
|
8723
|
+
firstSeq = Math.min(firstSeq, b.minSeq);
|
|
8724
|
+
lastSeq = Math.max(lastSeq, b.maxSeq);
|
|
8725
|
+
if (minName === void 0 || b.minName < minName) minName = b.minName;
|
|
8726
|
+
if (maxName === void 0 || b.maxName > maxName) maxName = b.maxName;
|
|
8727
|
+
if (b.nameKind === "child" || b.nameKind === "folded") childRuns = true;
|
|
8728
|
+
if (TERMINAL(b.status)) {
|
|
8729
|
+
if (!startedAt || b.minStartedAt < startedAt) startedAt = b.minStartedAt;
|
|
8730
|
+
if (!finishedAt || b.maxFinishedAt > finishedAt) finishedAt = b.maxFinishedAt;
|
|
8731
|
+
} else if (!startedAt || b.minEnqueuedAt < startedAt) {
|
|
8732
|
+
startedAt = b.minEnqueuedAt;
|
|
8733
|
+
}
|
|
8734
|
+
if (b.nameKind === "folded") continue;
|
|
8735
|
+
if (b.nameKind === "child" && b.status === "completed") continue;
|
|
8736
|
+
add(b.status, b.count);
|
|
8737
|
+
size += b.count;
|
|
8738
|
+
}
|
|
8739
|
+
for (const cp of folded) {
|
|
8740
|
+
const n = foldedCount(cp) ?? 1;
|
|
8741
|
+
add(isFailedMember(cp) ? "failed" : cp.status, n);
|
|
8742
|
+
size += n;
|
|
8743
|
+
}
|
|
8744
|
+
const children = await failedChildren(store, runId, group, buckets);
|
|
8745
|
+
const failedChildSet = new Set(children.failed);
|
|
8746
|
+
for (const cp of children.completed) add(failedChildSet.has(cp) ? "failed" : "completed", 1);
|
|
8747
|
+
size += children.completed.length;
|
|
8748
|
+
const failedStatusRows = buckets.some((b) => b.status === "failed") ? await store.queryCheckpoints(runId, {
|
|
8749
|
+
group,
|
|
8750
|
+
statuses: [
|
|
8751
|
+
"failed"
|
|
8752
|
+
],
|
|
8753
|
+
limit: FAN_SUMMARY_FAILURES
|
|
8754
|
+
}) : [];
|
|
8755
|
+
const failures = mergeBySeq(failedStatusRows, children.failed).slice(0, FAN_SUMMARY_FAILURES);
|
|
8756
|
+
const parsed = parseFanGroup(group);
|
|
8757
|
+
const total = parsed?.size;
|
|
8758
|
+
return {
|
|
8759
|
+
group,
|
|
8760
|
+
label: childRuns ? "child runs" : minName === maxName ? minName ?? "" : "steps",
|
|
8761
|
+
firstSeq,
|
|
8762
|
+
lastSeq,
|
|
8763
|
+
size,
|
|
8764
|
+
total,
|
|
8765
|
+
notStarted: total === void 0 ? void 0 : Math.max(0, total - size),
|
|
8766
|
+
failFast: parsed?.failFast ?? false,
|
|
8767
|
+
counts,
|
|
8768
|
+
startedAt,
|
|
8769
|
+
finishedAt,
|
|
8770
|
+
failures
|
|
8771
|
+
};
|
|
8772
|
+
}
|
|
8773
|
+
__name(summarizeFanFromStats, "summarizeFanFromStats");
|
|
8774
|
+
async function firstChildIds(store, runId) {
|
|
8775
|
+
const ids = [];
|
|
8776
|
+
for (let offset = 0; ids.length < SUMMARY_CHILD_LIMIT; offset += SUMMARY_CHILD_LIMIT) {
|
|
8777
|
+
const page = await store.queryCheckpoints(runId, {
|
|
8778
|
+
namePrefixes: CHILD_EDGE_PREFIXES,
|
|
8779
|
+
offset,
|
|
8780
|
+
limit: SUMMARY_CHILD_LIMIT
|
|
8781
|
+
});
|
|
8782
|
+
ids.push(...childIdsOf(page));
|
|
8783
|
+
if (page.length < SUMMARY_CHILD_LIMIT) break;
|
|
8784
|
+
}
|
|
8785
|
+
return ids.slice(0, SUMMARY_CHILD_LIMIT);
|
|
8786
|
+
}
|
|
8787
|
+
__name(firstChildIds, "firstChildIds");
|
|
8788
|
+
async function summarizeRunFromStore(store, runId, over, stats) {
|
|
8789
|
+
const all = stats ?? await store.checkpointStats(runId);
|
|
8790
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
8791
|
+
let edgeRows = 0;
|
|
8792
|
+
let hasFolded = false;
|
|
8793
|
+
for (const b of all) {
|
|
8794
|
+
if (b.nameKind === "child" || b.nameKind === "spawn") edgeRows += b.count;
|
|
8795
|
+
if (b.nameKind === "folded") hasFolded = true;
|
|
8796
|
+
if (b.group === null) continue;
|
|
8797
|
+
const buckets = byGroup.get(b.group);
|
|
8798
|
+
if (buckets) buckets.push(b);
|
|
8799
|
+
else byGroup.set(b.group, [
|
|
8800
|
+
b
|
|
8801
|
+
]);
|
|
8802
|
+
}
|
|
8803
|
+
const wide = [
|
|
8804
|
+
...byGroup
|
|
8805
|
+
].filter(([group, buckets]) => isWide(group, buckets.reduce((n, b) => n + b.count, 0), over));
|
|
8806
|
+
const [timeline, folded, children] = await Promise.all([
|
|
8807
|
+
store.queryCheckpoints(runId, {
|
|
8808
|
+
excludeGroups: wide.map(([group]) => group)
|
|
8809
|
+
}),
|
|
8810
|
+
hasFolded ? store.queryCheckpoints(runId, {
|
|
8811
|
+
namePrefixes: [
|
|
8812
|
+
FOLDED_PREFIX
|
|
8813
|
+
]
|
|
8814
|
+
}) : [],
|
|
8815
|
+
edgeRows > 0 || hasFolded ? firstChildIds(store, runId) : []
|
|
8816
|
+
]);
|
|
8817
|
+
const fans = await Promise.all(wide.map(([group, buckets]) => summarizeFanFromStats(store, runId, group, buckets, folded.filter((cp) => cp.parallelGroup === group))));
|
|
8818
|
+
const childCount = folded.reduce((n, cp) => n + childIdsOf([
|
|
8819
|
+
cp
|
|
8820
|
+
]).length, edgeRows);
|
|
8821
|
+
return {
|
|
8822
|
+
timeline,
|
|
8823
|
+
fans: fans.sort((a, b) => a.firstSeq - b.firstSeq),
|
|
8824
|
+
children,
|
|
8825
|
+
childCount
|
|
8826
|
+
};
|
|
8827
|
+
}
|
|
8828
|
+
__name(summarizeRunFromStore, "summarizeRunFromStore");
|
|
8829
|
+
async function pageFanFromStore(store, runId, group, query, stats) {
|
|
8830
|
+
const buckets = stats ?? await store.checkpointStats(runId, {
|
|
8831
|
+
group
|
|
8832
|
+
});
|
|
8833
|
+
const { offset, limit } = fanPageBounds(query);
|
|
8834
|
+
if (!query.failedOnly) {
|
|
8835
|
+
const total2 = buckets.reduce((n, b) => n + b.count, 0);
|
|
8836
|
+
const items = total2 > offset ? await store.queryCheckpoints(runId, {
|
|
8837
|
+
group,
|
|
8838
|
+
offset,
|
|
8839
|
+
limit
|
|
8840
|
+
}) : [];
|
|
8841
|
+
return {
|
|
8842
|
+
items,
|
|
8843
|
+
total: total2
|
|
8844
|
+
};
|
|
8845
|
+
}
|
|
8846
|
+
const failedStatus = buckets.filter((b) => b.status === "failed").reduce((n, b) => n + b.count, 0);
|
|
8847
|
+
const children = await failedChildren(store, runId, group, buckets);
|
|
8848
|
+
const total = failedStatus + children.failed.length;
|
|
8849
|
+
if (total <= offset) return {
|
|
8850
|
+
items: [],
|
|
8851
|
+
total
|
|
8852
|
+
};
|
|
8853
|
+
if (children.failed.length === 0) {
|
|
8854
|
+
return {
|
|
8855
|
+
items: await store.queryCheckpoints(runId, {
|
|
8856
|
+
group,
|
|
8857
|
+
statuses: [
|
|
8858
|
+
"failed"
|
|
8859
|
+
],
|
|
8860
|
+
offset,
|
|
8861
|
+
limit
|
|
8862
|
+
}),
|
|
8863
|
+
total
|
|
8864
|
+
};
|
|
8865
|
+
}
|
|
8866
|
+
const failedRows = await store.queryCheckpoints(runId, {
|
|
8867
|
+
group,
|
|
8868
|
+
statuses: [
|
|
8869
|
+
"failed"
|
|
8870
|
+
],
|
|
8871
|
+
limit: offset + limit
|
|
8872
|
+
});
|
|
8873
|
+
return {
|
|
8874
|
+
items: mergeBySeq(failedRows, children.failed).slice(offset, offset + limit),
|
|
8875
|
+
total
|
|
8876
|
+
};
|
|
8877
|
+
}
|
|
8878
|
+
__name(pageFanFromStore, "pageFanFromStore");
|
|
8879
|
+
function hash(text) {
|
|
8880
|
+
let h1 = 3735928559;
|
|
8881
|
+
let h2 = 1103547991;
|
|
8882
|
+
for (let i = 0; i < text.length; i++) {
|
|
8883
|
+
const ch = text.charCodeAt(i);
|
|
8884
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
8885
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
8886
|
+
}
|
|
8887
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
8888
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
8889
|
+
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16);
|
|
8890
|
+
}
|
|
8891
|
+
__name(hash, "hash");
|
|
8892
|
+
function statsFingerprint(stats) {
|
|
8893
|
+
return stats.map((b) => [
|
|
8894
|
+
b.group ?? "",
|
|
8895
|
+
b.status,
|
|
8896
|
+
b.nameKind,
|
|
8897
|
+
b.count,
|
|
8898
|
+
b.minSeq,
|
|
8899
|
+
b.maxSeq,
|
|
8900
|
+
b.minName,
|
|
8901
|
+
b.maxName,
|
|
8902
|
+
b.minEnqueuedAt.getTime(),
|
|
8903
|
+
b.minStartedAt.getTime(),
|
|
8904
|
+
b.maxStartedAt.getTime(),
|
|
8905
|
+
b.maxFinishedAt.getTime(),
|
|
8906
|
+
b.sumAttempts
|
|
8907
|
+
].join("")).sort().join("");
|
|
8908
|
+
}
|
|
8909
|
+
__name(statsFingerprint, "statsFingerprint");
|
|
8910
|
+
function runDetailEtag(run, stats, over) {
|
|
8911
|
+
return `"rd-${hash(`${over}${JSON.stringify(run)}${statsFingerprint(stats)}`)}"`;
|
|
8912
|
+
}
|
|
8913
|
+
__name(runDetailEtag, "runDetailEtag");
|
|
8914
|
+
function fanPageEtag(group, stats, query) {
|
|
8915
|
+
const { offset, limit } = fanPageBounds(query);
|
|
8916
|
+
const key = [
|
|
8917
|
+
group,
|
|
8918
|
+
offset,
|
|
8919
|
+
limit,
|
|
8920
|
+
query.failedOnly === true
|
|
8921
|
+
].join("");
|
|
8922
|
+
return `"fp-${hash(`${key}${statsFingerprint(stats)}`)}"`;
|
|
8923
|
+
}
|
|
8924
|
+
__name(fanPageEtag, "fanPageEtag");
|
|
8925
|
+
function etagMatches(ifNoneMatch, etag) {
|
|
8926
|
+
if (!ifNoneMatch) return false;
|
|
8927
|
+
const bare = /* @__PURE__ */ __name((tag) => tag.trim().replace(/^W\//, ""), "bare");
|
|
8928
|
+
const target = bare(etag);
|
|
8929
|
+
return ifNoneMatch.split(",").some((tag) => tag.trim() === "*" || bare(tag) === target);
|
|
8930
|
+
}
|
|
8931
|
+
__name(etagMatches, "etagMatches");
|
|
8932
|
+
|
|
7938
8933
|
// src/codec-state-store.ts
|
|
7939
8934
|
var CodecStateStore = class {
|
|
7940
8935
|
static {
|
|
@@ -7963,11 +8958,19 @@ var CodecStateStore = class {
|
|
|
7963
8958
|
/** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
|
|
7964
8959
|
* plain-save fallback still kicks in through the wrapper. */
|
|
7965
8960
|
saveCheckpointIfPending;
|
|
8961
|
+
/** Encodes each checkpoint like {@link saveCheckpoint}. Bound only when the inner store has it. */
|
|
8962
|
+
saveCheckpointsIfPending;
|
|
7966
8963
|
/** Forwarded verbatim — a count reads no payload. Bound only when the inner store has it. */
|
|
7967
8964
|
countCheckpoints;
|
|
7968
8965
|
/** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
|
|
7969
8966
|
* when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
|
|
7970
8967
|
pruneTerminalRuns;
|
|
8968
|
+
/** Forwarded verbatim — the buckets aggregate names, statuses and timestamps, never a payload.
|
|
8969
|
+
* Bound only when the inner store aggregates, so the run detail's fallback still applies. */
|
|
8970
|
+
checkpointStats;
|
|
8971
|
+
/** Filters on unencoded columns, then decodes the rows like {@link listCheckpoints}. Bound only
|
|
8972
|
+
* when the inner store has it. */
|
|
8973
|
+
queryCheckpoints;
|
|
7971
8974
|
/** Forwarded verbatim — a count reads no payload. Bound only when the inner store counts, so the
|
|
7972
8975
|
* engine's facet/listing fallback still applies through the wrapper. */
|
|
7973
8976
|
countRuns;
|
|
@@ -7994,8 +8997,14 @@ var CodecStateStore = class {
|
|
|
7994
8997
|
if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
|
|
7995
8998
|
const saveIfPending = inner.saveCheckpointIfPending;
|
|
7996
8999
|
if (saveIfPending) this.saveCheckpointIfPending = (cp) => saveIfPending.call(inner, this.encCp(cp));
|
|
9000
|
+
const saveManyIfPending = inner.saveCheckpointsIfPending;
|
|
9001
|
+
if (saveManyIfPending) this.saveCheckpointsIfPending = (cps) => saveManyIfPending.call(inner, cps.map((cp) => this.encCp(cp)));
|
|
7997
9002
|
const countCps = inner.countCheckpoints;
|
|
7998
9003
|
if (countCps) this.countCheckpoints = (runId, from, to, statuses) => countCps.call(inner, runId, from, to, statuses);
|
|
9004
|
+
const stats = inner.checkpointStats;
|
|
9005
|
+
if (stats) this.checkpointStats = (runId, opts) => stats.call(inner, runId, opts);
|
|
9006
|
+
const queryCps = inner.queryCheckpoints;
|
|
9007
|
+
if (queryCps) this.queryCheckpoints = async (runId, query) => (await queryCps.call(inner, runId, query)).map((c) => this.decCp(c));
|
|
7999
9008
|
const prune = inner.pruneTerminalRuns;
|
|
8000
9009
|
if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
|
|
8001
9010
|
const count = inner.countRuns;
|
|
@@ -8203,8 +9212,11 @@ function collectMetrics(engine) {
|
|
|
8203
9212
|
started: 0,
|
|
8204
9213
|
completed: 0,
|
|
8205
9214
|
failed: 0,
|
|
8206
|
-
suspended: 0
|
|
9215
|
+
suspended: 0,
|
|
9216
|
+
routed: 0,
|
|
9217
|
+
unrouted: 0
|
|
8207
9218
|
},
|
|
9219
|
+
routedByCode: {},
|
|
8208
9220
|
steps: {
|
|
8209
9221
|
started: 0,
|
|
8210
9222
|
completed: 0,
|
|
@@ -8246,6 +9258,15 @@ function collectMetrics(engine) {
|
|
|
8246
9258
|
case "run.suspended":
|
|
8247
9259
|
m.runs.suspended += 1;
|
|
8248
9260
|
break;
|
|
9261
|
+
case "run.routed": {
|
|
9262
|
+
m.runs.routed += 1;
|
|
9263
|
+
const code = e.error?.code ?? "*";
|
|
9264
|
+
m.routedByCode[code] = (m.routedByCode[code] ?? 0) + 1;
|
|
9265
|
+
break;
|
|
9266
|
+
}
|
|
9267
|
+
case "run.unrouted":
|
|
9268
|
+
m.runs.unrouted += 1;
|
|
9269
|
+
break;
|
|
8249
9270
|
case "step.started":
|
|
8250
9271
|
m.steps.started += 1;
|
|
8251
9272
|
break;
|
|
@@ -8272,6 +9293,8 @@ function collectMetrics(engine) {
|
|
|
8272
9293
|
for (const [k, v] of Object.entries(m.steps)) lines.push(`durable_steps_total{event="${k}"} ${v}`);
|
|
8273
9294
|
lines.push("# TYPE durable_runs_by_workflow_total counter");
|
|
8274
9295
|
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}`);
|
|
9296
|
+
lines.push("# TYPE durable_runs_routed_total counter");
|
|
9297
|
+
for (const [code, n] of Object.entries(m.routedByCode)) lines.push(`durable_runs_routed_total{code="${code}"} ${n}`);
|
|
8275
9298
|
lines.push("# TYPE durable_step_duration_ms_sum counter");
|
|
8276
9299
|
lines.push(`durable_step_duration_ms_sum ${m.stepDuration.sumMs}`);
|
|
8277
9300
|
lines.push("# TYPE durable_step_duration_count counter");
|
|
@@ -8442,6 +9465,11 @@ var InMemoryStateStore = class {
|
|
|
8442
9465
|
* so this lets the benchmark contrast "fetch N, scan in JS" against "fetch the k that match".
|
|
8443
9466
|
*/
|
|
8444
9467
|
lastCheckpointRowsRead = 0;
|
|
9468
|
+
/**
|
|
9469
|
+
* Test/observability hook: checkpoint rows materialized since construction, across every
|
|
9470
|
+
* checkpoint read that returns rows — what a run-detail benchmark compares between paths.
|
|
9471
|
+
*/
|
|
9472
|
+
checkpointRowsRead = 0;
|
|
8445
9473
|
key(runId, seq) {
|
|
8446
9474
|
return `${runId}:${seq}`;
|
|
8447
9475
|
}
|
|
@@ -8567,6 +9595,16 @@ var InMemoryStateStore = class {
|
|
|
8567
9595
|
});
|
|
8568
9596
|
return true;
|
|
8569
9597
|
}
|
|
9598
|
+
async saveCheckpointsIfPending(checkpoints) {
|
|
9599
|
+
return checkpoints.map((checkpoint) => {
|
|
9600
|
+
const key = this.key(checkpoint.runId, checkpoint.seq);
|
|
9601
|
+
if (this.checkpoints.get(key)?.status !== "pending") return false;
|
|
9602
|
+
this.checkpoints.set(key, {
|
|
9603
|
+
...checkpoint
|
|
9604
|
+
});
|
|
9605
|
+
return true;
|
|
9606
|
+
});
|
|
9607
|
+
}
|
|
8570
9608
|
async listIncompleteRuns(namespace) {
|
|
8571
9609
|
return [
|
|
8572
9610
|
...this.runs.values()
|
|
@@ -8846,6 +9884,23 @@ var InMemoryStateStore = class {
|
|
|
8846
9884
|
...cp
|
|
8847
9885
|
}));
|
|
8848
9886
|
this.lastCheckpointRowsRead = rows.length;
|
|
9887
|
+
this.checkpointRowsRead += rows.length;
|
|
9888
|
+
return rows;
|
|
9889
|
+
}
|
|
9890
|
+
runCheckpoints(runId) {
|
|
9891
|
+
return [
|
|
9892
|
+
...this.checkpoints.values()
|
|
9893
|
+
].filter((cp) => cp.runId === runId);
|
|
9894
|
+
}
|
|
9895
|
+
async checkpointStats(runId, opts) {
|
|
9896
|
+
return checkpointStatsOf(this.runCheckpoints(runId), opts);
|
|
9897
|
+
}
|
|
9898
|
+
async queryCheckpoints(runId, query) {
|
|
9899
|
+
const rows = queryCheckpointsOf(this.runCheckpoints(runId), query).map((cp) => ({
|
|
9900
|
+
...cp
|
|
9901
|
+
}));
|
|
9902
|
+
this.lastCheckpointRowsRead = rows.length;
|
|
9903
|
+
this.checkpointRowsRead += rows.length;
|
|
8849
9904
|
return rows;
|
|
8850
9905
|
}
|
|
8851
9906
|
async getLatestCheckpointByName(runId, name) {
|
|
@@ -8873,6 +9928,7 @@ var InMemoryStateStore = class {
|
|
|
8873
9928
|
...cp
|
|
8874
9929
|
}));
|
|
8875
9930
|
this.lastCheckpointRowsRead = rows.length;
|
|
9931
|
+
this.checkpointRowsRead += rows.length;
|
|
8876
9932
|
return rows;
|
|
8877
9933
|
}
|
|
8878
9934
|
};
|
|
@@ -8923,6 +9979,7 @@ var InMemoryTransport = class {
|
|
|
8923
9979
|
}
|
|
8924
9980
|
};
|
|
8925
9981
|
export {
|
|
9982
|
+
CHILD_EDGE_PREFIXES,
|
|
8926
9983
|
CONTROL_FLOW_SIGNAL,
|
|
8927
9984
|
CURRENT_PROTOCOL_VERSION,
|
|
8928
9985
|
CodecStateStore,
|
|
@@ -8940,8 +9997,10 @@ export {
|
|
|
8940
9997
|
EVENT_TOKEN_PREFIX,
|
|
8941
9998
|
Entities,
|
|
8942
9999
|
EventAccumulators,
|
|
10000
|
+
FAN_SUMMARY_FAILURES,
|
|
8943
10001
|
FatalError,
|
|
8944
10002
|
GatherError,
|
|
10003
|
+
HANDOFF_SEQ_BASE,
|
|
8945
10004
|
InMemoryAdmissionBackend,
|
|
8946
10005
|
InMemoryStateStore,
|
|
8947
10006
|
InMemoryTransport,
|
|
@@ -8961,6 +10020,7 @@ export {
|
|
|
8961
10020
|
SINGLETON_ADMITTED_TAG,
|
|
8962
10021
|
STATE_STORE,
|
|
8963
10022
|
STATE_STORE_CANONICAL,
|
|
10023
|
+
SUMMARY_CHILD_LIMIT,
|
|
8964
10024
|
ScheduleAlreadyExistsError,
|
|
8965
10025
|
ScheduleClient,
|
|
8966
10026
|
ScheduleNotFoundError,
|
|
@@ -8975,9 +10035,11 @@ export {
|
|
|
8975
10035
|
WorkflowEngine,
|
|
8976
10036
|
WorkflowSuspended,
|
|
8977
10037
|
aggregateAnnouncements,
|
|
10038
|
+
aggregatesCheckpoints,
|
|
8978
10039
|
announcementsOf,
|
|
8979
10040
|
applyAttributeQuery,
|
|
8980
10041
|
assertConcurrency,
|
|
10042
|
+
assertRoutes,
|
|
8981
10043
|
attributeColumnFor,
|
|
8982
10044
|
attributeOperand,
|
|
8983
10045
|
attributePredicateOperands,
|
|
@@ -8987,9 +10049,14 @@ export {
|
|
|
8987
10049
|
buildWorkflowDirectory,
|
|
8988
10050
|
canRoute,
|
|
8989
10051
|
capableWorkers,
|
|
10052
|
+
checkpointNameKind,
|
|
10053
|
+
checkpointStatsOf,
|
|
10054
|
+
childIdsOf,
|
|
8990
10055
|
classifyWaiterToken,
|
|
8991
10056
|
clearDurableExecutionWrappers,
|
|
8992
10057
|
clearTerminalSingletonAdmission,
|
|
10058
|
+
codeAndDetails,
|
|
10059
|
+
codeOf,
|
|
8993
10060
|
collectMetrics,
|
|
8994
10061
|
concurrencyTag,
|
|
8995
10062
|
controlPlaneDescriptor,
|
|
@@ -9002,23 +10069,32 @@ export {
|
|
|
9002
10069
|
debug,
|
|
9003
10070
|
describeWorker,
|
|
9004
10071
|
descriptorHash,
|
|
10072
|
+
detailsOf,
|
|
9005
10073
|
error,
|
|
10074
|
+
etagMatches,
|
|
9006
10075
|
eventMatchOf,
|
|
9007
10076
|
eventMatches,
|
|
9008
10077
|
eventNameOf,
|
|
9009
10078
|
eventPrefix,
|
|
9010
10079
|
eventToken,
|
|
9011
10080
|
facetOrigin,
|
|
10081
|
+
fanPageBounds,
|
|
10082
|
+
fanPageEtag,
|
|
10083
|
+
foldedCount,
|
|
10084
|
+
handoffHandlerId,
|
|
10085
|
+
handoffName,
|
|
9012
10086
|
hasDurableExecutionWrappers,
|
|
9013
10087
|
heartbeatStatus,
|
|
9014
10088
|
indexWaitersByRun,
|
|
9015
10089
|
info,
|
|
9016
10090
|
isEngineMintedTag,
|
|
9017
10091
|
isEventToken,
|
|
10092
|
+
isFailedMember,
|
|
9018
10093
|
isLegacyDescriptor,
|
|
9019
10094
|
isNonEmptyRunScope,
|
|
9020
10095
|
isWorkflowControlFlowSignal,
|
|
9021
10096
|
listSignalWaitersOfRuns,
|
|
10097
|
+
liveHandoff,
|
|
9022
10098
|
log,
|
|
9023
10099
|
matchesAttributes,
|
|
9024
10100
|
mergeRunFacetRows,
|
|
@@ -9029,16 +10105,23 @@ export {
|
|
|
9029
10105
|
normalizeAttributeRows,
|
|
9030
10106
|
normalizeDescriptor,
|
|
9031
10107
|
observedAnnouncements,
|
|
10108
|
+
onErrorRunId,
|
|
10109
|
+
pageFan,
|
|
10110
|
+
pageFanFromStore,
|
|
9032
10111
|
parentRunIdOf,
|
|
9033
10112
|
parseDuration,
|
|
9034
10113
|
partitionOfToken,
|
|
9035
10114
|
planDispatch,
|
|
9036
10115
|
prevCronFireMs,
|
|
10116
|
+
queryCheckpointsOf,
|
|
9037
10117
|
readSearchAttributes,
|
|
9038
10118
|
requiredCapabilities,
|
|
9039
10119
|
resolveConcurrency,
|
|
9040
10120
|
resolveRouting,
|
|
9041
10121
|
resolveRunWaiting,
|
|
10122
|
+
retiredHandoffName,
|
|
10123
|
+
routeFor,
|
|
10124
|
+
runDetailEtag,
|
|
9042
10125
|
runDurableExecution,
|
|
9043
10126
|
runInStepLogger,
|
|
9044
10127
|
runInStepScope,
|
|
@@ -9058,6 +10141,9 @@ export {
|
|
|
9058
10141
|
sub,
|
|
9059
10142
|
subEvent,
|
|
9060
10143
|
subProcess,
|
|
10144
|
+
summarizeFan,
|
|
10145
|
+
summarizeRunFromStore,
|
|
10146
|
+
summarizeTimeline,
|
|
9061
10147
|
tenantGroup,
|
|
9062
10148
|
useDurableExecution,
|
|
9063
10149
|
warn,
|