@dudousxd/nestjs-durable-core 0.72.1 → 0.74.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 +588 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +170 -16
- package/dist/index.d.ts +170 -16
- package/dist/index.js +581 -138
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -56,6 +56,8 @@ __export(index_exports, {
|
|
|
56
56
|
RemoteWorkflowExecutor: () => RemoteWorkflowExecutor,
|
|
57
57
|
RemoteWorkflowTimeout: () => RemoteWorkflowTimeout,
|
|
58
58
|
RunGateway: () => RunGateway,
|
|
59
|
+
SINGLETON_ACTIVE_STATUSES: () => SINGLETON_ACTIVE_STATUSES,
|
|
60
|
+
SINGLETON_ADMITTED_TAG: () => SINGLETON_ADMITTED_TAG,
|
|
59
61
|
STATE_STORE: () => STATE_STORE,
|
|
60
62
|
STATE_STORE_CANONICAL: () => STATE_STORE_CANONICAL,
|
|
61
63
|
ScheduleAlreadyExistsError: () => ScheduleAlreadyExistsError,
|
|
@@ -86,6 +88,7 @@ __export(index_exports, {
|
|
|
86
88
|
capableWorkers: () => capableWorkers,
|
|
87
89
|
classifyWaiterToken: () => classifyWaiterToken,
|
|
88
90
|
clearDurableExecutionWrappers: () => clearDurableExecutionWrappers,
|
|
91
|
+
clearTerminalSingletonAdmission: () => clearTerminalSingletonAdmission,
|
|
89
92
|
collectMetrics: () => collectMetrics,
|
|
90
93
|
concurrencyTag: () => concurrencyTag,
|
|
91
94
|
controlPlaneDescriptor: () => controlPlaneDescriptor,
|
|
@@ -114,6 +117,7 @@ __export(index_exports, {
|
|
|
114
117
|
isLegacyDescriptor: () => isLegacyDescriptor,
|
|
115
118
|
isNonEmptyRunScope: () => isNonEmptyRunScope,
|
|
116
119
|
isWorkflowControlFlowSignal: () => isWorkflowControlFlowSignal,
|
|
120
|
+
listSignalWaitersOfRuns: () => listSignalWaitersOfRuns,
|
|
117
121
|
log: () => log,
|
|
118
122
|
matchesAttributes: () => matchesAttributes,
|
|
119
123
|
mergeRunFacetRows: () => mergeRunFacetRows,
|
|
@@ -124,6 +128,7 @@ __export(index_exports, {
|
|
|
124
128
|
normalizeAttributeRows: () => normalizeAttributeRows,
|
|
125
129
|
normalizeDescriptor: () => normalizeDescriptor,
|
|
126
130
|
observedAnnouncements: () => observedAnnouncements,
|
|
131
|
+
parentRunIdOf: () => parentRunIdOf,
|
|
127
132
|
parseDuration: () => parseDuration,
|
|
128
133
|
partitionOfToken: () => partitionOfToken,
|
|
129
134
|
planDispatch: () => planDispatch,
|
|
@@ -143,6 +148,8 @@ __export(index_exports, {
|
|
|
143
148
|
runValueFacetsFromRuns: () => runValueFacetsFromRuns,
|
|
144
149
|
sanitizeQueueToken: () => sanitizeQueueToken,
|
|
145
150
|
scheduledRunId: () => scheduledRunId,
|
|
151
|
+
singletonAdmissionDenialPatch: () => singletonAdmissionDenialPatch,
|
|
152
|
+
singletonAdmissionTags: () => singletonAdmissionTags,
|
|
146
153
|
sqlComparator: () => sqlComparator,
|
|
147
154
|
stepConfigOf: () => stepConfigOf,
|
|
148
155
|
stepId: () => stepId,
|
|
@@ -545,12 +552,15 @@ var WorkflowSuspended = class extends Error {
|
|
|
545
552
|
}
|
|
546
553
|
/** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
|
|
547
554
|
wakeAt;
|
|
555
|
+
/** Set when awaited children are what the run is waiting on — see {@link FanJoin}. */
|
|
556
|
+
fan;
|
|
548
557
|
/** Marks this as a control-flow signal — see {@link isWorkflowControlFlowSignal}. */
|
|
549
558
|
[CONTROL_FLOW_SIGNAL] = true;
|
|
550
|
-
constructor(wakeAt) {
|
|
559
|
+
constructor(wakeAt, fan) {
|
|
551
560
|
super("workflow suspended");
|
|
552
561
|
this.name = "WorkflowSuspended";
|
|
553
562
|
this.wakeAt = wakeAt;
|
|
563
|
+
this.fan = fan;
|
|
554
564
|
}
|
|
555
565
|
};
|
|
556
566
|
var ContinueAsNew = class extends Error {
|
|
@@ -977,6 +987,21 @@ function instantCheckpoint(p) {
|
|
|
977
987
|
}
|
|
978
988
|
__name(instantCheckpoint, "instantCheckpoint");
|
|
979
989
|
|
|
990
|
+
// src/completion.ts
|
|
991
|
+
function isFailedCompletion(payload) {
|
|
992
|
+
const c = payload;
|
|
993
|
+
return !!c && typeof c === "object" && "ok" in c && c.ok === false;
|
|
994
|
+
}
|
|
995
|
+
__name(isFailedCompletion, "isFailedCompletion");
|
|
996
|
+
function unwrapCompletion(payload, label) {
|
|
997
|
+
const c = payload;
|
|
998
|
+
if (c && typeof c === "object" && "ok" in c && c.ok === false) {
|
|
999
|
+
throw new FatalError(`${label} failed: ${c.error}`);
|
|
1000
|
+
}
|
|
1001
|
+
return c?.value;
|
|
1002
|
+
}
|
|
1003
|
+
__name(unwrapCompletion, "unwrapCompletion");
|
|
1004
|
+
|
|
980
1005
|
// src/concurrency.ts
|
|
981
1006
|
var DEFAULT_CONCURRENCY_STATUSES = [
|
|
982
1007
|
"pending",
|
|
@@ -1216,6 +1241,81 @@ function eventMatches(payload, match) {
|
|
|
1216
1241
|
}
|
|
1217
1242
|
__name(eventMatches, "eventMatches");
|
|
1218
1243
|
|
|
1244
|
+
// src/fan-join.ts
|
|
1245
|
+
function allChildId(runId, firstSeq, index, wave) {
|
|
1246
|
+
return `${allBase(runId, firstSeq, wave)}${index}`;
|
|
1247
|
+
}
|
|
1248
|
+
__name(allChildId, "allChildId");
|
|
1249
|
+
function allGroup(firstSeq, mode) {
|
|
1250
|
+
return mode === "failFast" ? `all:${firstSeq}:failFast` : `all:${firstSeq}`;
|
|
1251
|
+
}
|
|
1252
|
+
__name(allGroup, "allGroup");
|
|
1253
|
+
function allWait(runId, firstSeq, wave) {
|
|
1254
|
+
const base = allBase(runId, firstSeq, wave);
|
|
1255
|
+
return {
|
|
1256
|
+
waiterPrefix: `child:${base}`,
|
|
1257
|
+
checkpointPrefix: `signal:child:${base}`
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
__name(allWait, "allWait");
|
|
1261
|
+
function fanJoin(waits) {
|
|
1262
|
+
return {
|
|
1263
|
+
waits
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
__name(fanJoin, "fanJoin");
|
|
1267
|
+
var FOLDED_PREFIX = "fan:";
|
|
1268
|
+
function allFoldedName(runId, firstSeq, wave) {
|
|
1269
|
+
return `${FOLDED_PREFIX}${allBase(runId, firstSeq, wave)}`;
|
|
1270
|
+
}
|
|
1271
|
+
__name(allFoldedName, "allFoldedName");
|
|
1272
|
+
function foldedChildIds(checkpoint) {
|
|
1273
|
+
const base = checkpoint.name.slice(FOLDED_PREFIX.length);
|
|
1274
|
+
const { from, values } = checkpoint.output;
|
|
1275
|
+
return values.map((_, j) => `${base}${from + j}`);
|
|
1276
|
+
}
|
|
1277
|
+
__name(foldedChildIds, "foldedChildIds");
|
|
1278
|
+
function allBase(runId, firstSeq, wave) {
|
|
1279
|
+
return wave === void 0 ? `${runId}.all.${firstSeq}.` : `${runId}.all.${firstSeq}.w${wave}.`;
|
|
1280
|
+
}
|
|
1281
|
+
__name(allBase, "allBase");
|
|
1282
|
+
function allWaves(size, concurrency) {
|
|
1283
|
+
if (concurrency === void 0 || concurrency >= size) return void 0;
|
|
1284
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
1285
|
+
throw new Error(`ctx.all: concurrency must be a positive integer, got ${concurrency}`);
|
|
1286
|
+
}
|
|
1287
|
+
if (concurrency === 1) return {
|
|
1288
|
+
waveSize: 1,
|
|
1289
|
+
maxWaves: 1
|
|
1290
|
+
};
|
|
1291
|
+
return {
|
|
1292
|
+
waveSize: Math.floor(concurrency / 2),
|
|
1293
|
+
maxWaves: 2
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
__name(allWaves, "allWaves");
|
|
1297
|
+
var ENGINE_CHILD_ID = /^(.+)\.(?:all\.\d+\.(?:w\d+\.)?\d+|child\.\d+)$/;
|
|
1298
|
+
function parentRunIdOf(runId) {
|
|
1299
|
+
return ENGINE_CHILD_ID.exec(runId)?.[1];
|
|
1300
|
+
}
|
|
1301
|
+
__name(parentRunIdOf, "parentRunIdOf");
|
|
1302
|
+
var GROUP = /^all:(\d+)(:failFast)?$/;
|
|
1303
|
+
var REST = /^(?:w(\d+)\.)?\d+$/;
|
|
1304
|
+
function fanOfWaiter(waiter) {
|
|
1305
|
+
const group = waiter.parallelGroup ? GROUP.exec(waiter.parallelGroup) : null;
|
|
1306
|
+
if (!group) return void 0;
|
|
1307
|
+
const base = `child:${waiter.runId}.all.${group[1]}.`;
|
|
1308
|
+
if (!waiter.token.startsWith(base)) return void 0;
|
|
1309
|
+
const rest = REST.exec(waiter.token.slice(base.length));
|
|
1310
|
+
if (!rest) return void 0;
|
|
1311
|
+
const waiterPrefix = rest[1] === void 0 ? base : `${base}w${rest[1]}.`;
|
|
1312
|
+
return {
|
|
1313
|
+
waiterPrefix,
|
|
1314
|
+
failFast: group[2] !== void 0
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
__name(fanOfWaiter, "fanOfWaiter");
|
|
1318
|
+
|
|
1219
1319
|
// src/handshake/descriptor.ts
|
|
1220
1320
|
var CURRENT_PROTOCOL_VERSION = 1;
|
|
1221
1321
|
var LEGACY_V1_PROTOCOL = {
|
|
@@ -1957,6 +2057,23 @@ function resolveRunWaiting(run, waiterByRun) {
|
|
|
1957
2057
|
return waiter ? classifyWaiterToken(waiter.token) : void 0;
|
|
1958
2058
|
}
|
|
1959
2059
|
__name(resolveRunWaiting, "resolveRunWaiting");
|
|
2060
|
+
var RUN_ID_BATCH = 500;
|
|
2061
|
+
async function listSignalWaitersOfRuns(store, runIds) {
|
|
2062
|
+
const unique = [
|
|
2063
|
+
...new Set(runIds)
|
|
2064
|
+
];
|
|
2065
|
+
if (unique.length === 0) return [];
|
|
2066
|
+
if (!store.listSignalWaitersByRun) {
|
|
2067
|
+
const ids = new Set(unique);
|
|
2068
|
+
return (await store.listSignalWaiters("")).filter((w) => ids.has(w.runId));
|
|
2069
|
+
}
|
|
2070
|
+
const out = [];
|
|
2071
|
+
for (let at = 0; at < unique.length; at += RUN_ID_BATCH) {
|
|
2072
|
+
out.push(...await store.listSignalWaitersByRun(unique.slice(at, at + RUN_ID_BATCH)));
|
|
2073
|
+
}
|
|
2074
|
+
return out;
|
|
2075
|
+
}
|
|
2076
|
+
__name(listSignalWaitersOfRuns, "listSignalWaitersOfRuns");
|
|
1960
2077
|
function indexWaitersByRun(waiters) {
|
|
1961
2078
|
const byRun = /* @__PURE__ */ new Map();
|
|
1962
2079
|
for (const w of waiters) if (!byRun.has(w.runId)) byRun.set(w.runId, w);
|
|
@@ -2444,6 +2561,54 @@ function errorMessage(error2) {
|
|
|
2444
2561
|
}
|
|
2445
2562
|
__name(errorMessage, "errorMessage");
|
|
2446
2563
|
|
|
2564
|
+
// src/singleton-admission.ts
|
|
2565
|
+
var SINGLETON_ADMITTED_TAG = "durable:singleton:admitted";
|
|
2566
|
+
var SINGLETON_ACTIVE_STATUSES = [
|
|
2567
|
+
"pending",
|
|
2568
|
+
"running",
|
|
2569
|
+
"suspended",
|
|
2570
|
+
"blocked",
|
|
2571
|
+
"cancelling"
|
|
2572
|
+
];
|
|
2573
|
+
function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
|
|
2574
|
+
if (!Number.isInteger(limit) || limit < 1) throw new Error("singleton limit must be a positive integer");
|
|
2575
|
+
const active = runs.filter((run) => run.workflow === workflow && SINGLETON_ACTIVE_STATUSES.includes(run.status) && run.tags?.includes(tag));
|
|
2576
|
+
const candidate = active.find((run) => run.id === runId);
|
|
2577
|
+
if (!candidate) return null;
|
|
2578
|
+
if (candidate.tags?.includes(SINGLETON_ADMITTED_TAG)) return candidate.tags;
|
|
2579
|
+
if (candidate.status === "cancelling") return null;
|
|
2580
|
+
const holders = active.filter((run) => run.tags?.includes(SINGLETON_ADMITTED_TAG));
|
|
2581
|
+
const remaining = limit - holders.length;
|
|
2582
|
+
if (remaining <= 0) return null;
|
|
2583
|
+
const waiters = active.filter((run) => !run.tags?.includes(SINGLETON_ADMITTED_TAG)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
|
|
2584
|
+
if (!waiters.slice(0, remaining).some((run) => run.id === runId)) return null;
|
|
2585
|
+
return [
|
|
2586
|
+
...candidate.tags ?? [],
|
|
2587
|
+
SINGLETON_ADMITTED_TAG
|
|
2588
|
+
];
|
|
2589
|
+
}
|
|
2590
|
+
__name(singletonAdmissionTags, "singletonAdmissionTags");
|
|
2591
|
+
async function clearTerminalSingletonAdmission(runId, patch, getRun) {
|
|
2592
|
+
if (!patch.status || !TERMINAL_RUN_STATUSES.includes(patch.status)) return patch;
|
|
2593
|
+
const tags = "tags" in patch ? patch.tags : (await getRun(runId))?.tags;
|
|
2594
|
+
return {
|
|
2595
|
+
...patch,
|
|
2596
|
+
tags: tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
__name(clearTerminalSingletonAdmission, "clearTerminalSingletonAdmission");
|
|
2600
|
+
function singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt) {
|
|
2601
|
+
if (retryWakeAt === void 0) return void 0;
|
|
2602
|
+
const candidate = runs.find((run) => run.id === runId && run.workflow === workflow && run.tags?.includes(tag) && SINGLETON_ACTIVE_STATUSES.includes(run.status));
|
|
2603
|
+
if (!candidate || candidate.status === "cancelling") return void 0;
|
|
2604
|
+
return {
|
|
2605
|
+
status: "suspended",
|
|
2606
|
+
wakeAt: retryWakeAt,
|
|
2607
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
__name(singletonAdmissionDenialPatch, "singletonAdmissionDenialPatch");
|
|
2611
|
+
|
|
2447
2612
|
// src/singleton-gate.ts
|
|
2448
2613
|
var SINGLETON_RETRY_MS = 1e3;
|
|
2449
2614
|
var SINGLETON_RETRY_JITTER_MS = 250;
|
|
@@ -2457,6 +2622,9 @@ var SingletonGate = class {
|
|
|
2457
2622
|
}
|
|
2458
2623
|
/** The tag a singleton run carries, so the gate can find others sharing its key. */
|
|
2459
2624
|
tag(cfg, input) {
|
|
2625
|
+
if (!this.deps.store.tryAdmitSingleton) {
|
|
2626
|
+
throw new Error("Singleton workflows require StateStore.tryAdmitSingleton for atomic durable admission");
|
|
2627
|
+
}
|
|
2460
2628
|
return `singleton:${cfg.key(input)}`;
|
|
2461
2629
|
}
|
|
2462
2630
|
/** Next wake time for a gated run: the base retry delay jittered to avoid a wakeup stampede. */
|
|
@@ -2465,7 +2633,7 @@ var SingletonGate = class {
|
|
|
2465
2633
|
}
|
|
2466
2634
|
/**
|
|
2467
2635
|
* Reject a start that would grow the same-key backlog past `limit + maxQueueDepth` (counting
|
|
2468
|
-
* `pending`/`running`/`suspended` runs sharing the key in one scan). No-op when no `maxQueueDepth`
|
|
2636
|
+
* `pending`/`running`/`suspended`/`blocked`/`cancelling` runs sharing the key in one scan). No-op when no `maxQueueDepth`
|
|
2469
2637
|
* is configured.
|
|
2470
2638
|
*/
|
|
2471
2639
|
async assertCapacity(workflow, cfg, input) {
|
|
@@ -2474,34 +2642,25 @@ var SingletonGate = class {
|
|
|
2474
2642
|
const queued = await this.deps.store.listRuns({
|
|
2475
2643
|
tag: this.tag(cfg, input),
|
|
2476
2644
|
workflow,
|
|
2477
|
-
statuses:
|
|
2478
|
-
"pending",
|
|
2479
|
-
"running",
|
|
2480
|
-
"suspended",
|
|
2481
|
-
"cancelling"
|
|
2482
|
-
]
|
|
2645
|
+
statuses: SINGLETON_ACTIVE_STATUSES
|
|
2483
2646
|
});
|
|
2484
2647
|
if (queued.length >= cap) {
|
|
2485
2648
|
throw new SingletonQueueFullError(workflow, cfg.key(input), cfg.maxQueueDepth);
|
|
2486
2649
|
}
|
|
2487
2650
|
}
|
|
2488
|
-
/**
|
|
2489
|
-
* Whether `run` may run now under its key: it's among the `limit` oldest in-flight (running or
|
|
2490
|
-
* suspended) runs sharing the key, by `(createdAt, id)` order. A consistent store gives every
|
|
2491
|
-
* instance the same ordering, so admission is race-free + FIFO.
|
|
2492
|
-
*/
|
|
2651
|
+
/** Preserve durable holders and atomically claim one of the remaining slots. */
|
|
2493
2652
|
async admit(run, cfg) {
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
return
|
|
2653
|
+
if (!this.deps.store.tryAdmitSingleton) {
|
|
2654
|
+
throw new Error("Singleton workflows require StateStore.tryAdmitSingleton for atomic durable admission");
|
|
2655
|
+
}
|
|
2656
|
+
const admitted = await this.deps.store.tryAdmitSingleton(run.id, this.tag(cfg, run.input), run.workflow, cfg.limit ?? 1, this.retryWakeAt());
|
|
2657
|
+
if (admitted && !run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
|
|
2658
|
+
run.tags = [
|
|
2659
|
+
...run.tags ?? [],
|
|
2660
|
+
SINGLETON_ADMITTED_TAG
|
|
2661
|
+
];
|
|
2662
|
+
}
|
|
2663
|
+
return admitted;
|
|
2505
2664
|
}
|
|
2506
2665
|
/**
|
|
2507
2666
|
* Notify-on-release: a singleton run settled (freeing a slot), so dispatch the oldest gated
|
|
@@ -2725,16 +2884,6 @@ var TransportPool = class {
|
|
|
2725
2884
|
}
|
|
2726
2885
|
};
|
|
2727
2886
|
|
|
2728
|
-
// src/completion.ts
|
|
2729
|
-
function unwrapCompletion(payload, label) {
|
|
2730
|
-
const c = payload;
|
|
2731
|
-
if (c && typeof c === "object" && "ok" in c && c.ok === false) {
|
|
2732
|
-
throw new FatalError(`${label} failed: ${c.error}`);
|
|
2733
|
-
}
|
|
2734
|
-
return c?.value;
|
|
2735
|
-
}
|
|
2736
|
-
__name(unwrapCompletion, "unwrapCompletion");
|
|
2737
|
-
|
|
2738
2887
|
// src/step-name-symbol.ts
|
|
2739
2888
|
var DURABLE_STEP_NAME = Symbol.for("durable.step.name");
|
|
2740
2889
|
var DURABLE_STEP_CONFIG = Symbol.for("durable.step.config");
|
|
@@ -2756,6 +2905,8 @@ function stepConfigOf(ref) {
|
|
|
2756
2905
|
__name(stepConfigOf, "stepConfigOf");
|
|
2757
2906
|
|
|
2758
2907
|
// src/workflow-ctx.ts
|
|
2908
|
+
var FAN_CHUNK = 500;
|
|
2909
|
+
var FOLD_MIN_FAN = 100;
|
|
2759
2910
|
function normalizeChildOptions(options) {
|
|
2760
2911
|
return typeof options === "string" ? {
|
|
2761
2912
|
childId: options
|
|
@@ -3098,7 +3249,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3098
3249
|
if (existing && existing.status === "completed") {
|
|
3099
3250
|
return unwrapCompletion(existing.output, `child "${id}"`);
|
|
3100
3251
|
}
|
|
3101
|
-
await store.putSignalWaiter({
|
|
3252
|
+
if (!existing) await store.putSignalWaiter({
|
|
3102
3253
|
token: `child:${id}`,
|
|
3103
3254
|
runId,
|
|
3104
3255
|
seq: current
|
|
@@ -3129,16 +3280,33 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3129
3280
|
status: "running"
|
|
3130
3281
|
}));
|
|
3131
3282
|
}
|
|
3132
|
-
throw new WorkflowSuspended(
|
|
3283
|
+
throw new WorkflowSuspended(void 0, fanJoin([
|
|
3284
|
+
{
|
|
3285
|
+
waiterPrefix: `child:${id}`,
|
|
3286
|
+
checkpointPrefix: `signal:child:${id}`
|
|
3287
|
+
}
|
|
3288
|
+
]));
|
|
3133
3289
|
}, "child");
|
|
3134
3290
|
const all = /* @__PURE__ */ __name(async (workflow2, inputs, opts) => {
|
|
3135
3291
|
if (inputs.length === 0) return [];
|
|
3136
3292
|
const mode = opts?.mode ?? "waitAll";
|
|
3137
3293
|
const name = workflowName(workflow2);
|
|
3294
|
+
const waves = allWaves(inputs.length, opts?.concurrency);
|
|
3138
3295
|
const positions = inputs.map(() => pos.next());
|
|
3139
|
-
const
|
|
3140
|
-
const
|
|
3141
|
-
const
|
|
3296
|
+
const firstSeq = positions[0];
|
|
3297
|
+
const group = allGroup(firstSeq, mode);
|
|
3298
|
+
const waveOf = /* @__PURE__ */ __name((i) => waves ? Math.floor(i / waves.waveSize) : void 0, "waveOf");
|
|
3299
|
+
const id = /* @__PURE__ */ __name((i) => allChildId(runId, firstSeq, i, waveOf(i)), "id");
|
|
3300
|
+
const { checkpoints: existing, folded } = await readFan(positions, [
|
|
3301
|
+
allWait(runId, firstSeq).checkpointPrefix,
|
|
3302
|
+
allFoldedName(runId, firstSeq)
|
|
3303
|
+
]);
|
|
3304
|
+
const waveCount = waves ? Math.ceil(inputs.length / waves.waveSize) : 1;
|
|
3305
|
+
const waveItems = /* @__PURE__ */ __name((k) => {
|
|
3306
|
+
if (!waves) return inputs.map((_, i) => i);
|
|
3307
|
+
const from = k * waves.waveSize;
|
|
3308
|
+
return inputs.map((_, i) => i).slice(from, from + waves.waveSize);
|
|
3309
|
+
}, "waveItems");
|
|
3142
3310
|
if (mode === "failFast") {
|
|
3143
3311
|
for (let i = 0; i < inputs.length; i += 1) {
|
|
3144
3312
|
const cp = existing[i];
|
|
@@ -3157,32 +3325,91 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3157
3325
|
}
|
|
3158
3326
|
}
|
|
3159
3327
|
}
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
if (!cp) {
|
|
3175
|
-
await writeCheckpoint(instantCheckpoint({
|
|
3328
|
+
if (store.compactCheckpoints && inputs.length >= FOLD_MIN_FAN) {
|
|
3329
|
+
for (let k = 0; k < waveCount; k += 1) {
|
|
3330
|
+
const items = waveItems(k);
|
|
3331
|
+
const first = items[0];
|
|
3332
|
+
const last = items[items.length - 1];
|
|
3333
|
+
if (folded[first]) continue;
|
|
3334
|
+
const values = [];
|
|
3335
|
+
for (const i of items) {
|
|
3336
|
+
const c = existing[i]?.status === "completed" ? existing[i]?.output : void 0;
|
|
3337
|
+
if (!c || typeof c !== "object" || c.ok !== true) break;
|
|
3338
|
+
values.push(c.value);
|
|
3339
|
+
}
|
|
3340
|
+
if (values.length < items.length) continue;
|
|
3341
|
+
const into = instantCheckpoint({
|
|
3176
3342
|
runId,
|
|
3177
|
-
seq,
|
|
3178
|
-
name:
|
|
3343
|
+
seq: positions[first],
|
|
3344
|
+
name: allFoldedName(runId, firstSeq, waves ? k : void 0),
|
|
3179
3345
|
kind: "signal",
|
|
3180
|
-
|
|
3346
|
+
output: {
|
|
3347
|
+
from: first,
|
|
3348
|
+
values
|
|
3349
|
+
},
|
|
3181
3350
|
parallelGroup: group
|
|
3351
|
+
});
|
|
3352
|
+
await store.compactCheckpoints(runId, positions[first], positions[last], into);
|
|
3353
|
+
for (const i of items) replay?.delete(positions[i]);
|
|
3354
|
+
replay?.set(into.seq, into);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
const sent = /* @__PURE__ */ __name((k) => waveItems(k).some((i) => existing[i]), "sent");
|
|
3358
|
+
const settled = /* @__PURE__ */ __name((k) => waveItems(k).every((i) => existing[i]?.status === "completed"), "settled");
|
|
3359
|
+
const active = [];
|
|
3360
|
+
let unsettled = 0;
|
|
3361
|
+
for (let k = 0; k < waveCount; k += 1) {
|
|
3362
|
+
if (settled(k)) continue;
|
|
3363
|
+
if (sent(k) || unsettled < (waves?.maxWaves ?? 1)) {
|
|
3364
|
+
active.push(k);
|
|
3365
|
+
unsettled += 1;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
if (active.length > 0) {
|
|
3369
|
+
const toRegister = [];
|
|
3370
|
+
const toEnsure = [];
|
|
3371
|
+
for (const k of active) {
|
|
3372
|
+
for (const i of waveItems(k)) {
|
|
3373
|
+
const cp = existing[i];
|
|
3374
|
+
if (cp?.status === "completed") continue;
|
|
3375
|
+
(cp ? toEnsure : toRegister).push(i);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
for (let at = 0; at < toRegister.length; at += FAN_CHUNK) {
|
|
3379
|
+
const chunk = toRegister.slice(at, at + FAN_CHUNK);
|
|
3380
|
+
const entries = chunk.map((i) => ({
|
|
3381
|
+
// Carry the fan `group` onto the waiter too: the child's terminal `signal:child:`
|
|
3382
|
+
// checkpoint is (re)written by engine.signal when the child notifies the parent,
|
|
3383
|
+
// OVERWRITING the running placeholder at the same seq — so without this the resolved
|
|
3384
|
+
// checkpoint would lose the group and the dashboard would render the resolved fan as a
|
|
3385
|
+
// sequential chain.
|
|
3386
|
+
waiter: {
|
|
3387
|
+
token: `child:${id(i)}`,
|
|
3388
|
+
runId,
|
|
3389
|
+
seq: positions[i],
|
|
3390
|
+
parallelGroup: group
|
|
3391
|
+
},
|
|
3392
|
+
checkpoint: instantCheckpoint({
|
|
3393
|
+
runId,
|
|
3394
|
+
seq: positions[i],
|
|
3395
|
+
name: `signal:child:${id(i)}`,
|
|
3396
|
+
kind: "signal",
|
|
3397
|
+
status: "running",
|
|
3398
|
+
parallelGroup: group
|
|
3399
|
+
})
|
|
3400
|
+
}));
|
|
3401
|
+
await registerChildWaits(entries);
|
|
3402
|
+
for (const i of chunk) host.startChild(name, inputs[i], id(i));
|
|
3403
|
+
}
|
|
3404
|
+
for (let at = 0; at < toEnsure.length; at += FAN_CHUNK) {
|
|
3405
|
+
await Promise.all(toEnsure.slice(at, at + FAN_CHUNK).map(async (i) => {
|
|
3406
|
+
if (!await store.getRun(id(i))) host.startChild(name, inputs[i], id(i));
|
|
3182
3407
|
}));
|
|
3183
3408
|
}
|
|
3184
3409
|
}
|
|
3185
|
-
if (
|
|
3410
|
+
if (active.length > 0) {
|
|
3411
|
+
throw new WorkflowSuspended(void 0, fanJoin(active.map((k) => allWait(runId, firstSeq, waves ? k : void 0))));
|
|
3412
|
+
}
|
|
3186
3413
|
const outputs = [];
|
|
3187
3414
|
const failures = [];
|
|
3188
3415
|
for (let i = 0; i < inputs.length; i += 1) {
|
|
@@ -3201,6 +3428,67 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3201
3428
|
if (failures.length > 0) throw new GatherError(failures, inputs.length);
|
|
3202
3429
|
return outputs;
|
|
3203
3430
|
}, "all");
|
|
3431
|
+
const readFan = /* @__PURE__ */ __name(async (positions, prefixes) => {
|
|
3432
|
+
const checkpoints = positions.map((seq) => replay?.get(seq) ?? null);
|
|
3433
|
+
const folded = positions.map(() => false);
|
|
3434
|
+
const expand = /* @__PURE__ */ __name(() => {
|
|
3435
|
+
for (let i = 0; i < checkpoints.length; i += 1) {
|
|
3436
|
+
const cp = checkpoints[i];
|
|
3437
|
+
if (!cp?.name.startsWith(FOLDED_PREFIX) || folded[i]) continue;
|
|
3438
|
+
const fold = cp.output;
|
|
3439
|
+
if (!fold || typeof fold.from !== "number" || !Array.isArray(fold.values)) {
|
|
3440
|
+
throw new Error(`ctx.all: folded checkpoint "${cp.name}" at seq ${cp.seq} is malformed`);
|
|
3441
|
+
}
|
|
3442
|
+
const { from, values } = fold;
|
|
3443
|
+
values.forEach((value, j) => {
|
|
3444
|
+
checkpoints[from + j] = {
|
|
3445
|
+
...cp,
|
|
3446
|
+
seq: positions[from + j],
|
|
3447
|
+
output: {
|
|
3448
|
+
ok: true,
|
|
3449
|
+
value
|
|
3450
|
+
}
|
|
3451
|
+
};
|
|
3452
|
+
folded[from + j] = true;
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
}, "expand");
|
|
3456
|
+
expand();
|
|
3457
|
+
const missing = positions.filter((_, i) => !checkpoints[i]);
|
|
3458
|
+
if (missing.length === 0) return {
|
|
3459
|
+
checkpoints,
|
|
3460
|
+
folded
|
|
3461
|
+
};
|
|
3462
|
+
if (missing.length > 1 && store.listCheckpointsByNamePrefix) {
|
|
3463
|
+
const bySeq = /* @__PURE__ */ new Map();
|
|
3464
|
+
for (const cp of await store.listCheckpointsByNamePrefix(runId, prefixes)) {
|
|
3465
|
+
bySeq.set(cp.seq, cp);
|
|
3466
|
+
}
|
|
3467
|
+
positions.forEach((seq, i) => {
|
|
3468
|
+
checkpoints[i] ??= bySeq.get(seq) ?? null;
|
|
3469
|
+
});
|
|
3470
|
+
} else {
|
|
3471
|
+
await Promise.all(positions.map(async (seq, i) => {
|
|
3472
|
+
checkpoints[i] ??= await readCheckpoint(seq);
|
|
3473
|
+
}));
|
|
3474
|
+
}
|
|
3475
|
+
expand();
|
|
3476
|
+
return {
|
|
3477
|
+
checkpoints,
|
|
3478
|
+
folded
|
|
3479
|
+
};
|
|
3480
|
+
}, "readFan");
|
|
3481
|
+
const registerChildWaits = /* @__PURE__ */ __name(async (entries) => {
|
|
3482
|
+
if (store.registerChildWaits) {
|
|
3483
|
+
await store.registerChildWaits(entries);
|
|
3484
|
+
} else {
|
|
3485
|
+
await Promise.all(entries.map(async (e) => {
|
|
3486
|
+
await store.putSignalWaiter(e.waiter);
|
|
3487
|
+
await store.saveCheckpoint(e.checkpoint);
|
|
3488
|
+
}));
|
|
3489
|
+
}
|
|
3490
|
+
for (const e of entries) replay?.set(e.checkpoint.seq, e.checkpoint);
|
|
3491
|
+
}, "registerChildWaits");
|
|
3204
3492
|
const startChild = /* @__PURE__ */ __name(async (workflow2, input, options) => {
|
|
3205
3493
|
const { childId, priority, version } = normalizeChildOptions(options);
|
|
3206
3494
|
const current = pos.next();
|
|
@@ -4018,7 +4306,7 @@ var WorkflowEngine = class {
|
|
|
4018
4306
|
VERSION_UNDECLARED_TAG
|
|
4019
4307
|
] : []
|
|
4020
4308
|
];
|
|
4021
|
-
const tags = mergeTags(registered.tags, stampedTags);
|
|
4309
|
+
const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
|
|
4022
4310
|
if (registered.singleton) {
|
|
4023
4311
|
await this.singletons.assertCapacity(name, registered.singleton, input);
|
|
4024
4312
|
}
|
|
@@ -4093,6 +4381,12 @@ var WorkflowEngine = class {
|
|
|
4093
4381
|
await this.store.releaseRunLock(runId);
|
|
4094
4382
|
throw new NamespaceMismatch();
|
|
4095
4383
|
}
|
|
4384
|
+
if (run.status === "failed" && run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
|
|
4385
|
+
run.tags = run.tags.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
|
|
4386
|
+
await this.store.updateRun(run.id, {
|
|
4387
|
+
tags: run.tags
|
|
4388
|
+
});
|
|
4389
|
+
}
|
|
4096
4390
|
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
|
|
4097
4391
|
if (!registered) {
|
|
4098
4392
|
throw new Error(`workflow ${run.workflow}@${run.workflowVersion} is not registered \u2014 keep the prior version deployed so in-flight runs can drain (skew protection)`);
|
|
@@ -4234,15 +4528,16 @@ var WorkflowEngine = class {
|
|
|
4234
4528
|
*/
|
|
4235
4529
|
async findParentRunId(childRunId) {
|
|
4236
4530
|
const token = `child:${childRunId}`;
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
}
|
|
4240
|
-
return void 0;
|
|
4531
|
+
const waiters = await this.store.listSignalWaiters(token);
|
|
4532
|
+
return waiters.find((waiter) => waiter.token === token)?.runId;
|
|
4241
4533
|
}
|
|
4242
4534
|
/** Track an in-flight execution so {@link drain} can wait for it. */
|
|
4243
4535
|
track(p) {
|
|
4244
4536
|
this.inflight.add(p);
|
|
4245
|
-
|
|
4537
|
+
const done = /* @__PURE__ */ __name(() => {
|
|
4538
|
+
this.inflight.delete(p);
|
|
4539
|
+
}, "done");
|
|
4540
|
+
void p.then(done, done);
|
|
4246
4541
|
return p;
|
|
4247
4542
|
}
|
|
4248
4543
|
/**
|
|
@@ -4455,7 +4750,10 @@ var WorkflowEngine = class {
|
|
|
4455
4750
|
const due = await this.store.listDueTimers(nowMs, this.namespace);
|
|
4456
4751
|
const blocked = await this.dueBlockedRuns(nowMs);
|
|
4457
4752
|
if (due.length === 0 && blocked.length === 0) return [];
|
|
4458
|
-
const waiterByRun = indexWaitersByRun(await this.store
|
|
4753
|
+
const waiterByRun = indexWaitersByRun(await listSignalWaitersOfRuns(this.store, [
|
|
4754
|
+
...due,
|
|
4755
|
+
...blocked
|
|
4756
|
+
].map((run) => run.id)));
|
|
4459
4757
|
return this.resumeLeased([
|
|
4460
4758
|
...due,
|
|
4461
4759
|
...blocked
|
|
@@ -4565,11 +4863,6 @@ var WorkflowEngine = class {
|
|
|
4565
4863
|
} : {}
|
|
4566
4864
|
});
|
|
4567
4865
|
} else if (cp.kind === "signal" && cp.name.startsWith("signal:child:") && cp.status === "completed" && isFailureCompletion(cp.output)) {
|
|
4568
|
-
await this.store.saveCheckpoint({
|
|
4569
|
-
...cp,
|
|
4570
|
-
status: "running",
|
|
4571
|
-
output: void 0
|
|
4572
|
-
});
|
|
4573
4866
|
const childId = cp.name.slice("signal:child:".length);
|
|
4574
4867
|
const parked = [];
|
|
4575
4868
|
for (; ; ) {
|
|
@@ -4577,8 +4870,28 @@ var WorkflowEngine = class {
|
|
|
4577
4870
|
if (!buffered) break;
|
|
4578
4871
|
parked.push(buffered.payload);
|
|
4579
4872
|
}
|
|
4873
|
+
const successAt = parked.findIndex((payload) => typeof payload === "object" && payload !== null && payload.ok === true);
|
|
4874
|
+
const success = successAt === -1 ? void 0 : parked.splice(successAt, 1)[0];
|
|
4580
4875
|
for (const payload of parked) await this.store.bufferSignal(`child:${childId}`, payload);
|
|
4581
|
-
const bufferedSuccess =
|
|
4876
|
+
const bufferedSuccess = success !== void 0;
|
|
4877
|
+
if (bufferedSuccess) {
|
|
4878
|
+
await this.store.saveCheckpoint({
|
|
4879
|
+
...cp,
|
|
4880
|
+
output: success
|
|
4881
|
+
});
|
|
4882
|
+
} else {
|
|
4883
|
+
await this.store.putSignalWaiter({
|
|
4884
|
+
token: `child:${childId}`,
|
|
4885
|
+
runId,
|
|
4886
|
+
seq: cp.seq,
|
|
4887
|
+
parallelGroup: cp.parallelGroup
|
|
4888
|
+
});
|
|
4889
|
+
await this.store.saveCheckpoint({
|
|
4890
|
+
...cp,
|
|
4891
|
+
status: "running",
|
|
4892
|
+
output: void 0
|
|
4893
|
+
});
|
|
4894
|
+
}
|
|
4582
4895
|
if (!bufferedSuccess) {
|
|
4583
4896
|
const child = await this.store.getRun(childId);
|
|
4584
4897
|
if (child && (child.status === "failed" || child.status === "dead")) {
|
|
@@ -4593,7 +4906,8 @@ var WorkflowEngine = class {
|
|
|
4593
4906
|
status: "pending",
|
|
4594
4907
|
error: void 0,
|
|
4595
4908
|
...resurrecting ? {
|
|
4596
|
-
recoveryAttempts: 0
|
|
4909
|
+
recoveryAttempts: 0,
|
|
4910
|
+
tags: run.tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
4597
4911
|
} : {},
|
|
4598
4912
|
updatedAt: /* @__PURE__ */ new Date()
|
|
4599
4913
|
});
|
|
@@ -4712,10 +5026,19 @@ var WorkflowEngine = class {
|
|
|
4712
5026
|
off();
|
|
4713
5027
|
resolve(toResult(run));
|
|
4714
5028
|
}, "finish");
|
|
5029
|
+
const fail = /* @__PURE__ */ __name((error2) => {
|
|
5030
|
+
if (done) return;
|
|
5031
|
+
done = true;
|
|
5032
|
+
if (timer) clearTimeout(timer);
|
|
5033
|
+
if (poller) clearInterval(poller);
|
|
5034
|
+
off();
|
|
5035
|
+
reject(error2);
|
|
5036
|
+
}, "fail");
|
|
4715
5037
|
const check = /* @__PURE__ */ __name(() => {
|
|
5038
|
+
if (done) return;
|
|
4716
5039
|
void this.store.getRun(runId).then((run) => {
|
|
4717
5040
|
if (run && isSettled(run)) finish(run);
|
|
4718
|
-
});
|
|
5041
|
+
}, fail);
|
|
4719
5042
|
}, "check");
|
|
4720
5043
|
const poller = opts?.until === "terminal" ? (() => {
|
|
4721
5044
|
const interval = setInterval(check, 250);
|
|
@@ -4848,12 +5171,62 @@ var WorkflowEngine = class {
|
|
|
4848
5171
|
// for an ordinary (non-fan) signal/child await.
|
|
4849
5172
|
parallelGroup: waiter.parallelGroup
|
|
4850
5173
|
}));
|
|
5174
|
+
if (await this.fanStillWaiting(waiter, payload)) {
|
|
5175
|
+
return {
|
|
5176
|
+
runId: waiter.runId,
|
|
5177
|
+
status: "suspended"
|
|
5178
|
+
};
|
|
5179
|
+
}
|
|
4851
5180
|
return this.resumeImplicit(waiter.runId).catch((err) => {
|
|
4852
5181
|
if (err instanceof NamespaceMismatch) return null;
|
|
4853
5182
|
throw err;
|
|
4854
5183
|
});
|
|
4855
5184
|
}
|
|
4856
5185
|
/**
|
|
5186
|
+
* The `ctx.all` join barrier: true when `waiter` was one child of a fan (or of one wave of it)
|
|
5187
|
+
* whose siblings are still out, so waking the parent now would only replay the fan and re-suspend.
|
|
5188
|
+
* Skipping that wake is what keeps a fan of N children at O(N) work instead of N replays of N items.
|
|
5189
|
+
*
|
|
5190
|
+
* Safe because the LAST sibling to settle always wakes the parent: each child consumes its own
|
|
5191
|
+
* waiter before it asks, so whichever asks last sees none left. A failed child of a failFast fan
|
|
5192
|
+
* always wakes it. Anything that is not a `ctx.all` waiter, or a store that cannot answer the
|
|
5193
|
+
* question cheaply, wakes the parent as before.
|
|
5194
|
+
*/
|
|
5195
|
+
async fanStillWaiting(waiter, payload) {
|
|
5196
|
+
if (!this.store.hasSignalWaiter) return false;
|
|
5197
|
+
const fan = fanOfWaiter(waiter);
|
|
5198
|
+
if (!fan) return false;
|
|
5199
|
+
if (fan.failFast && isFailedCompletion(payload)) return false;
|
|
5200
|
+
return this.store.hasSignalWaiter(waiter.runId, fan.waiterPrefix);
|
|
5201
|
+
}
|
|
5202
|
+
/**
|
|
5203
|
+
* Re-drive a run that just suspended on awaited children (a `ctx.all` fan or wave, a `ctx.child`)
|
|
5204
|
+
* which, by now, have settled.
|
|
5205
|
+
*
|
|
5206
|
+
* {@link execute} drops a wake that finds the run's lease held. So if a child settles while the
|
|
5207
|
+
* parent is still executing (the turn that dispatched it, or one woken by an earlier child), that
|
|
5208
|
+
* wake is spent, and the turn suspends on a snapshot that predates the child. With the `ctx.all`
|
|
5209
|
+
* barrier that wake was the only one the fan would get. Nothing would wake the run until the
|
|
5210
|
+
* `reconcileMs` sweep, minutes later.
|
|
5211
|
+
*
|
|
5212
|
+
* Called after the turn released its lease, so any child that settles from here on wakes the run
|
|
5213
|
+
* itself. Only re-drives for a group with no waiter left whose checkpoints are all settled, which
|
|
5214
|
+
* the next turn is then guaranteed to see — so it cannot loop.
|
|
5215
|
+
*/
|
|
5216
|
+
async redriveSettledChildren(runId, fan) {
|
|
5217
|
+
if (!this.store.hasSignalWaiter) return;
|
|
5218
|
+
for (const wait of fan.waits) {
|
|
5219
|
+
if (await this.store.hasSignalWaiter(runId, wait.waiterPrefix)) continue;
|
|
5220
|
+
const checkpoints = this.store.listCheckpointsByNamePrefix ? await this.store.listCheckpointsByNamePrefix(runId, [
|
|
5221
|
+
wait.checkpointPrefix
|
|
5222
|
+
]) : (await this.store.listCheckpoints(runId)).filter((cp) => cp.name.startsWith(wait.checkpointPrefix));
|
|
5223
|
+
if (checkpoints.length > 0 && checkpoints.every((cp) => cp.status === "completed")) {
|
|
5224
|
+
await this.resumeImplicit(runId);
|
|
5225
|
+
return;
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
/**
|
|
4857
5230
|
* Deliver an external signal to the run waiting on `token`, resume it with `payload`, and return
|
|
4858
5231
|
* the run result — or null if no run is (or ends up) waiting for it.
|
|
4859
5232
|
*
|
|
@@ -5146,23 +5519,11 @@ var WorkflowEngine = class {
|
|
|
5146
5519
|
}
|
|
5147
5520
|
/** The given runs plus every descendant, children before parents. One waiter scan for the batch. */
|
|
5148
5521
|
async collectSubtrees(rootIds) {
|
|
5149
|
-
const
|
|
5150
|
-
for (const w of await this.store.listSignalWaiters("child:")) {
|
|
5151
|
-
const list = waitersByParent.get(w.runId) ?? [];
|
|
5152
|
-
list.push(w.token.slice("child:".length));
|
|
5153
|
-
waitersByParent.set(w.runId, list);
|
|
5154
|
-
}
|
|
5155
|
-
const prefixes = [
|
|
5156
|
-
"signal:child:",
|
|
5157
|
-
"spawn:"
|
|
5158
|
-
];
|
|
5522
|
+
const prefixes = CHILD_EDGE_PREFIXES;
|
|
5159
5523
|
const childrenOf = /* @__PURE__ */ __name(async (parent) => {
|
|
5160
|
-
const ids = new Set(
|
|
5524
|
+
const ids = new Set(await this.awaitedChildIds(parent));
|
|
5161
5525
|
const cps = this.store.listCheckpointsByNamePrefix ? await this.store.listCheckpointsByNamePrefix(parent, prefixes) : (await this.store.listCheckpoints(parent)).filter((cp) => prefixes.some((p) => cp.name.startsWith(p)));
|
|
5162
|
-
for (const
|
|
5163
|
-
if (cp.name.startsWith("signal:child:")) ids.add(cp.name.slice("signal:child:".length));
|
|
5164
|
-
if (cp.name.startsWith("spawn:") && typeof cp.output === "string") ids.add(cp.output);
|
|
5165
|
-
}
|
|
5526
|
+
for (const id of childIdsOf(cps)) ids.add(id);
|
|
5166
5527
|
return [
|
|
5167
5528
|
...ids
|
|
5168
5529
|
];
|
|
@@ -5212,22 +5573,20 @@ var WorkflowEngine = class {
|
|
|
5212
5573
|
* so a completed parent (or a completed awaited child) would otherwise drop out of the tree. The
|
|
5213
5574
|
* `signal:child:<id>` checkpoint (the placeholder written when the parent first awaits the child,
|
|
5214
5575
|
* overwritten as completed/failed when it settles) persists across completion, so we read both and
|
|
5215
|
-
* dedupe — the edge stays stable for a finished run, not just a live one.
|
|
5576
|
+
* dedupe — the edge stays stable for a finished run, not just a live one. A settled `ctx.all`
|
|
5577
|
+
* group folded into one `fan:` checkpoint stands for every child in it.
|
|
5216
5578
|
*/
|
|
5579
|
+
/** The children `parentRunId` is awaiting right now, read off its own live `child:` waiters. */
|
|
5580
|
+
async awaitedChildIds(parentRunId) {
|
|
5581
|
+
return (await listSignalWaitersOfRuns(this.store, [
|
|
5582
|
+
parentRunId
|
|
5583
|
+
])).filter((w) => w.token.startsWith("child:")).map((w) => w.token.slice("child:".length));
|
|
5584
|
+
}
|
|
5217
5585
|
async getRunChildren(parentRunId) {
|
|
5218
|
-
const childIds =
|
|
5219
|
-
|
|
5220
|
-
if (w.runId === parentRunId) childIds.add(w.token.slice("child:".length));
|
|
5221
|
-
}
|
|
5222
|
-
const prefixes = [
|
|
5223
|
-
"signal:child:",
|
|
5224
|
-
"spawn:"
|
|
5225
|
-
];
|
|
5586
|
+
const childIds = new Set(await this.awaitedChildIds(parentRunId));
|
|
5587
|
+
const prefixes = CHILD_EDGE_PREFIXES;
|
|
5226
5588
|
const childCheckpoints = this.store.listCheckpointsByNamePrefix ? await this.store.listCheckpointsByNamePrefix(parentRunId, prefixes) : (await this.store.listCheckpoints(parentRunId)).filter((cp) => prefixes.some((p) => cp.name.startsWith(p)));
|
|
5227
|
-
for (const
|
|
5228
|
-
if (cp.name.startsWith("signal:child:")) childIds.add(cp.name.slice("signal:child:".length));
|
|
5229
|
-
if (cp.name.startsWith("spawn:") && typeof cp.output === "string") childIds.add(cp.output);
|
|
5230
|
-
}
|
|
5589
|
+
for (const id of childIdsOf(childCheckpoints)) childIds.add(id);
|
|
5231
5590
|
return [
|
|
5232
5591
|
...childIds
|
|
5233
5592
|
];
|
|
@@ -5498,6 +5857,26 @@ var WorkflowEngine = class {
|
|
|
5498
5857
|
durationMs: Date.now() - step.startedAt.getTime()
|
|
5499
5858
|
});
|
|
5500
5859
|
}
|
|
5860
|
+
/** Admission persists the wait atomically; never overwrite a cancellation after it returns. */
|
|
5861
|
+
async singletonWaitResult(run) {
|
|
5862
|
+
const current = await this.store.getRun(run.id);
|
|
5863
|
+
await this.store.releaseRunLock(run.id);
|
|
5864
|
+
if (!current) throw new Error(`run ${run.id} was deleted during singleton admission`);
|
|
5865
|
+
if (current.status === "suspended") {
|
|
5866
|
+
this.emit({
|
|
5867
|
+
type: "run.suspended",
|
|
5868
|
+
runId: current.id,
|
|
5869
|
+
workflow: current.workflow,
|
|
5870
|
+
namespace: current.namespace
|
|
5871
|
+
});
|
|
5872
|
+
}
|
|
5873
|
+
return {
|
|
5874
|
+
runId: current.id,
|
|
5875
|
+
status: current.status,
|
|
5876
|
+
output: current.output,
|
|
5877
|
+
error: current.error
|
|
5878
|
+
};
|
|
5879
|
+
}
|
|
5501
5880
|
async execute(run, fn) {
|
|
5502
5881
|
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
|
|
5503
5882
|
const lockNow = this.clock();
|
|
@@ -5667,23 +6046,7 @@ var WorkflowEngine = class {
|
|
|
5667
6046
|
});
|
|
5668
6047
|
}
|
|
5669
6048
|
if (registered.singleton && !await this.singletons.admit(run, registered.singleton)) {
|
|
5670
|
-
|
|
5671
|
-
await this.store.updateRun(run.id, {
|
|
5672
|
-
status: "suspended",
|
|
5673
|
-
wakeAt,
|
|
5674
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
5675
|
-
});
|
|
5676
|
-
this.emit({
|
|
5677
|
-
type: "run.suspended",
|
|
5678
|
-
runId: run.id,
|
|
5679
|
-
workflow: run.workflow,
|
|
5680
|
-
namespace: run.namespace
|
|
5681
|
-
});
|
|
5682
|
-
await this.store.releaseRunLock(run.id);
|
|
5683
|
-
return {
|
|
5684
|
-
runId: run.id,
|
|
5685
|
-
status: "suspended"
|
|
5686
|
-
};
|
|
6049
|
+
return this.singletonWaitResult(run);
|
|
5687
6050
|
}
|
|
5688
6051
|
const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
|
|
5689
6052
|
if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
|
|
@@ -6297,28 +6660,13 @@ var WorkflowEngine = class {
|
|
|
6297
6660
|
});
|
|
6298
6661
|
}
|
|
6299
6662
|
if (run.status !== "cancelling" && registered?.singleton && !await this.singletons.admit(run, registered.singleton)) {
|
|
6300
|
-
|
|
6301
|
-
await this.store.updateRun(run.id, {
|
|
6302
|
-
status: "suspended",
|
|
6303
|
-
wakeAt,
|
|
6304
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
6305
|
-
});
|
|
6306
|
-
this.emit({
|
|
6307
|
-
type: "run.suspended",
|
|
6308
|
-
runId: run.id,
|
|
6309
|
-
workflow: run.workflow,
|
|
6310
|
-
namespace: run.namespace
|
|
6311
|
-
});
|
|
6312
|
-
await this.store.releaseRunLock(run.id);
|
|
6313
|
-
return {
|
|
6314
|
-
runId: run.id,
|
|
6315
|
-
status: "suspended"
|
|
6316
|
-
};
|
|
6663
|
+
return this.singletonWaitResult(run);
|
|
6317
6664
|
}
|
|
6318
6665
|
const compensations = [];
|
|
6319
6666
|
const snapshot = await this.store.listCheckpoints(run.id);
|
|
6320
6667
|
const replay = /* @__PURE__ */ new Map();
|
|
6321
6668
|
for (const cp of snapshot) replay.set(cp.seq, cp);
|
|
6669
|
+
let suspendedOnFan;
|
|
6322
6670
|
const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow, registered?.searchAttributesSchema);
|
|
6323
6671
|
try {
|
|
6324
6672
|
const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
|
|
@@ -6370,10 +6718,12 @@ var WorkflowEngine = class {
|
|
|
6370
6718
|
if (this.cancelRequested.has(run.id)) {
|
|
6371
6719
|
return await this.compensateAndCancel(run, compensations, replay);
|
|
6372
6720
|
}
|
|
6373
|
-
|
|
6721
|
+
const suspended = await this.settleRun(run, {
|
|
6374
6722
|
kind: "suspended",
|
|
6375
6723
|
wakeAt: err.wakeAt
|
|
6376
6724
|
});
|
|
6725
|
+
suspendedOnFan = err.fan;
|
|
6726
|
+
return suspended;
|
|
6377
6727
|
}
|
|
6378
6728
|
if (this.cancelRequested.has(run.id)) {
|
|
6379
6729
|
return await this.compensateAndCancel(run, compensations, replay);
|
|
@@ -6393,6 +6743,9 @@ var WorkflowEngine = class {
|
|
|
6393
6743
|
});
|
|
6394
6744
|
} finally {
|
|
6395
6745
|
await this.store.releaseRunLock(run.id);
|
|
6746
|
+
if (suspendedOnFan) {
|
|
6747
|
+
void this.redriveSettledChildren(run.id, suspendedOnFan).catch(() => void 0);
|
|
6748
|
+
}
|
|
6396
6749
|
}
|
|
6397
6750
|
}
|
|
6398
6751
|
/**
|
|
@@ -7207,6 +7560,21 @@ var RemoteStepError = class extends Error {
|
|
|
7207
7560
|
this.stepError = stepError;
|
|
7208
7561
|
}
|
|
7209
7562
|
};
|
|
7563
|
+
var CHILD_EDGE_PREFIXES = [
|
|
7564
|
+
"signal:child:",
|
|
7565
|
+
FOLDED_PREFIX,
|
|
7566
|
+
"spawn:"
|
|
7567
|
+
];
|
|
7568
|
+
function childIdsOf(checkpoints) {
|
|
7569
|
+
const ids = [];
|
|
7570
|
+
for (const cp of checkpoints) {
|
|
7571
|
+
if (cp.name.startsWith("signal:child:")) ids.push(cp.name.slice("signal:child:".length));
|
|
7572
|
+
if (cp.name.startsWith(FOLDED_PREFIX)) ids.push(...foldedChildIds(cp));
|
|
7573
|
+
if (cp.name.startsWith("spawn:") && typeof cp.output === "string") ids.push(cp.output);
|
|
7574
|
+
}
|
|
7575
|
+
return ids;
|
|
7576
|
+
}
|
|
7577
|
+
__name(childIdsOf, "childIdsOf");
|
|
7210
7578
|
|
|
7211
7579
|
// src/run-facets.ts
|
|
7212
7580
|
function facetOrigin(origin) {
|
|
@@ -7332,6 +7700,8 @@ var CodecStateStore = class {
|
|
|
7332
7700
|
}
|
|
7333
7701
|
inner;
|
|
7334
7702
|
codec;
|
|
7703
|
+
/** Atomic claims touch only searchable metadata and bypass payload encoding. */
|
|
7704
|
+
tryAdmitSingleton;
|
|
7335
7705
|
/**
|
|
7336
7706
|
* Forwarded verbatim — a facet cell counts `status`/`origin`, neither of which the codec touches
|
|
7337
7707
|
* (it encodes payloads: input/output/error). Bound as a PROPERTY, and only when the inner store
|
|
@@ -7354,6 +7724,14 @@ var CodecStateStore = class {
|
|
|
7354
7724
|
/** Forwarded verbatim — a count reads no payload. Bound only when the inner store counts, so the
|
|
7355
7725
|
* engine's facet/listing fallback still applies through the wrapper. */
|
|
7356
7726
|
countRuns;
|
|
7727
|
+
/** The placeholder checkpoints are encoded like any other checkpoint. */
|
|
7728
|
+
registerChildWaits;
|
|
7729
|
+
/** The folded checkpoint carries the children's outputs, so it is encoded. */
|
|
7730
|
+
compactCheckpoints;
|
|
7731
|
+
/** Forwarded verbatim — a waiter carries no payload. */
|
|
7732
|
+
listSignalWaitersByRun;
|
|
7733
|
+
/** Forwarded verbatim — a waiter carries no payload. */
|
|
7734
|
+
hasSignalWaiter;
|
|
7357
7735
|
/** Schedules persist their run `input` inside `spec` — encoded like any other payload. Bound only
|
|
7358
7736
|
* when the inner store persists schedules, so `engine.schedules` reports absence faithfully. */
|
|
7359
7737
|
saveSchedule;
|
|
@@ -7364,12 +7742,24 @@ var CodecStateStore = class {
|
|
|
7364
7742
|
constructor(inner, codec) {
|
|
7365
7743
|
this.inner = inner;
|
|
7366
7744
|
this.codec = codec;
|
|
7745
|
+
if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
|
|
7367
7746
|
const deleteRuns = inner.deleteRuns;
|
|
7368
7747
|
if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
|
|
7369
7748
|
const prune = inner.pruneTerminalRuns;
|
|
7370
7749
|
if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
|
|
7371
7750
|
const count = inner.countRuns;
|
|
7372
7751
|
if (count) this.countRuns = (query) => count.call(inner, query);
|
|
7752
|
+
const registerChildWaits = inner.registerChildWaits;
|
|
7753
|
+
if (registerChildWaits) this.registerChildWaits = (entries) => registerChildWaits.call(inner, entries.map((e) => ({
|
|
7754
|
+
waiter: e.waiter,
|
|
7755
|
+
checkpoint: this.encCp(e.checkpoint)
|
|
7756
|
+
})));
|
|
7757
|
+
const compact = inner.compactCheckpoints;
|
|
7758
|
+
if (compact) this.compactCheckpoints = (runId, fromSeq, toSeq, into) => compact.call(inner, runId, fromSeq, toSeq, this.encCp(into));
|
|
7759
|
+
const byRun = inner.listSignalWaitersByRun;
|
|
7760
|
+
if (byRun) this.listSignalWaitersByRun = (runIds) => byRun.call(inner, runIds);
|
|
7761
|
+
const hasWaiter = inner.hasSignalWaiter;
|
|
7762
|
+
if (hasWaiter) this.hasSignalWaiter = (runId, tokenPrefix) => hasWaiter.call(inner, runId, tokenPrefix);
|
|
7373
7763
|
const { saveSchedule, getSchedule, updateSchedule, deleteSchedule, listSchedules } = inner;
|
|
7374
7764
|
if (saveSchedule && getSchedule && updateSchedule && deleteSchedule && listSchedules) {
|
|
7375
7765
|
this.saveSchedule = (r) => saveSchedule.call(inner, this.encSchedule(r));
|
|
@@ -7816,6 +8206,28 @@ var InMemoryStateStore = class {
|
|
|
7816
8206
|
byRun.set(runId, row);
|
|
7817
8207
|
}
|
|
7818
8208
|
}
|
|
8209
|
+
async tryAdmitSingleton(runId, tag, workflow, limit, retryWakeAt) {
|
|
8210
|
+
const runs = [
|
|
8211
|
+
...this.runs.values()
|
|
8212
|
+
];
|
|
8213
|
+
const tags = singletonAdmissionTags(runs, runId, tag, workflow, limit);
|
|
8214
|
+
if (!tags) {
|
|
8215
|
+
const patch = singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt);
|
|
8216
|
+
const candidate = this.runs.get(runId);
|
|
8217
|
+
if (patch && candidate) this.runs.set(runId, {
|
|
8218
|
+
...candidate,
|
|
8219
|
+
...patch
|
|
8220
|
+
});
|
|
8221
|
+
return false;
|
|
8222
|
+
}
|
|
8223
|
+
const run = this.runs.get(runId);
|
|
8224
|
+
if (!run) return false;
|
|
8225
|
+
this.runs.set(runId, {
|
|
8226
|
+
...run,
|
|
8227
|
+
tags
|
|
8228
|
+
});
|
|
8229
|
+
return true;
|
|
8230
|
+
}
|
|
7819
8231
|
async createRun(run) {
|
|
7820
8232
|
this.runs.set(run.id, {
|
|
7821
8233
|
...run,
|
|
@@ -7823,7 +8235,11 @@ var InMemoryStateStore = class {
|
|
|
7823
8235
|
});
|
|
7824
8236
|
this.reindexAttributes(run.id, run.searchAttributes);
|
|
7825
8237
|
}
|
|
7826
|
-
async updateRun(runId,
|
|
8238
|
+
async updateRun(runId, inputPatch) {
|
|
8239
|
+
const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
|
|
8240
|
+
...inputPatch,
|
|
8241
|
+
tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
8242
|
+
} : inputPatch;
|
|
7827
8243
|
const existing = this.runs.get(runId);
|
|
7828
8244
|
if (!existing) throw new Error(`run ${runId} not found`);
|
|
7829
8245
|
const next = {
|
|
@@ -7954,6 +8370,33 @@ var InMemoryStateStore = class {
|
|
|
7954
8370
|
...w
|
|
7955
8371
|
}));
|
|
7956
8372
|
}
|
|
8373
|
+
async registerChildWaits(entries) {
|
|
8374
|
+
for (const { waiter } of entries) this.signalWaiters.set(waiter.token, {
|
|
8375
|
+
...waiter
|
|
8376
|
+
});
|
|
8377
|
+
for (const { checkpoint } of entries) await this.saveCheckpoint(checkpoint);
|
|
8378
|
+
}
|
|
8379
|
+
async compactCheckpoints(runId, fromSeq, toSeq, into) {
|
|
8380
|
+
for (let seq = fromSeq; seq <= toSeq; seq += 1) this.checkpoints.delete(this.key(runId, seq));
|
|
8381
|
+
this.checkpoints.set(this.key(runId, into.seq), {
|
|
8382
|
+
...into,
|
|
8383
|
+
runId
|
|
8384
|
+
});
|
|
8385
|
+
}
|
|
8386
|
+
async listSignalWaitersByRun(runIds) {
|
|
8387
|
+
const ids = new Set(runIds);
|
|
8388
|
+
return [
|
|
8389
|
+
...this.signalWaiters.values()
|
|
8390
|
+
].filter((w) => ids.has(w.runId)).map((w) => ({
|
|
8391
|
+
...w
|
|
8392
|
+
}));
|
|
8393
|
+
}
|
|
8394
|
+
async hasSignalWaiter(runId, tokenPrefix) {
|
|
8395
|
+
for (const w of this.signalWaiters.values()) {
|
|
8396
|
+
if (w.runId === runId && w.token.startsWith(tokenPrefix)) return true;
|
|
8397
|
+
}
|
|
8398
|
+
return false;
|
|
8399
|
+
}
|
|
7957
8400
|
async removeSignalWaiter(waiter) {
|
|
7958
8401
|
const current = this.signalWaiters.get(waiter.token);
|
|
7959
8402
|
if (current && current.runId === waiter.runId && current.seq === waiter.seq) {
|
|
@@ -8248,6 +8691,8 @@ var InMemoryTransport = class {
|
|
|
8248
8691
|
RemoteWorkflowExecutor,
|
|
8249
8692
|
RemoteWorkflowTimeout,
|
|
8250
8693
|
RunGateway,
|
|
8694
|
+
SINGLETON_ACTIVE_STATUSES,
|
|
8695
|
+
SINGLETON_ADMITTED_TAG,
|
|
8251
8696
|
STATE_STORE,
|
|
8252
8697
|
STATE_STORE_CANONICAL,
|
|
8253
8698
|
ScheduleAlreadyExistsError,
|
|
@@ -8278,6 +8723,7 @@ var InMemoryTransport = class {
|
|
|
8278
8723
|
capableWorkers,
|
|
8279
8724
|
classifyWaiterToken,
|
|
8280
8725
|
clearDurableExecutionWrappers,
|
|
8726
|
+
clearTerminalSingletonAdmission,
|
|
8281
8727
|
collectMetrics,
|
|
8282
8728
|
concurrencyTag,
|
|
8283
8729
|
controlPlaneDescriptor,
|
|
@@ -8306,6 +8752,7 @@ var InMemoryTransport = class {
|
|
|
8306
8752
|
isLegacyDescriptor,
|
|
8307
8753
|
isNonEmptyRunScope,
|
|
8308
8754
|
isWorkflowControlFlowSignal,
|
|
8755
|
+
listSignalWaitersOfRuns,
|
|
8309
8756
|
log,
|
|
8310
8757
|
matchesAttributes,
|
|
8311
8758
|
mergeRunFacetRows,
|
|
@@ -8316,6 +8763,7 @@ var InMemoryTransport = class {
|
|
|
8316
8763
|
normalizeAttributeRows,
|
|
8317
8764
|
normalizeDescriptor,
|
|
8318
8765
|
observedAnnouncements,
|
|
8766
|
+
parentRunIdOf,
|
|
8319
8767
|
parseDuration,
|
|
8320
8768
|
partitionOfToken,
|
|
8321
8769
|
planDispatch,
|
|
@@ -8335,6 +8783,8 @@ var InMemoryTransport = class {
|
|
|
8335
8783
|
runValueFacetsFromRuns,
|
|
8336
8784
|
sanitizeQueueToken,
|
|
8337
8785
|
scheduledRunId,
|
|
8786
|
+
singletonAdmissionDenialPatch,
|
|
8787
|
+
singletonAdmissionTags,
|
|
8338
8788
|
sqlComparator,
|
|
8339
8789
|
stepConfigOf,
|
|
8340
8790
|
stepId,
|