@dudousxd/nestjs-durable-core 0.84.1 → 0.85.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1029 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +268 -7
- package/dist/index.d.ts +268 -7
- package/dist/index.js +1026 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -47,6 +47,7 @@ __export(index_exports, {
|
|
|
47
47
|
GatherError: () => GatherError,
|
|
48
48
|
HANDOFF_SEQ_BASE: () => HANDOFF_SEQ_BASE,
|
|
49
49
|
InMemoryAdmissionBackend: () => InMemoryAdmissionBackend,
|
|
50
|
+
InMemoryRunQueue: () => InMemoryRunQueue,
|
|
50
51
|
InMemoryStateStore: () => InMemoryStateStore,
|
|
51
52
|
InMemoryTransport: () => InMemoryTransport,
|
|
52
53
|
LEGACY_V1_CAPABILITIES: () => LEGACY_V1_CAPABILITIES,
|
|
@@ -131,6 +132,7 @@ __export(index_exports, {
|
|
|
131
132
|
fanPageEtag: () => fanPageEtag,
|
|
132
133
|
findStalledRuns: () => findStalledRuns,
|
|
133
134
|
foldedCount: () => foldedCount,
|
|
135
|
+
getRunsByIds: () => getRunsByIds,
|
|
134
136
|
handoffHandlerId: () => handoffHandlerId,
|
|
135
137
|
handoffName: () => handoffName,
|
|
136
138
|
hasDurableExecutionWrappers: () => hasDurableExecutionWrappers,
|
|
@@ -168,6 +170,7 @@ __export(index_exports, {
|
|
|
168
170
|
poolNamespace: () => poolNamespace,
|
|
169
171
|
prevCronFireMs: () => prevCronFireMs,
|
|
170
172
|
queryCheckpointsOf: () => queryCheckpointsOf,
|
|
173
|
+
readRunsByIds: () => readRunsByIds,
|
|
171
174
|
readSearchAttributes: () => readSearchAttributes,
|
|
172
175
|
requiredCapabilities: () => requiredCapabilities,
|
|
173
176
|
resolveConcurrency: () => resolveConcurrency,
|
|
@@ -780,6 +783,82 @@ function backoffDelay(attempt, options) {
|
|
|
780
783
|
}
|
|
781
784
|
__name(backoffDelay, "backoffDelay");
|
|
782
785
|
|
|
786
|
+
// src/bounded-runner.ts
|
|
787
|
+
var BoundedRunner = class {
|
|
788
|
+
static {
|
|
789
|
+
__name(this, "BoundedRunner");
|
|
790
|
+
}
|
|
791
|
+
limit;
|
|
792
|
+
active = 0;
|
|
793
|
+
waiting = /* @__PURE__ */ new Map();
|
|
794
|
+
seq = 0;
|
|
795
|
+
constructor(limit) {
|
|
796
|
+
this.limit = limit;
|
|
797
|
+
}
|
|
798
|
+
/** Tasks running now plus tasks waiting for a slot. */
|
|
799
|
+
get size() {
|
|
800
|
+
return this.active + this.waiting.size;
|
|
801
|
+
}
|
|
802
|
+
/** Tasks running now. */
|
|
803
|
+
get running() {
|
|
804
|
+
return this.active;
|
|
805
|
+
}
|
|
806
|
+
submit(key, task) {
|
|
807
|
+
if (this.active < this.limit) {
|
|
808
|
+
this.active += 1;
|
|
809
|
+
queueMicrotask(() => this.execute(task));
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (!this.waiting.has(key)) this.waiting.set(key, {
|
|
813
|
+
task
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Run `task` under the bound and resolve once it settled, or once it was dropped by
|
|
818
|
+
* {@link clear} before it got a slot. Never deduplicated.
|
|
819
|
+
*/
|
|
820
|
+
run(task) {
|
|
821
|
+
return new Promise((resolve) => {
|
|
822
|
+
const done = /* @__PURE__ */ __name(() => resolve(), "done");
|
|
823
|
+
const wrapped = /* @__PURE__ */ __name(() => task().then(done, done), "wrapped");
|
|
824
|
+
if (this.active < this.limit) {
|
|
825
|
+
this.active += 1;
|
|
826
|
+
queueMicrotask(() => this.execute(wrapped));
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
this.seq += 1;
|
|
830
|
+
this.waiting.set(`\0run:${this.seq}`, {
|
|
831
|
+
task: wrapped,
|
|
832
|
+
dropped: done
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
/** Forget every task still waiting for a slot. */
|
|
837
|
+
clear() {
|
|
838
|
+
for (const { dropped } of this.waiting.values()) dropped?.();
|
|
839
|
+
this.waiting.clear();
|
|
840
|
+
}
|
|
841
|
+
execute(task) {
|
|
842
|
+
let settled;
|
|
843
|
+
try {
|
|
844
|
+
settled = task();
|
|
845
|
+
} catch {
|
|
846
|
+
settled = Promise.resolve();
|
|
847
|
+
}
|
|
848
|
+
void settled.catch(() => void 0).then(() => {
|
|
849
|
+
const next = this.waiting.entries().next();
|
|
850
|
+
if (next.done) {
|
|
851
|
+
this.active -= 1;
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const [key, waiting] = next.value;
|
|
855
|
+
this.waiting.delete(key);
|
|
856
|
+
this.execute(waiting.task);
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
var LOCAL_RUN_CONCURRENCY = 256;
|
|
861
|
+
|
|
783
862
|
// src/execution-hooks.ts
|
|
784
863
|
var wrappers = [];
|
|
785
864
|
function useDurableExecution(wrapper) {
|
|
@@ -2340,6 +2419,61 @@ var RemoteWorkflowExecutor = class {
|
|
|
2340
2419
|
}
|
|
2341
2420
|
};
|
|
2342
2421
|
|
|
2422
|
+
// src/run-dispatch-batcher.ts
|
|
2423
|
+
var RUN_DISPATCH_BATCH = 500;
|
|
2424
|
+
var RUN_DISPATCH_WINDOW_MS = 5;
|
|
2425
|
+
var RunDispatchBatcher = class {
|
|
2426
|
+
static {
|
|
2427
|
+
__name(this, "RunDispatchBatcher");
|
|
2428
|
+
}
|
|
2429
|
+
send;
|
|
2430
|
+
onError;
|
|
2431
|
+
buffer = /* @__PURE__ */ new Map();
|
|
2432
|
+
timer;
|
|
2433
|
+
sending = /* @__PURE__ */ new Set();
|
|
2434
|
+
constructor(send, onError) {
|
|
2435
|
+
this.send = send;
|
|
2436
|
+
this.onError = onError;
|
|
2437
|
+
}
|
|
2438
|
+
/** Nothing buffered and nothing being sent. */
|
|
2439
|
+
get idle() {
|
|
2440
|
+
return this.buffer.size === 0 && this.sending.size === 0;
|
|
2441
|
+
}
|
|
2442
|
+
/** Buffer `run`. A wake for a run already buffered replaces its start: a wake also runs a `pending` run. */
|
|
2443
|
+
add(run) {
|
|
2444
|
+
const prior = this.buffer.get(run.runId);
|
|
2445
|
+
if (prior && run.reason !== "wake") return;
|
|
2446
|
+
this.buffer.set(run.runId, run);
|
|
2447
|
+
if (this.buffer.size >= RUN_DISPATCH_BATCH) {
|
|
2448
|
+
this.flush();
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
if (!this.timer) {
|
|
2452
|
+
this.timer = setTimeout(() => this.flush(), RUN_DISPATCH_WINDOW_MS);
|
|
2453
|
+
this.timer.unref?.();
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
/** Send what is buffered now. */
|
|
2457
|
+
flush() {
|
|
2458
|
+
if (this.timer) clearTimeout(this.timer);
|
|
2459
|
+
this.timer = void 0;
|
|
2460
|
+
if (this.buffer.size === 0) return;
|
|
2461
|
+
const runs = [
|
|
2462
|
+
...this.buffer.values()
|
|
2463
|
+
];
|
|
2464
|
+
this.buffer = /* @__PURE__ */ new Map();
|
|
2465
|
+
const sent = this.send(runs).catch((err) => this.onError(runs, err)).finally(() => this.sending.delete(sent));
|
|
2466
|
+
this.sending.add(sent);
|
|
2467
|
+
}
|
|
2468
|
+
/** Send what is buffered and wait for every send in flight. */
|
|
2469
|
+
async settle() {
|
|
2470
|
+
this.flush();
|
|
2471
|
+
await Promise.allSettled([
|
|
2472
|
+
...this.sending
|
|
2473
|
+
]);
|
|
2474
|
+
}
|
|
2475
|
+
};
|
|
2476
|
+
|
|
2343
2477
|
// src/run-waiting.ts
|
|
2344
2478
|
var WEBHOOK_PREFIX = "wh:";
|
|
2345
2479
|
var CHILD_PREFIX = "child:";
|
|
@@ -2410,6 +2544,51 @@ function indexWaitersByRun(waiters) {
|
|
|
2410
2544
|
return byRun;
|
|
2411
2545
|
}
|
|
2412
2546
|
__name(indexWaitersByRun, "indexWaitersByRun");
|
|
2547
|
+
var GET_RUN_FALLBACK_PARALLEL = 50;
|
|
2548
|
+
async function getRunsByIds(store, runIds) {
|
|
2549
|
+
const { runs, unread, error: error2 } = await readRunsByIds(store, runIds);
|
|
2550
|
+
if (unread.length > 0) throw error2;
|
|
2551
|
+
return runs;
|
|
2552
|
+
}
|
|
2553
|
+
__name(getRunsByIds, "getRunsByIds");
|
|
2554
|
+
async function readRunsByIds(store, runIds) {
|
|
2555
|
+
const unique = [
|
|
2556
|
+
...new Set(runIds)
|
|
2557
|
+
];
|
|
2558
|
+
const runs = /* @__PURE__ */ new Map();
|
|
2559
|
+
const unread = [];
|
|
2560
|
+
let error2;
|
|
2561
|
+
const batch = store.getRuns ? RUN_ID_BATCH : GET_RUN_FALLBACK_PARALLEL;
|
|
2562
|
+
for (let at = 0; at < unique.length; at += batch) {
|
|
2563
|
+
const ids = unique.slice(at, at + batch);
|
|
2564
|
+
if (store.getRuns) {
|
|
2565
|
+
try {
|
|
2566
|
+
for (const run of await store.getRuns(ids)) runs.set(run.id, run);
|
|
2567
|
+
} catch (err) {
|
|
2568
|
+
error2 ??= err;
|
|
2569
|
+
unread.push(...ids);
|
|
2570
|
+
}
|
|
2571
|
+
continue;
|
|
2572
|
+
}
|
|
2573
|
+
const reads = await Promise.allSettled(ids.map((id) => store.getRun(id)));
|
|
2574
|
+
reads.forEach((read, k) => {
|
|
2575
|
+
if (read.status === "fulfilled") {
|
|
2576
|
+
if (read.value) runs.set(read.value.id, read.value);
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
error2 ??= read.reason;
|
|
2580
|
+
unread.push(ids[k]);
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
return {
|
|
2584
|
+
runs,
|
|
2585
|
+
unread,
|
|
2586
|
+
...unread.length > 0 ? {
|
|
2587
|
+
error: error2
|
|
2588
|
+
} : {}
|
|
2589
|
+
};
|
|
2590
|
+
}
|
|
2591
|
+
__name(readRunsByIds, "readRunsByIds");
|
|
2413
2592
|
|
|
2414
2593
|
// src/scheduler.ts
|
|
2415
2594
|
var import_node_module = require("module");
|
|
@@ -2451,7 +2630,31 @@ __name(resolveCronParse, "resolveCronParse");
|
|
|
2451
2630
|
|
|
2452
2631
|
// src/scheduler.ts
|
|
2453
2632
|
var nodeRequire = (0, import_node_module.createRequire)(__importMetaUrl);
|
|
2454
|
-
var
|
|
2633
|
+
var jitterFireAt = /* @__PURE__ */ new WeakMap();
|
|
2634
|
+
function deferredWindows(engine, key) {
|
|
2635
|
+
return [
|
|
2636
|
+
...jitterFireAt.get(engine)?.get(key)?.keys() ?? []
|
|
2637
|
+
];
|
|
2638
|
+
}
|
|
2639
|
+
__name(deferredWindows, "deferredWindows");
|
|
2640
|
+
function jitterDue(engine, s, candidates, nowMs, random) {
|
|
2641
|
+
let byKey = jitterFireAt.get(engine);
|
|
2642
|
+
if (byKey === void 0) {
|
|
2643
|
+
byKey = /* @__PURE__ */ new Map();
|
|
2644
|
+
jitterFireAt.set(engine, byKey);
|
|
2645
|
+
}
|
|
2646
|
+
const previous = byKey.get(s.key);
|
|
2647
|
+
const current = /* @__PURE__ */ new Map();
|
|
2648
|
+
const due = [];
|
|
2649
|
+
for (const runId of candidates) {
|
|
2650
|
+
const at = previous?.get(runId) ?? nowMs + Math.floor(random() * s.jitter);
|
|
2651
|
+
if (nowMs >= at) due.push(runId);
|
|
2652
|
+
else current.set(runId, at);
|
|
2653
|
+
}
|
|
2654
|
+
byKey.set(s.key, current);
|
|
2655
|
+
return due;
|
|
2656
|
+
}
|
|
2657
|
+
__name(jitterDue, "jitterDue");
|
|
2455
2658
|
function scheduledRunId(key, everyMs, nowMs) {
|
|
2456
2659
|
return `sched:${key}:${Math.floor(nowMs / everyMs)}`;
|
|
2457
2660
|
}
|
|
@@ -2521,13 +2724,19 @@ function scheduleRunIdsAt(s, nowMs) {
|
|
|
2521
2724
|
__name(scheduleRunIdsAt, "scheduleRunIdsAt");
|
|
2522
2725
|
async function runSchedules(engine, schedules, nowMs, opts) {
|
|
2523
2726
|
const random = opts?.random ?? Math.random;
|
|
2524
|
-
const sleep = opts?.sleep ?? defaultSleep;
|
|
2525
2727
|
const settled = settledIdsFor(engine);
|
|
2526
2728
|
const ids = [];
|
|
2527
2729
|
for (const s of schedules) {
|
|
2528
2730
|
if (engine.isDraining?.()) break;
|
|
2529
2731
|
if (s.paused) continue;
|
|
2530
|
-
const
|
|
2732
|
+
const due = scheduleRunIdsAt(s, nowMs);
|
|
2733
|
+
const windows = s.jitter && s.jitter > 0 ? [
|
|
2734
|
+
.../* @__PURE__ */ new Set([
|
|
2735
|
+
...deferredWindows(engine, s.key),
|
|
2736
|
+
...due
|
|
2737
|
+
])
|
|
2738
|
+
] : due;
|
|
2739
|
+
const candidates = windows.filter((id) => !settled.get(s.key)?.has(id));
|
|
2531
2740
|
if (candidates.length === 0) continue;
|
|
2532
2741
|
if (s.overlap === "skip" && s.everyMs) {
|
|
2533
2742
|
const prevBucket = Math.floor(nowMs / s.everyMs) - 1;
|
|
@@ -2536,8 +2745,9 @@ async function runSchedules(engine, schedules, nowMs, opts) {
|
|
|
2536
2745
|
if (prev && (prev.status === "running" || prev.status === "suspended")) continue;
|
|
2537
2746
|
}
|
|
2538
2747
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
2748
|
+
const firing = s.jitter && s.jitter > 0 ? jitterDue(engine, s, candidates, nowMs, random) : candidates;
|
|
2749
|
+
if (firing.length === 0) continue;
|
|
2750
|
+
for (const runId of firing) {
|
|
2541
2751
|
if (engine.isDraining?.()) break;
|
|
2542
2752
|
const existing = await engine.getRun(runId);
|
|
2543
2753
|
remember(settled, s.key, runId);
|
|
@@ -3302,6 +3512,39 @@ var TransportPool = class {
|
|
|
3302
3512
|
transport.onStartRun?.(handler);
|
|
3303
3513
|
}
|
|
3304
3514
|
}
|
|
3515
|
+
/** Does any transport here carry a run queue ({@link Transport.dispatchRuns} + `onDispatchedRun`)? */
|
|
3516
|
+
get dispatchesRuns() {
|
|
3517
|
+
return this.transports.some(({ transport }) => transport.dispatchRuns && transport.onDispatchedRun);
|
|
3518
|
+
}
|
|
3519
|
+
/**
|
|
3520
|
+
* Queue `runs` on the first transport that carries a run queue, failing over to the next on an
|
|
3521
|
+
* error, like {@link dispatch}. Throws if none accepted them.
|
|
3522
|
+
*/
|
|
3523
|
+
async dispatchRuns(runs, opts) {
|
|
3524
|
+
let lastErr;
|
|
3525
|
+
for (const { transport } of this.transports) {
|
|
3526
|
+
if (!transport.dispatchRuns || !transport.onDispatchedRun) continue;
|
|
3527
|
+
try {
|
|
3528
|
+
await transport.dispatchRuns(runs, opts);
|
|
3529
|
+
return;
|
|
3530
|
+
} catch (err) {
|
|
3531
|
+
lastErr = err;
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
throw lastErr ?? new Error("no transport carries a run queue");
|
|
3535
|
+
}
|
|
3536
|
+
/**
|
|
3537
|
+
* Consume the run queue of every transport that carries one (a run may have been queued on any of
|
|
3538
|
+
* them after a failover). Returns one stop function per consumer.
|
|
3539
|
+
*/
|
|
3540
|
+
onDispatchedRun(handler, opts) {
|
|
3541
|
+
const stops = [];
|
|
3542
|
+
for (const { transport } of this.transports) {
|
|
3543
|
+
if (!transport.dispatchRuns || !transport.onDispatchedRun) continue;
|
|
3544
|
+
stops.push(transport.onDispatchedRun(handler, opts));
|
|
3545
|
+
}
|
|
3546
|
+
return stops;
|
|
3547
|
+
}
|
|
3305
3548
|
/**
|
|
3306
3549
|
* The live handshake descriptors advertised for `group` (design §7.2), merged across every transport
|
|
3307
3550
|
* that can report them. Returns `[]` when NO transport implements
|
|
@@ -3834,21 +4077,28 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3834
4077
|
return inputs.map((_, i) => i).slice(from, from + waves.waveSize);
|
|
3835
4078
|
}, "waveItems");
|
|
3836
4079
|
if (mode === "failFast") {
|
|
3837
|
-
|
|
3838
|
-
const cp = existing[
|
|
3839
|
-
const
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
]
|
|
4080
|
+
const failure = /* @__PURE__ */ __name((i2) => {
|
|
4081
|
+
const cp = existing[i2];
|
|
4082
|
+
const c2 = cp?.status === "completed" ? cp.output : null;
|
|
4083
|
+
return c2 && c2.ok === false ? c2 : null;
|
|
4084
|
+
}, "failure");
|
|
4085
|
+
const first = /* @__PURE__ */ __name((cancelled) => inputs.findIndex((_, i2) => {
|
|
4086
|
+
const c2 = failure(i2);
|
|
4087
|
+
return c2 !== null && c2.code === "cancelled" === cancelled;
|
|
4088
|
+
}), "first");
|
|
4089
|
+
const i = first(false) !== -1 ? first(false) : first(true);
|
|
4090
|
+
const c = i === -1 ? null : failure(i);
|
|
4091
|
+
if (c) {
|
|
4092
|
+
for (let j = 0; j < inputs.length; j += 1) {
|
|
4093
|
+
if (j !== i && existing[j]?.status !== "completed") host.cancelChild?.(id(j));
|
|
3851
4094
|
}
|
|
4095
|
+
throw new GatherError([
|
|
4096
|
+
{
|
|
4097
|
+
index: i,
|
|
4098
|
+
id: id(i),
|
|
4099
|
+
error: c.error ?? "unknown"
|
|
4100
|
+
}
|
|
4101
|
+
], inputs.length);
|
|
3852
4102
|
}
|
|
3853
4103
|
}
|
|
3854
4104
|
if (store.compactCheckpoints && inputs.length >= FOLD_MIN_FAN) {
|
|
@@ -3927,10 +4177,12 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3927
4177
|
await registerChildWaits(entries);
|
|
3928
4178
|
for (const i of chunk) host.startChild(name, inputs[i], id(i));
|
|
3929
4179
|
}
|
|
3930
|
-
|
|
3931
|
-
await
|
|
3932
|
-
|
|
3933
|
-
|
|
4180
|
+
if (toEnsure.length > 0) {
|
|
4181
|
+
const { runs: started, unread } = await readRunsByIds(store, toEnsure.map((i) => id(i)));
|
|
4182
|
+
const skip = new Set(unread);
|
|
4183
|
+
for (const i of toEnsure) {
|
|
4184
|
+
if (!started.has(id(i)) && !skip.has(id(i))) host.startChild(name, inputs[i], id(i));
|
|
4185
|
+
}
|
|
3934
4186
|
}
|
|
3935
4187
|
}
|
|
3936
4188
|
if (active.length > 0) {
|
|
@@ -4390,6 +4642,56 @@ var ANNOUNCED_ROUTES_TTL_MS = 5e3;
|
|
|
4390
4642
|
var VERSION_UNDECLARED_TAG = "version:undeclared";
|
|
4391
4643
|
var versionKey = /* @__PURE__ */ __name((name, version) => `${name}@${version}`, "versionKey");
|
|
4392
4644
|
var STEP_FAN_CHUNK = 200;
|
|
4645
|
+
var PARENT_NOTIFY_ATTEMPTS = 4;
|
|
4646
|
+
var PARENT_NOTIFY_BACKOFF_MS = 250;
|
|
4647
|
+
var LEASE_RECHECK_MAX_MS = 2e3;
|
|
4648
|
+
var LEASE_RECHECK_SLACK_MS = 100;
|
|
4649
|
+
var LEASE_RECHECK_ATTEMPTS = 30;
|
|
4650
|
+
function deferred() {
|
|
4651
|
+
let resolve;
|
|
4652
|
+
let reject;
|
|
4653
|
+
const promise = new Promise((res, rej) => {
|
|
4654
|
+
resolve = res;
|
|
4655
|
+
reject = rej;
|
|
4656
|
+
});
|
|
4657
|
+
promise.catch(() => void 0);
|
|
4658
|
+
return {
|
|
4659
|
+
promise,
|
|
4660
|
+
resolve,
|
|
4661
|
+
reject
|
|
4662
|
+
};
|
|
4663
|
+
}
|
|
4664
|
+
__name(deferred, "deferred");
|
|
4665
|
+
var pendingStateKey = /* @__PURE__ */ __name((at) => String(new Date(at).getTime()), "pendingStateKey");
|
|
4666
|
+
var freshPendingKey = /* @__PURE__ */ __name(() => `${Date.now()}-${globalThis.crypto.randomUUID().slice(0, 8)}`, "freshPendingKey");
|
|
4667
|
+
var CHILD_SIGNAL_PREFIX = "signal:child:";
|
|
4668
|
+
var DELIVER_CHUNK = 50;
|
|
4669
|
+
function childCompletion(child) {
|
|
4670
|
+
if (child.status === "completed") return {
|
|
4671
|
+
ok: true,
|
|
4672
|
+
value: child.output
|
|
4673
|
+
};
|
|
4674
|
+
if (child.status === "failed") return failedCompletion(child.error ?? {
|
|
4675
|
+
message: "failed"
|
|
4676
|
+
});
|
|
4677
|
+
if (child.status === "dead") return failedCompletion(child.error ?? {
|
|
4678
|
+
message: "dead"
|
|
4679
|
+
});
|
|
4680
|
+
if (child.status === "cancelled") return cancelledCompletion(child.error);
|
|
4681
|
+
return void 0;
|
|
4682
|
+
}
|
|
4683
|
+
__name(childCompletion, "childCompletion");
|
|
4684
|
+
var CHILD_CANCELLED = "cancelled";
|
|
4685
|
+
function cancelledCompletion(error2) {
|
|
4686
|
+
const base = error2 ?? {
|
|
4687
|
+
message: "cancelled"
|
|
4688
|
+
};
|
|
4689
|
+
return failedCompletion({
|
|
4690
|
+
...base,
|
|
4691
|
+
code: base.code ?? CHILD_CANCELLED
|
|
4692
|
+
});
|
|
4693
|
+
}
|
|
4694
|
+
__name(cancelledCompletion, "cancelledCompletion");
|
|
4393
4695
|
var RESULT_BATCH_MAX = 256;
|
|
4394
4696
|
function shuffle(items) {
|
|
4395
4697
|
for (let i = items.length - 1; i > 0; i -= 1) {
|
|
@@ -4566,6 +4868,35 @@ var WorkflowEngine = class {
|
|
|
4566
4868
|
/** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
|
|
4567
4869
|
inflight = /* @__PURE__ */ new Set();
|
|
4568
4870
|
/**
|
|
4871
|
+
* Work a settled turn leaves running in the background (a parent notification, a post-release
|
|
4872
|
+
* re-drive), so a graceful shutdown waits for it too: dropping one on exit strands the run until
|
|
4873
|
+
* its reconcile timer.
|
|
4874
|
+
*/
|
|
4875
|
+
background = /* @__PURE__ */ new Set();
|
|
4876
|
+
/**
|
|
4877
|
+
* Runs with an implicit wake executing on this instance, and whether another wake arrived for the
|
|
4878
|
+
* run meanwhile (see {@link resumeImplicit}).
|
|
4879
|
+
*/
|
|
4880
|
+
wakeTurns = /* @__PURE__ */ new Map();
|
|
4881
|
+
/** Re-checks scheduled for runs whose wake found another instance's lease (see {@link execute}). */
|
|
4882
|
+
leaseRetries = /* @__PURE__ */ new Map();
|
|
4883
|
+
/**
|
|
4884
|
+
* Every run turn and child start this instance takes on itself, under one budget of
|
|
4885
|
+
* {@link LOCAL_RUN_CONCURRENCY}: the runs it dispatches to itself, a fan's deferred child starts,
|
|
4886
|
+
* enqueued pickups and the runs it takes off the transport's run queue.
|
|
4887
|
+
*/
|
|
4888
|
+
localWork;
|
|
4889
|
+
/** The dispatcher the caller supplied, if any; without one the engine picks (see {@link dispatchRun}). */
|
|
4890
|
+
customDispatcher;
|
|
4891
|
+
/** Runs on their way to the transport's run queue (see {@link consumeRuns}). */
|
|
4892
|
+
runBatcher;
|
|
4893
|
+
/** Set while this engine consumes a transport's run queue, and so dispatches its runs to it. */
|
|
4894
|
+
dispatchesOverTransport = false;
|
|
4895
|
+
/** Stops for the run-queue consumers started by {@link consumeRuns}. */
|
|
4896
|
+
runConsumerStops = [];
|
|
4897
|
+
/** The consumers being stopped, so {@link drain} waits for their runs in flight. */
|
|
4898
|
+
runConsumersStopping;
|
|
4899
|
+
/**
|
|
4569
4900
|
* The run lease of each execution in flight on THIS instance, by run id — renewed while it runs
|
|
4570
4901
|
* and checked by every settle (the zombie fence, see {@link ExecutionLease}). Also what `drain()`
|
|
4571
4902
|
* releases when its budget runs out.
|
|
@@ -4621,15 +4952,27 @@ var WorkflowEngine = class {
|
|
|
4621
4952
|
this.rehydrate = deps.rehydrate ?? ((_carrier, fn) => fn());
|
|
4622
4953
|
this.compensationRetries = Math.max(1, deps.compensationRetries ?? 1);
|
|
4623
4954
|
this.trackStepStart = deps.trackStepStart ?? true;
|
|
4624
|
-
this.
|
|
4625
|
-
|
|
4626
|
-
|
|
4955
|
+
this.localWork = new BoundedRunner(LOCAL_RUN_CONCURRENCY);
|
|
4956
|
+
this.customDispatcher = deps.runDispatcher;
|
|
4957
|
+
this.runBatcher = new RunDispatchBatcher((runs) => this.pool.dispatchRuns(runs, {
|
|
4958
|
+
namespace: this.namespace
|
|
4959
|
+
}), (runs, err) => {
|
|
4960
|
+
for (const run of runs) {
|
|
4961
|
+
if (run.reason === "wake" && !this.draining) {
|
|
4962
|
+
this.inBackground(this.resumeImplicit(run.runId));
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
console.error(`[nestjs-durable] could not queue ${runs.length} run(s) on the transport; starts stay pending for the next poll, wakes resume here:`, err);
|
|
4966
|
+
});
|
|
4967
|
+
this.runDispatcher = {
|
|
4968
|
+
dispatch: /* @__PURE__ */ __name((runId) => this.dispatchRun(runId), "dispatch")
|
|
4627
4969
|
};
|
|
4628
4970
|
this.singletons = new SingletonGate({
|
|
4629
4971
|
store: this.store,
|
|
4630
4972
|
clock: this.clock,
|
|
4631
4973
|
dispatch: /* @__PURE__ */ __name((runId) => {
|
|
4632
|
-
this.
|
|
4974
|
+
if (this.queuesRunsOverTransport) void this.wake(runId);
|
|
4975
|
+
else this.runDispatcher.dispatch(runId);
|
|
4633
4976
|
}, "dispatch"),
|
|
4634
4977
|
configFor: /* @__PURE__ */ __name((run) => (this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? this.latest.get(run.workflow))?.singleton, "configFor")
|
|
4635
4978
|
});
|
|
@@ -4754,13 +5097,224 @@ var WorkflowEngine = class {
|
|
|
4754
5097
|
}
|
|
4755
5098
|
/**
|
|
4756
5099
|
* Be notified when a run is enqueued on ANOTHER instance (via the control plane), so a worker can
|
|
4757
|
-
* pick it up at once — e.g. `engine.onEnqueued((runId) => engine.
|
|
5100
|
+
* pick it up at once — e.g. `engine.onEnqueued((runId) => engine.runEnqueued(runId))`. Returns an
|
|
4758
5101
|
* unsubscribe function. Only wire this on instances that should execute runs (workers).
|
|
4759
5102
|
*/
|
|
4760
5103
|
onEnqueued(listener) {
|
|
4761
5104
|
this.enqueuedListeners.add(listener);
|
|
4762
5105
|
return () => this.enqueuedListeners.delete(listener);
|
|
4763
5106
|
}
|
|
5107
|
+
/**
|
|
5108
|
+
* Pick up a run another instance announced as enqueued: run it here if it is still `pending`.
|
|
5109
|
+
* Bounded by {@link LOCAL_RUN_CONCURRENCY}, and a run another instance already took is skipped
|
|
5110
|
+
* after one read, instead of being leased and replayed here once more.
|
|
5111
|
+
*/
|
|
5112
|
+
runEnqueued(runId) {
|
|
5113
|
+
this.localWork.submit(`run:${runId}`, () => this.runIfPending(runId));
|
|
5114
|
+
}
|
|
5115
|
+
/**
|
|
5116
|
+
* Consume the run queue of every transport that carries one ({@link Transport.dispatchRuns} and
|
|
5117
|
+
* {@link Transport.onDispatchedRun}), and from then on dispatch the runs this engine starts there
|
|
5118
|
+
* rather than executing them in-process: a run, a `ctx.all` fan's children included, executes on
|
|
5119
|
+
* whichever consuming instance takes it, at most {@link LOCAL_RUN_CONCURRENCY} at once per
|
|
5120
|
+
* instance. Call it on every instance that drives runs; the Nest module does on a driving operator.
|
|
5121
|
+
*
|
|
5122
|
+
* Returns a function that stops consuming and resolves once the runs in flight settled, after
|
|
5123
|
+
* which runs are executed in-process again; {@link beginDrain} stops consuming too. Undefined
|
|
5124
|
+
* when no transport carries a run queue: runs keep executing in-process.
|
|
5125
|
+
*/
|
|
5126
|
+
consumeRuns() {
|
|
5127
|
+
if (!this.pool.dispatchesRuns) return void 0;
|
|
5128
|
+
if (this.runConsumerStops.length === 0) {
|
|
5129
|
+
this.runConsumerStops = this.pool.onDispatchedRun((run) => this.localWork.run(() => this.runFromQueue(run)), {
|
|
5130
|
+
concurrency: LOCAL_RUN_CONCURRENCY,
|
|
5131
|
+
namespace: this.namespace
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
5134
|
+
this.dispatchesOverTransport = true;
|
|
5135
|
+
return async () => {
|
|
5136
|
+
this.dispatchesOverTransport = false;
|
|
5137
|
+
await this.runBatcher.settle();
|
|
5138
|
+
await this.stopRunConsumers();
|
|
5139
|
+
};
|
|
5140
|
+
}
|
|
5141
|
+
/** Stop every run-queue consumer; resolves once their runs in flight settled. */
|
|
5142
|
+
stopRunConsumers() {
|
|
5143
|
+
if (this.runConsumerStops.length > 0) {
|
|
5144
|
+
const stops = this.runConsumerStops;
|
|
5145
|
+
this.runConsumerStops = [];
|
|
5146
|
+
this.runConsumersStopping = Promise.allSettled(stops.map((stop) => stop()));
|
|
5147
|
+
}
|
|
5148
|
+
return this.runConsumersStopping ?? Promise.resolve();
|
|
5149
|
+
}
|
|
5150
|
+
/** Whether {@link dispatchRun} queues runs on the transport (see {@link consumeRuns}). */
|
|
5151
|
+
get queuesRunsOverTransport() {
|
|
5152
|
+
return this.customDispatcher === void 0 && this.dispatchesOverTransport;
|
|
5153
|
+
}
|
|
5154
|
+
/**
|
|
5155
|
+
* Hand a `pending` run to whatever executes it: the caller's {@link WorkflowEngineDeps.runDispatcher};
|
|
5156
|
+
* else the transport's run queue while this engine consumes it, batched; else this instance,
|
|
5157
|
+
* bounded by {@link LOCAL_RUN_CONCURRENCY}. A run whose queueing fails stays `pending` for a
|
|
5158
|
+
* `runPending` poll.
|
|
5159
|
+
*/
|
|
5160
|
+
dispatchRun(runId, priority, key) {
|
|
5161
|
+
if (this.customDispatcher) return this.customDispatcher.dispatch(runId);
|
|
5162
|
+
if (this.dispatchesOverTransport) {
|
|
5163
|
+
this.runBatcher.add({
|
|
5164
|
+
runId,
|
|
5165
|
+
reason: "start",
|
|
5166
|
+
...priority !== void 0 ? {
|
|
5167
|
+
priority
|
|
5168
|
+
} : {},
|
|
5169
|
+
...key !== void 0 ? {
|
|
5170
|
+
key
|
|
5171
|
+
} : {}
|
|
5172
|
+
});
|
|
5173
|
+
return;
|
|
5174
|
+
}
|
|
5175
|
+
this.localWork.submit(`run:${runId}`, () => this.runOne(runId));
|
|
5176
|
+
}
|
|
5177
|
+
/**
|
|
5178
|
+
* Wake a suspended run that has something new to see (a delivered signal or child completion, a
|
|
5179
|
+
* lease that came free, a freed slot): while this engine consumes the transport's run queue, on
|
|
5180
|
+
* whichever instance takes the wake (the result then only says it was handed over); otherwise
|
|
5181
|
+
* here, now.
|
|
5182
|
+
*/
|
|
5183
|
+
wake(runId) {
|
|
5184
|
+
if (this.queuesRunsOverTransport) {
|
|
5185
|
+
this.runBatcher.add({
|
|
5186
|
+
runId,
|
|
5187
|
+
reason: "wake"
|
|
5188
|
+
});
|
|
5189
|
+
return Promise.resolve({
|
|
5190
|
+
runId,
|
|
5191
|
+
status: "suspended"
|
|
5192
|
+
});
|
|
5193
|
+
}
|
|
5194
|
+
return this.resumeImplicit(runId);
|
|
5195
|
+
}
|
|
5196
|
+
/**
|
|
5197
|
+
* Take a turn of a run taken off the transport's run queue. A `start` runs only while the run is
|
|
5198
|
+
* still `pending` (or `cancelling`, a recovered cancel): any other state means another instance
|
|
5199
|
+
* already took it. A `wake` leases and resumes the run unless it is finished. The run lease admits
|
|
5200
|
+
* one executor either way. Never rejects.
|
|
5201
|
+
*/
|
|
5202
|
+
async runFromQueue(dispatch) {
|
|
5203
|
+
if (this.draining) return;
|
|
5204
|
+
const { runId } = dispatch;
|
|
5205
|
+
try {
|
|
5206
|
+
const run = await this.store.getRun(runId);
|
|
5207
|
+
if (!run) return;
|
|
5208
|
+
if (run.status === "pending" || run.status === "cancelling") {
|
|
5209
|
+
await this.runOne(runId);
|
|
5210
|
+
return;
|
|
5211
|
+
}
|
|
5212
|
+
if (dispatch.reason !== "wake" || TERMINAL_RUN_STATUSES.includes(run.status)) return;
|
|
5213
|
+
const nowMs = this.clock();
|
|
5214
|
+
if (run.wakeAt !== void 0 && run.wakeAt <= nowMs) {
|
|
5215
|
+
const waiters = indexWaitersByRun(await listSignalWaitersOfRuns(this.store, [
|
|
5216
|
+
runId
|
|
5217
|
+
]));
|
|
5218
|
+
const resumed = await this.resumeLeased([
|
|
5219
|
+
run
|
|
5220
|
+
], nowMs, (locked) => this.reconcileSignalWaiter(locked, waiters.get(locked.id)));
|
|
5221
|
+
if (resumed.length === 0) this.recheckAfterLease(runId, run.lockedUntil);
|
|
5222
|
+
return;
|
|
5223
|
+
}
|
|
5224
|
+
await this.resumeImplicit(runId).catch((err) => {
|
|
5225
|
+
if (!(err instanceof NamespaceMismatch)) throw err;
|
|
5226
|
+
});
|
|
5227
|
+
} catch (error2) {
|
|
5228
|
+
console.error(`[nestjs-durable] could not run queued run ${runId}:`, error2);
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
/**
|
|
5232
|
+
* Hand due runs (timers, blocked runs due for a fleet re-check) to whichever instance takes them
|
|
5233
|
+
* off the run queue. Each is claimed first, under its lease, by pushing its `wakeAt` one lease
|
|
5234
|
+
* period out, so the next poll (here or elsewhere) does not queue it again while it waits; a wake
|
|
5235
|
+
* that is lost brings it back due after that period. False when the transport refused the wakes,
|
|
5236
|
+
* so the caller resumes them itself.
|
|
5237
|
+
*/
|
|
5238
|
+
async handOffDue(runs, nowMs) {
|
|
5239
|
+
const claimed = [];
|
|
5240
|
+
for (const run of runs) {
|
|
5241
|
+
if (this.draining) break;
|
|
5242
|
+
if (!await this.store.tryLockRun(run.id, this.instanceId, nowMs + this.leaseMs, nowMs)) {
|
|
5243
|
+
continue;
|
|
5244
|
+
}
|
|
5245
|
+
try {
|
|
5246
|
+
const patch = {
|
|
5247
|
+
wakeAt: nowMs + this.leaseMs
|
|
5248
|
+
};
|
|
5249
|
+
if (this.store.updateRunIf) {
|
|
5250
|
+
if (await this.store.updateRunIf(run.id, [
|
|
5251
|
+
run.status
|
|
5252
|
+
], patch)) claimed.push(run);
|
|
5253
|
+
} else {
|
|
5254
|
+
await this.store.updateRun(run.id, patch);
|
|
5255
|
+
claimed.push(run);
|
|
5256
|
+
}
|
|
5257
|
+
} finally {
|
|
5258
|
+
await this.store.releaseRunLock(run.id);
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
try {
|
|
5262
|
+
for (let at = 0; at < claimed.length; at += RUN_DISPATCH_BATCH) {
|
|
5263
|
+
await this.pool.dispatchRuns(claimed.slice(at, at + RUN_DISPATCH_BATCH).map((run) => ({
|
|
5264
|
+
runId: run.id,
|
|
5265
|
+
reason: "wake",
|
|
5266
|
+
...run.priority !== void 0 ? {
|
|
5267
|
+
priority: run.priority
|
|
5268
|
+
} : {}
|
|
5269
|
+
})), {
|
|
5270
|
+
namespace: this.namespace
|
|
5271
|
+
});
|
|
5272
|
+
}
|
|
5273
|
+
return true;
|
|
5274
|
+
} catch (error2) {
|
|
5275
|
+
console.error("[nestjs-durable] could not queue due runs on the transport:", error2);
|
|
5276
|
+
return false;
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
/**
|
|
5280
|
+
* Queue `runs` on the transport for whichever consuming instance takes them, in batches. False
|
|
5281
|
+
* when the transport refused them, so the caller executes them itself instead.
|
|
5282
|
+
*/
|
|
5283
|
+
async queueOnTransport(runs) {
|
|
5284
|
+
try {
|
|
5285
|
+
for (let at = 0; at < runs.length; at += RUN_DISPATCH_BATCH) {
|
|
5286
|
+
await this.pool.dispatchRuns(runs.slice(at, at + RUN_DISPATCH_BATCH).map((run) => ({
|
|
5287
|
+
runId: run.id,
|
|
5288
|
+
reason: "start",
|
|
5289
|
+
key: pendingStateKey(run.updatedAt),
|
|
5290
|
+
...run.priority !== void 0 ? {
|
|
5291
|
+
priority: run.priority
|
|
5292
|
+
} : {}
|
|
5293
|
+
})), {
|
|
5294
|
+
namespace: this.namespace
|
|
5295
|
+
});
|
|
5296
|
+
}
|
|
5297
|
+
return true;
|
|
5298
|
+
} catch (error2) {
|
|
5299
|
+
console.error("[nestjs-durable] could not queue runs on the transport; running them here:", error2);
|
|
5300
|
+
return false;
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
/**
|
|
5304
|
+
* Execute a run announced by another instance, if it is still `pending`: a
|
|
5305
|
+
* run another instance already took is skipped after one read instead of being leased and
|
|
5306
|
+
* replayed here once more. Never rejects.
|
|
5307
|
+
*/
|
|
5308
|
+
async runIfPending(runId) {
|
|
5309
|
+
if (this.draining) return;
|
|
5310
|
+
try {
|
|
5311
|
+
const run = await this.store.getRun(runId);
|
|
5312
|
+
if (run?.status !== "pending") return;
|
|
5313
|
+
await this.runOne(runId);
|
|
5314
|
+
} catch (error2) {
|
|
5315
|
+
console.error(`[nestjs-durable] could not run enqueued run ${runId}:`, error2);
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
4764
5318
|
/** Fire cooperative-cancellation listeners for `runId` (a worker bridge aborts in-flight work). */
|
|
4765
5319
|
notifyCancelled(runId) {
|
|
4766
5320
|
for (const fn of this.cancelListeners) {
|
|
@@ -5156,8 +5710,8 @@ var WorkflowEngine = class {
|
|
|
5156
5710
|
status: "suspended"
|
|
5157
5711
|
};
|
|
5158
5712
|
}
|
|
5159
|
-
await this.
|
|
5160
|
-
if (this.controlPlane) {
|
|
5713
|
+
await this.dispatchRun(runId, run.priority, pendingStateKey(run.updatedAt));
|
|
5714
|
+
if (this.controlPlane && !this.queuesRunsOverTransport) {
|
|
5161
5715
|
void this.controlPlane.publishControl({
|
|
5162
5716
|
kind: "enqueued",
|
|
5163
5717
|
runId,
|
|
@@ -5196,7 +5750,43 @@ var WorkflowEngine = class {
|
|
|
5196
5750
|
* stale waiter or a late result left behind by a failed run silently re-executes its body.
|
|
5197
5751
|
*/
|
|
5198
5752
|
resumeImplicit(runId) {
|
|
5199
|
-
|
|
5753
|
+
const turn = this.wakeTurns.get(runId);
|
|
5754
|
+
if (turn) {
|
|
5755
|
+
turn.again ??= deferred();
|
|
5756
|
+
if (currentWorkflowCtx()?.runId === runId) {
|
|
5757
|
+
return Promise.resolve({
|
|
5758
|
+
runId,
|
|
5759
|
+
status: "running"
|
|
5760
|
+
});
|
|
5761
|
+
}
|
|
5762
|
+
return turn.again.promise;
|
|
5763
|
+
}
|
|
5764
|
+
const mine = {};
|
|
5765
|
+
this.wakeTurns.set(runId, mine);
|
|
5766
|
+
const finish = /* @__PURE__ */ __name(() => {
|
|
5767
|
+
if (this.wakeTurns.get(runId) === mine) this.wakeTurns.delete(runId);
|
|
5768
|
+
const again = mine.again;
|
|
5769
|
+
if (!again) return;
|
|
5770
|
+
if (this.draining) {
|
|
5771
|
+
again.resolve({
|
|
5772
|
+
runId,
|
|
5773
|
+
status: "running"
|
|
5774
|
+
});
|
|
5775
|
+
return;
|
|
5776
|
+
}
|
|
5777
|
+
const next = this.resumeImplicit(runId);
|
|
5778
|
+
next.then(again.resolve, again.reject);
|
|
5779
|
+
this.inBackground(next);
|
|
5780
|
+
}, "finish");
|
|
5781
|
+
const result = this.resumeRun(runId, false);
|
|
5782
|
+
void result.then(finish, finish);
|
|
5783
|
+
return result;
|
|
5784
|
+
}
|
|
5785
|
+
/** Keep `work` in {@link background} until it settles; its failure is swallowed. */
|
|
5786
|
+
inBackground(work) {
|
|
5787
|
+
const tracked = work.catch(() => void 0);
|
|
5788
|
+
this.background.add(tracked);
|
|
5789
|
+
void tracked.then(() => this.background.delete(tracked));
|
|
5200
5790
|
}
|
|
5201
5791
|
/**
|
|
5202
5792
|
* One execution of a run at a time in this process. A resume that lands while the run executes
|
|
@@ -5483,6 +6073,10 @@ var WorkflowEngine = class {
|
|
|
5483
6073
|
*/
|
|
5484
6074
|
beginDrain(spentMs = 0) {
|
|
5485
6075
|
this.draining = true;
|
|
6076
|
+
if (this.runConsumerStops.length > 0) {
|
|
6077
|
+
this.runBatcher.flush();
|
|
6078
|
+
void this.stopRunConsumers();
|
|
6079
|
+
}
|
|
5486
6080
|
if (Number.isFinite(spentMs) && spentMs > 0) {
|
|
5487
6081
|
this.drainSpentMs += spentMs;
|
|
5488
6082
|
for (const rearm of this.drainRearms) rearm();
|
|
@@ -5503,7 +6097,14 @@ var WorkflowEngine = class {
|
|
|
5503
6097
|
*/
|
|
5504
6098
|
async drain(timeoutMs = 1e4) {
|
|
5505
6099
|
this.beginDrain();
|
|
5506
|
-
|
|
6100
|
+
this.localWork.clear();
|
|
6101
|
+
for (const timer2 of this.leaseRetries.values()) clearTimeout(timer2);
|
|
6102
|
+
this.leaseRetries.clear();
|
|
6103
|
+
const consumers = this.runConsumersStopping;
|
|
6104
|
+
if (this.inflight.size === 0 && this.background.size === 0 && !consumers && this.runBatcher.idle) {
|
|
6105
|
+
return;
|
|
6106
|
+
}
|
|
6107
|
+
const queued = this.runBatcher.settle();
|
|
5507
6108
|
const start = Date.now();
|
|
5508
6109
|
let rearm = /* @__PURE__ */ __name(() => {
|
|
5509
6110
|
}, "rearm");
|
|
@@ -5522,7 +6123,10 @@ var WorkflowEngine = class {
|
|
|
5522
6123
|
try {
|
|
5523
6124
|
await Promise.race([
|
|
5524
6125
|
Promise.allSettled([
|
|
5525
|
-
...this.inflight
|
|
6126
|
+
...this.inflight,
|
|
6127
|
+
...this.background,
|
|
6128
|
+
queued,
|
|
6129
|
+
consumers
|
|
5526
6130
|
]).then(() => {
|
|
5527
6131
|
settled = true;
|
|
5528
6132
|
}),
|
|
@@ -5629,6 +6233,7 @@ var WorkflowEngine = class {
|
|
|
5629
6233
|
error: error2
|
|
5630
6234
|
});
|
|
5631
6235
|
await this.cancelChildren(run.id).catch(() => void 0);
|
|
6236
|
+
this.notifyParent(run.id, cancelledCompletion(error2));
|
|
5632
6237
|
}
|
|
5633
6238
|
}
|
|
5634
6239
|
}
|
|
@@ -5643,7 +6248,9 @@ var WorkflowEngine = class {
|
|
|
5643
6248
|
if (this.draining) break;
|
|
5644
6249
|
const outcome = this.isOwnLiveLease(listed, nowMs) ? await this.reclaimOwnLease(listed, nowMs) : await this.recoverOrphan(listed, nowMs);
|
|
5645
6250
|
if (!outcome) continue;
|
|
5646
|
-
if (outcome.dispatch)
|
|
6251
|
+
if (outcome.dispatch) {
|
|
6252
|
+
await this.dispatchRun(outcome.result.runId, listed.priority, freshPendingKey());
|
|
6253
|
+
}
|
|
5647
6254
|
results.push(outcome.result);
|
|
5648
6255
|
}
|
|
5649
6256
|
return results;
|
|
@@ -5835,6 +6442,18 @@ var WorkflowEngine = class {
|
|
|
5835
6442
|
const due = shuffle(await this.store.listDueTimers(nowMs, this.namespace, this.timerBatchSize));
|
|
5836
6443
|
const blocked = await this.dueBlockedRuns(nowMs);
|
|
5837
6444
|
if (due.length === 0 && blocked.length === 0) return [];
|
|
6445
|
+
if (this.queuesRunsOverTransport && !this.draining) {
|
|
6446
|
+
const runs = [
|
|
6447
|
+
...due,
|
|
6448
|
+
...blocked
|
|
6449
|
+
];
|
|
6450
|
+
if (await this.handOffDue(runs, nowMs)) {
|
|
6451
|
+
return runs.map((run) => ({
|
|
6452
|
+
runId: run.id,
|
|
6453
|
+
status: run.status
|
|
6454
|
+
}));
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
5838
6457
|
const waiterByRun = indexWaitersByRun(await listSignalWaitersOfRuns(this.store, [
|
|
5839
6458
|
...due,
|
|
5840
6459
|
...blocked
|
|
@@ -5868,7 +6487,7 @@ var WorkflowEngine = class {
|
|
|
5868
6487
|
const buffered = await this.store.takeBufferedSignal(waiter.token);
|
|
5869
6488
|
if (!buffered) return void 0;
|
|
5870
6489
|
await this.store.removeSignalWaiter(waiter);
|
|
5871
|
-
const settled = await this.deliverSignal(waiter, buffered.payload);
|
|
6490
|
+
const settled = await this.deliverSignal(waiter, buffered.payload, true);
|
|
5872
6491
|
return settled ?? {
|
|
5873
6492
|
runId: run.id,
|
|
5874
6493
|
status: run.status
|
|
@@ -5894,7 +6513,7 @@ var WorkflowEngine = class {
|
|
|
5894
6513
|
if (!eventMatches(candidate.payload, match)) continue;
|
|
5895
6514
|
if (!await this.store.removeBufferedEvent(candidate.id)) continue;
|
|
5896
6515
|
await this.store.removeSignalWaiter(waiter);
|
|
5897
|
-
const settled = await this.deliverSignal(waiter, candidate.payload);
|
|
6516
|
+
const settled = await this.deliverSignal(waiter, candidate.payload, true);
|
|
5898
6517
|
return settled ?? {
|
|
5899
6518
|
runId: run.id,
|
|
5900
6519
|
status: run.status
|
|
@@ -6014,7 +6633,7 @@ var WorkflowEngine = class {
|
|
|
6014
6633
|
} : {},
|
|
6015
6634
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6016
6635
|
});
|
|
6017
|
-
await this.
|
|
6636
|
+
await this.dispatchRun(runId, run.priority, freshPendingKey());
|
|
6018
6637
|
return {
|
|
6019
6638
|
runId,
|
|
6020
6639
|
status: "pending"
|
|
@@ -6089,7 +6708,16 @@ var WorkflowEngine = class {
|
|
|
6089
6708
|
* {@link resumeDueTimers}.
|
|
6090
6709
|
*/
|
|
6091
6710
|
async runPending(nowMs = this.clock()) {
|
|
6092
|
-
|
|
6711
|
+
const pending = await this.store.listPendingRuns(100, this.namespace);
|
|
6712
|
+
if (this.queuesRunsOverTransport && !this.draining && pending.length > 0) {
|
|
6713
|
+
if (await this.queueOnTransport(pending)) {
|
|
6714
|
+
return pending.map((run) => ({
|
|
6715
|
+
runId: run.id,
|
|
6716
|
+
status: run.status
|
|
6717
|
+
}));
|
|
6718
|
+
}
|
|
6719
|
+
}
|
|
6720
|
+
return this.resumeLeased(pending, nowMs);
|
|
6093
6721
|
}
|
|
6094
6722
|
/**
|
|
6095
6723
|
* Resolve once `runId` reaches a settled state — terminal (completed/failed/cancelled/dead) or
|
|
@@ -6266,7 +6894,7 @@ var WorkflowEngine = class {
|
|
|
6266
6894
|
* due-timer reconcile sweep ({@link reconcileSignalWaiter}) — all three resolve a waiter the exact
|
|
6267
6895
|
* same way.
|
|
6268
6896
|
*/
|
|
6269
|
-
async deliverSignal(waiter, payload) {
|
|
6897
|
+
async deliverSignal(waiter, payload, resumeHere = false) {
|
|
6270
6898
|
await this.store.saveCheckpoint(instantCheckpoint({
|
|
6271
6899
|
runId: waiter.runId,
|
|
6272
6900
|
seq: waiter.seq,
|
|
@@ -6285,7 +6913,7 @@ var WorkflowEngine = class {
|
|
|
6285
6913
|
status: "suspended"
|
|
6286
6914
|
};
|
|
6287
6915
|
}
|
|
6288
|
-
return this.resumeImplicit(waiter.runId).catch((err) => {
|
|
6916
|
+
return (resumeHere ? this.resumeImplicit(waiter.runId) : this.wake(waiter.runId)).catch((err) => {
|
|
6289
6917
|
if (err instanceof NamespaceMismatch) return null;
|
|
6290
6918
|
throw err;
|
|
6291
6919
|
});
|
|
@@ -6372,6 +7000,101 @@ var WorkflowEngine = class {
|
|
|
6372
7000
|
return;
|
|
6373
7001
|
}
|
|
6374
7002
|
}
|
|
7003
|
+
/**
|
|
7004
|
+
* Deliver the outcome of every awaited child that already finished but whose completion never
|
|
7005
|
+
* reached this run, into `checkpoints` (this turn's snapshot) and the store.
|
|
7006
|
+
*
|
|
7007
|
+
* A child persists its terminal status first and tells its parent after ({@link notifyParent}). A
|
|
7008
|
+
* crash, a scale-down or a store error between the two leaves the parent's `running` placeholder
|
|
7009
|
+
* and `child:<id>` waiter behind for a child that is done, and nothing else would ever settle
|
|
7010
|
+
* them: the parent's reconcile wake replays straight back to the same wait. So each turn looks up
|
|
7011
|
+
* the children of its `running` placeholders (one batched read per 500) and settles the finished
|
|
7012
|
+
* ones exactly as the child's own notification would have: the `signal:child:<id>` checkpoint
|
|
7013
|
+
* with the child's completion, then the waiter removed. A notification arriving late writes the
|
|
7014
|
+
* same checkpoint and finds no waiter. Best effort: a store error leaves the rest to the next turn.
|
|
7015
|
+
*/
|
|
7016
|
+
async deliverFinishedChildren(runId, checkpoints) {
|
|
7017
|
+
const awaited = [];
|
|
7018
|
+
for (const cp of checkpoints) {
|
|
7019
|
+
if (cp.kind !== "signal" || cp.status !== "running") continue;
|
|
7020
|
+
if (!cp.name.startsWith(CHILD_SIGNAL_PREFIX)) continue;
|
|
7021
|
+
awaited.push({
|
|
7022
|
+
childId: cp.name.slice(CHILD_SIGNAL_PREFIX.length),
|
|
7023
|
+
seq: cp.seq,
|
|
7024
|
+
parallelGroup: cp.parallelGroup
|
|
7025
|
+
});
|
|
7026
|
+
}
|
|
7027
|
+
await this.settleFinishedChildren(runId, awaited, checkpoints);
|
|
7028
|
+
}
|
|
7029
|
+
/**
|
|
7030
|
+
* {@link deliverFinishedChildren} for a remote workflow, whose awaited children (`startChild`
|
|
7031
|
+
* commands) leave a `child:<id>` waiter but no placeholder checkpoint: the run's own waiters say
|
|
7032
|
+
* which children it is waiting on. One indexed read per turn of a remote run.
|
|
7033
|
+
*/
|
|
7034
|
+
async deliverFinishedChildWaiters(runId, checkpoints) {
|
|
7035
|
+
let waiters;
|
|
7036
|
+
try {
|
|
7037
|
+
waiters = await listSignalWaitersOfRuns(this.store, [
|
|
7038
|
+
runId
|
|
7039
|
+
]);
|
|
7040
|
+
} catch {
|
|
7041
|
+
return;
|
|
7042
|
+
}
|
|
7043
|
+
const settled = new Set(checkpoints.filter((cp) => cp.status === "completed").map((cp) => cp.seq));
|
|
7044
|
+
const awaited = waiters.filter((w) => w.token.startsWith("child:") && !settled.has(w.seq)).map((w) => ({
|
|
7045
|
+
childId: w.token.slice("child:".length),
|
|
7046
|
+
seq: w.seq,
|
|
7047
|
+
parallelGroup: w.parallelGroup
|
|
7048
|
+
}));
|
|
7049
|
+
await this.settleFinishedChildren(runId, awaited, checkpoints);
|
|
7050
|
+
}
|
|
7051
|
+
/**
|
|
7052
|
+
* Settle every child in `awaited` that has finished: its `signal:child:<id>` checkpoint with the
|
|
7053
|
+
* child's completion, then its waiter removed, exactly as the child's own notification does, and
|
|
7054
|
+
* the checkpoint put into `checkpoints`. Children are read in batches of 500.
|
|
7055
|
+
*/
|
|
7056
|
+
async settleFinishedChildren(runId, awaited, checkpoints) {
|
|
7057
|
+
if (awaited.length === 0) return;
|
|
7058
|
+
try {
|
|
7059
|
+
const { runs: children } = await readRunsByIds(this.store, awaited.map((a) => a.childId));
|
|
7060
|
+
const finished = awaited.flatMap((a) => {
|
|
7061
|
+
const child = children.get(a.childId);
|
|
7062
|
+
const completion = child ? childCompletion(child) : void 0;
|
|
7063
|
+
return completion ? [
|
|
7064
|
+
{
|
|
7065
|
+
...a,
|
|
7066
|
+
completion
|
|
7067
|
+
}
|
|
7068
|
+
] : [];
|
|
7069
|
+
});
|
|
7070
|
+
for (let at = 0; at < finished.length; at += DELIVER_CHUNK) {
|
|
7071
|
+
await Promise.all(finished.slice(at, at + DELIVER_CHUNK).map(async (a) => {
|
|
7072
|
+
const settled = instantCheckpoint({
|
|
7073
|
+
runId,
|
|
7074
|
+
seq: a.seq,
|
|
7075
|
+
name: `${CHILD_SIGNAL_PREFIX}${a.childId}`,
|
|
7076
|
+
kind: "signal",
|
|
7077
|
+
output: a.completion,
|
|
7078
|
+
parallelGroup: a.parallelGroup
|
|
7079
|
+
});
|
|
7080
|
+
await this.store.saveCheckpoint(settled);
|
|
7081
|
+
await this.store.removeSignalWaiter({
|
|
7082
|
+
token: `child:${a.childId}`,
|
|
7083
|
+
runId,
|
|
7084
|
+
seq: a.seq,
|
|
7085
|
+
...a.parallelGroup !== void 0 ? {
|
|
7086
|
+
parallelGroup: a.parallelGroup
|
|
7087
|
+
} : {}
|
|
7088
|
+
});
|
|
7089
|
+
const index = checkpoints.findIndex((cp) => cp.seq === a.seq);
|
|
7090
|
+
if (index === -1) checkpoints.push(settled);
|
|
7091
|
+
else checkpoints[index] = settled;
|
|
7092
|
+
}));
|
|
7093
|
+
}
|
|
7094
|
+
checkpoints.sort((a, b) => a.seq - b.seq);
|
|
7095
|
+
} catch {
|
|
7096
|
+
}
|
|
7097
|
+
}
|
|
6375
7098
|
async redriveSettledChildren(runId, fan) {
|
|
6376
7099
|
if (!this.store.hasSignalWaiter) return;
|
|
6377
7100
|
for (const wait of fan.waits) {
|
|
@@ -6470,13 +7193,63 @@ var WorkflowEngine = class {
|
|
|
6470
7193
|
* knowing about the child feature.
|
|
6471
7194
|
*/
|
|
6472
7195
|
notifyParent(runId, completion) {
|
|
6473
|
-
|
|
7196
|
+
this.inBackground(this.deliverToParent(`child:${runId}`, completion));
|
|
6474
7197
|
const at = runId.lastIndexOf("~retry~");
|
|
6475
7198
|
if (at !== -1 && completion.ok) {
|
|
6476
|
-
|
|
7199
|
+
this.inBackground(this.deliverToParent(`child:${runId.slice(0, at)}`, completion));
|
|
6477
7200
|
}
|
|
6478
7201
|
}
|
|
6479
7202
|
/**
|
|
7203
|
+
* {@link signal} a child's completion to its parent, retrying a store error a few times. The child
|
|
7204
|
+
* is already persisted terminal, so a failure here would leave the parent waiting on a child that
|
|
7205
|
+
* finished; the parent's own turns reconcile that ({@link deliverFinishedChildren}), but only on
|
|
7206
|
+
* its next wake. A waiter already taken is delivered to directly on a retry, so the completion is
|
|
7207
|
+
* not buffered for a waiter that no longer exists.
|
|
7208
|
+
*/
|
|
7209
|
+
async deliverToParent(token, completion) {
|
|
7210
|
+
let waiter;
|
|
7211
|
+
const cancelled = !completion.ok && completion.code === CHILD_CANCELLED;
|
|
7212
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
7213
|
+
try {
|
|
7214
|
+
if (waiter === void 0) waiter = await this.store.takeSignalWaiter(token);
|
|
7215
|
+
if (waiter) {
|
|
7216
|
+
const parent = cancelled ? await this.store.getRun(waiter.runId) : void 0;
|
|
7217
|
+
if (parent && TERMINAL_RUN_STATUSES.includes(parent.status)) return;
|
|
7218
|
+
await this.deliverSignal(waiter, completion);
|
|
7219
|
+
return;
|
|
7220
|
+
}
|
|
7221
|
+
if (cancelled || await this.parentHoldsCompletion(token, completion)) return;
|
|
7222
|
+
await this.signal(token, completion);
|
|
7223
|
+
return;
|
|
7224
|
+
} catch (err) {
|
|
7225
|
+
if (err instanceof NamespaceMismatch) return;
|
|
7226
|
+
if (attempt >= PARENT_NOTIFY_ATTEMPTS || this.draining) return;
|
|
7227
|
+
await new Promise((resolve) => {
|
|
7228
|
+
const timer = setTimeout(resolve, PARENT_NOTIFY_BACKOFF_MS * 4 ** (attempt - 1));
|
|
7229
|
+
timer.unref?.();
|
|
7230
|
+
});
|
|
7231
|
+
}
|
|
7232
|
+
}
|
|
7233
|
+
}
|
|
7234
|
+
/**
|
|
7235
|
+
* Whether the parent of the child behind `token` (`child:<id>`) already holds `completion` as that
|
|
7236
|
+
* child's settled `signal:child:<id>` checkpoint: its own turn settled the child from the child's
|
|
7237
|
+
* row, so this notification is a duplicate. A different completion (a fix-and-replay run's success
|
|
7238
|
+
* for a child the parent saw fail) is not, and is buffered for the parent's retry. Only for a child
|
|
7239
|
+
* whose id the engine minted (its parent is derivable from it).
|
|
7240
|
+
*/
|
|
7241
|
+
async parentHoldsCompletion(token, completion) {
|
|
7242
|
+
const childId = token.slice("child:".length);
|
|
7243
|
+
const parentId = parentRunIdOf(childId);
|
|
7244
|
+
if (!parentId) return false;
|
|
7245
|
+
const name = `${CHILD_SIGNAL_PREFIX}${childId}`;
|
|
7246
|
+
const checkpoints = this.store.listCheckpointsByNamePrefix ? await this.store.listCheckpointsByNamePrefix(parentId, [
|
|
7247
|
+
name
|
|
7248
|
+
]) : await this.store.listCheckpoints(parentId);
|
|
7249
|
+
const expected = JSON.stringify(completion);
|
|
7250
|
+
return checkpoints.some((cp) => cp.name === name && cp.status === "completed" && JSON.stringify(cp.output) === expected);
|
|
7251
|
+
}
|
|
7252
|
+
/**
|
|
6480
7253
|
* Deferred child start shared by the in-process ctx host and the remote `startChild` command.
|
|
6481
7254
|
* Deferred (microtask) so a fast child can't reentrantly resume a still-suspending parent. A start
|
|
6482
7255
|
* that THROWS (unregistered/unroutable workflow, input validation, singleton back-pressure, store
|
|
@@ -6489,13 +7262,16 @@ var WorkflowEngine = class {
|
|
|
6489
7262
|
* correctly observes the failed start.
|
|
6490
7263
|
*/
|
|
6491
7264
|
startChildDeferred(workflow, input, childId, opts) {
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
this.
|
|
6495
|
-
|
|
6496
|
-
|
|
7265
|
+
this.localWork.submit(`start:${childId}`, async () => {
|
|
7266
|
+
if (this.draining) return;
|
|
7267
|
+
await this.start(workflow, input, childId, opts).catch((err) => {
|
|
7268
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7269
|
+
this.notifyParent(childId, {
|
|
7270
|
+
ok: false,
|
|
7271
|
+
error: `child workflow "${workflow}" failed to start: ${message}`
|
|
7272
|
+
});
|
|
6497
7273
|
});
|
|
6498
|
-
})
|
|
7274
|
+
});
|
|
6499
7275
|
}
|
|
6500
7276
|
/**
|
|
6501
7277
|
* Cancel a run (e.g. from the dashboard). Returns null if the run does not exist. Pass
|
|
@@ -6559,6 +7335,7 @@ var WorkflowEngine = class {
|
|
|
6559
7335
|
error: error2
|
|
6560
7336
|
});
|
|
6561
7337
|
await this.cancelChildren(runId, opts);
|
|
7338
|
+
this.notifyParent(runId, cancelledCompletion(error2));
|
|
6562
7339
|
this.notifyCancelled(runId);
|
|
6563
7340
|
if (this.controlPlane) {
|
|
6564
7341
|
void this.controlPlane.publishControl({
|
|
@@ -7073,6 +7850,7 @@ var WorkflowEngine = class {
|
|
|
7073
7850
|
const lockNow = this.clock();
|
|
7074
7851
|
if (run.lockedBy !== this.instanceId || (run.lockedUntil ?? 0) <= lockNow) {
|
|
7075
7852
|
if (!await this.store.tryLockRun(run.id, this.instanceId, lockNow + this.leaseMs, lockNow)) {
|
|
7853
|
+
this.recheckAfterLease(run.id, run.lockedUntil);
|
|
7076
7854
|
return {
|
|
7077
7855
|
runId: run.id,
|
|
7078
7856
|
status: run.status
|
|
@@ -7091,6 +7869,51 @@ var WorkflowEngine = class {
|
|
|
7091
7869
|
}
|
|
7092
7870
|
}
|
|
7093
7871
|
/**
|
|
7872
|
+
* A wake that found the run leased by another instance is dropped, and normally that is fine: the
|
|
7873
|
+
* holder's turn either sees what the wake wrote, or re-checks once it releases the lease
|
|
7874
|
+
* ({@link redriveSettledSteps}, {@link redriveSettledChildren}). Not if the holder goes down first
|
|
7875
|
+
* (a scale-down mid-turn, an OOM): its lease lapses with the run `suspended` on a step that has
|
|
7876
|
+
* finished, and nothing wakes it before its timer.
|
|
7877
|
+
*
|
|
7878
|
+
* So the dropped wake is kept here: once the lease is free (looked at every
|
|
7879
|
+
* {@link LEASE_RECHECK_MAX_MS} at most, for a bounded number of times), a run still `suspended` is
|
|
7880
|
+
* leased and resumed. A holder that already moved the run on makes this one spare replay that
|
|
7881
|
+
* re-parks. Best effort; the run's timer remains the fallback if this instance goes down too.
|
|
7882
|
+
*/
|
|
7883
|
+
recheckAfterLease(runId, lockedUntil, attempt = 1) {
|
|
7884
|
+
if (this.draining || this.leaseRetries.has(runId)) return;
|
|
7885
|
+
const remaining = Math.max(0, (lockedUntil ?? this.clock()) - this.clock());
|
|
7886
|
+
const timer = setTimeout(() => {
|
|
7887
|
+
this.leaseRetries.delete(runId);
|
|
7888
|
+
this.inBackground(this.retryDroppedWake(runId, attempt));
|
|
7889
|
+
}, Math.min(remaining, LEASE_RECHECK_MAX_MS) + LEASE_RECHECK_SLACK_MS);
|
|
7890
|
+
timer.unref?.();
|
|
7891
|
+
this.leaseRetries.set(runId, timer);
|
|
7892
|
+
}
|
|
7893
|
+
async retryDroppedWake(runId, attempt) {
|
|
7894
|
+
if (this.draining) return;
|
|
7895
|
+
const run = await this.store.getRun(runId);
|
|
7896
|
+
if (run?.status !== "suspended") return;
|
|
7897
|
+
const nowMs = this.clock();
|
|
7898
|
+
if (run.lockedUntil !== void 0 && run.lockedUntil > nowMs) {
|
|
7899
|
+
if (attempt < LEASE_RECHECK_ATTEMPTS) this.recheckAfterLease(runId, run.lockedUntil, attempt + 1);
|
|
7900
|
+
return;
|
|
7901
|
+
}
|
|
7902
|
+
if (this.queuesRunsOverTransport) {
|
|
7903
|
+
await this.wake(runId);
|
|
7904
|
+
return;
|
|
7905
|
+
}
|
|
7906
|
+
if (!await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs)) return;
|
|
7907
|
+
const fresh = await this.store.getRun(runId);
|
|
7908
|
+
if (fresh?.status !== "suspended") {
|
|
7909
|
+
await this.store.releaseRunLock(runId);
|
|
7910
|
+
return;
|
|
7911
|
+
}
|
|
7912
|
+
await this.resumeImplicit(runId).catch((err) => {
|
|
7913
|
+
if (!(err instanceof NamespaceMismatch)) throw err;
|
|
7914
|
+
});
|
|
7915
|
+
}
|
|
7916
|
+
/**
|
|
7094
7917
|
* Renew `runId`'s lease for as long as this execution holds it, and register it in
|
|
7095
7918
|
* {@link executionLeases} so settles can check the zombie fence and `drain()` can release it.
|
|
7096
7919
|
*
|
|
@@ -7454,6 +8277,7 @@ ${run.workflowVersion}`;
|
|
|
7454
8277
|
namespace: run.namespace,
|
|
7455
8278
|
error: error2
|
|
7456
8279
|
});
|
|
8280
|
+
this.notifyParent(run.id, cancelledCompletion(error2));
|
|
7457
8281
|
this.notifyCancelled(run.id);
|
|
7458
8282
|
if (this.controlPlane) {
|
|
7459
8283
|
void this.controlPlane.publishControl({
|
|
@@ -7485,6 +8309,8 @@ ${run.workflowVersion}`;
|
|
|
7485
8309
|
const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
|
|
7486
8310
|
if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
|
|
7487
8311
|
const checkpoints = await this.store.listCheckpoints(run.id);
|
|
8312
|
+
await this.deliverFinishedChildren(run.id, checkpoints);
|
|
8313
|
+
await this.deliverFinishedChildWaiters(run.id, checkpoints);
|
|
7488
8314
|
const handoff = liveHandoff(run.id, checkpoints);
|
|
7489
8315
|
if (handoff) {
|
|
7490
8316
|
const result = await this.settleRun(run, await this.followHandoff(run, handoff));
|
|
@@ -8059,6 +8885,7 @@ ${run.workflowVersion}`;
|
|
|
8059
8885
|
namespace: run.namespace,
|
|
8060
8886
|
error: error2
|
|
8061
8887
|
});
|
|
8888
|
+
this.notifyParent(run.id, cancelledCompletion(error2));
|
|
8062
8889
|
return {
|
|
8063
8890
|
runId: run.id,
|
|
8064
8891
|
status: "cancelled",
|
|
@@ -8105,6 +8932,7 @@ ${run.workflowVersion}`;
|
|
|
8105
8932
|
}
|
|
8106
8933
|
const compensations = [];
|
|
8107
8934
|
const snapshot = await this.store.listCheckpoints(run.id);
|
|
8935
|
+
await this.deliverFinishedChildren(run.id, snapshot);
|
|
8108
8936
|
const replay = /* @__PURE__ */ new Map();
|
|
8109
8937
|
for (const cp of snapshot) replay.set(cp.seq, cp);
|
|
8110
8938
|
let suspendedOnFan;
|
|
@@ -8201,13 +9029,13 @@ ${run.workflowVersion}`;
|
|
|
8201
9029
|
if (this.awaitedStepFans.get(run.id) === awaitedFans) this.awaitedStepFans.delete(run.id);
|
|
8202
9030
|
await this.releaseLeaseBestEffort(run.id);
|
|
8203
9031
|
if (suspendedOnFan) {
|
|
8204
|
-
|
|
9032
|
+
this.inBackground(this.redriveSettledChildren(run.id, suspendedOnFan));
|
|
8205
9033
|
}
|
|
8206
9034
|
if (suspendedOnSteps && awaitedSteps.size > 0) {
|
|
8207
|
-
|
|
9035
|
+
this.inBackground(this.redriveSettledSteps(run.id, awaitedSteps));
|
|
8208
9036
|
}
|
|
8209
9037
|
if (suspendedOnSteps && awaitedFans.length > 0) {
|
|
8210
|
-
|
|
9038
|
+
this.inBackground(this.redriveSettledStepFans(run.id, awaitedFans));
|
|
8211
9039
|
}
|
|
8212
9040
|
}
|
|
8213
9041
|
}
|
|
@@ -8647,9 +9475,11 @@ ${run.workflowVersion}`;
|
|
|
8647
9475
|
})) {
|
|
8648
9476
|
return this.callRemote(runId, seq, step, input, queue, transport, void 0, admission, routing);
|
|
8649
9477
|
}
|
|
8650
|
-
throw new WorkflowSuspended(wakeAt);
|
|
9478
|
+
throw new WorkflowSuspended(this.stepLeaseWakeAt(wakeAt));
|
|
9479
|
+
}
|
|
9480
|
+
if (this.clock() < existing.wakeAt) {
|
|
9481
|
+
throw new WorkflowSuspended(this.stepLeaseWakeAt(existing.wakeAt));
|
|
8651
9482
|
}
|
|
8652
|
-
if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
|
|
8653
9483
|
if (existing.attempts >= this.remoteRedispatchMax) {
|
|
8654
9484
|
throw new RemoteStepError({
|
|
8655
9485
|
message: `remote step "${step.name}" lost \u2014 no result after ${existing.attempts} re-dispatch(es)`,
|
|
@@ -8695,7 +9525,7 @@ ${run.workflowVersion}`;
|
|
|
8695
9525
|
kind: "remote",
|
|
8696
9526
|
redispatched: true
|
|
8697
9527
|
});
|
|
8698
|
-
throw new WorkflowSuspended(nextDeadline);
|
|
9528
|
+
throw new WorkflowSuspended(this.stepLeaseWakeAt(nextDeadline));
|
|
8699
9529
|
}
|
|
8700
9530
|
let attempt = 1;
|
|
8701
9531
|
if (existing?.status === "failed") {
|
|
@@ -9058,6 +9888,7 @@ ${run.workflowVersion}`;
|
|
|
9058
9888
|
});
|
|
9059
9889
|
const reads = await Promise.allSettled(live.map((i) => this.store.getCheckpoint(runId, results[i].seq)));
|
|
9060
9890
|
const pending = [];
|
|
9891
|
+
const resettled = [];
|
|
9061
9892
|
reads.forEach((read, k) => {
|
|
9062
9893
|
const index = live[k];
|
|
9063
9894
|
if (read.status === "rejected") outcomes[index] = {
|
|
@@ -9067,8 +9898,17 @@ ${run.workflowVersion}`;
|
|
|
9067
9898
|
index,
|
|
9068
9899
|
cp: read.value
|
|
9069
9900
|
});
|
|
9901
|
+
else if (read.value?.status === "completed" || read.value?.status === "failed") {
|
|
9902
|
+
resettled.push({
|
|
9903
|
+
index,
|
|
9904
|
+
cp: read.value
|
|
9905
|
+
});
|
|
9906
|
+
}
|
|
9070
9907
|
});
|
|
9071
|
-
if (pending.length === 0)
|
|
9908
|
+
if (pending.length === 0) {
|
|
9909
|
+
if (resettled.length === 0) return outcomes;
|
|
9910
|
+
return this.resumeForRedelivered(runId, resettled, outcomes);
|
|
9911
|
+
}
|
|
9072
9912
|
for (const { cp } of pending) void this.releaseQueueSlot(cp.stepId).catch(() => void 0);
|
|
9073
9913
|
const run = await this.store.getRun(runId);
|
|
9074
9914
|
if (run && (run.status === "cancelled" || run.status === "completed")) return outcomes;
|
|
@@ -9120,6 +9960,29 @@ ${run.workflowVersion}`;
|
|
|
9120
9960
|
return outcomes;
|
|
9121
9961
|
}
|
|
9122
9962
|
/**
|
|
9963
|
+
* The outcomes for a batch of redelivered results (their steps already settled): one of them
|
|
9964
|
+
* resumes the run if it is `suspended` and, for a step of a fan, the fan's barrier is open. A
|
|
9965
|
+
* resume of a run that had in fact moved on replays to its current wait and re-parks.
|
|
9966
|
+
*/
|
|
9967
|
+
async resumeForRedelivered(runId, resettled, outcomes) {
|
|
9968
|
+
const run = await this.store.getRun(runId);
|
|
9969
|
+
if (run?.status !== "suspended") return outcomes;
|
|
9970
|
+
const barriers = /* @__PURE__ */ new Map();
|
|
9971
|
+
for (const { index, cp } of resettled) {
|
|
9972
|
+
const asResult = {
|
|
9973
|
+
runId,
|
|
9974
|
+
seq: cp.seq,
|
|
9975
|
+
stepId: cp.stepId ?? stepId(runId, cp.seq),
|
|
9976
|
+
status: cp.status === "failed" ? "failed" : "completed"
|
|
9977
|
+
};
|
|
9978
|
+
if (!await this.stepFanStillRunning(cp, asResult, barriers)) {
|
|
9979
|
+
outcomes[index] = true;
|
|
9980
|
+
break;
|
|
9981
|
+
}
|
|
9982
|
+
}
|
|
9983
|
+
return outcomes;
|
|
9984
|
+
}
|
|
9985
|
+
/**
|
|
9123
9986
|
* The join barrier of a step fan: a result for one of its steps resumes the parent only once no
|
|
9124
9987
|
* step of the fan is still `pending` (a settled wave), or on a failure (failFast surfaces it, and a
|
|
9125
9988
|
* retry needs the parent to schedule it). Without it every result would replay the whole parent,
|
|
@@ -9243,7 +10106,7 @@ ${run.workflowVersion}`;
|
|
|
9243
10106
|
...waiters
|
|
9244
10107
|
];
|
|
9245
10108
|
waiters.clear();
|
|
9246
|
-
for (const runId of runIds) void this.
|
|
10109
|
+
for (const runId of runIds) void this.wake(runId).catch(() => void 0);
|
|
9247
10110
|
}
|
|
9248
10111
|
/** Release the flow-control slot a dispatched step held (if any), by its stepId. */
|
|
9249
10112
|
async releaseQueueSlot(id) {
|
|
@@ -9985,6 +10848,8 @@ var CodecStateStore = class {
|
|
|
9985
10848
|
registerChildWaits;
|
|
9986
10849
|
/** The folded checkpoint carries the children's outputs, so it is encoded. */
|
|
9987
10850
|
compactCheckpoints;
|
|
10851
|
+
/** Decodes each run like {@link getRun}. Bound only when the inner store has it. */
|
|
10852
|
+
getRuns;
|
|
9988
10853
|
/** Forwarded verbatim — a waiter carries no payload. */
|
|
9989
10854
|
listSignalWaitersByRun;
|
|
9990
10855
|
/** Forwarded verbatim — a waiter carries no payload. */
|
|
@@ -10028,6 +10893,8 @@ var CodecStateStore = class {
|
|
|
10028
10893
|
})));
|
|
10029
10894
|
const compact = inner.compactCheckpoints;
|
|
10030
10895
|
if (compact) this.compactCheckpoints = (runId, fromSeq, toSeq, into) => compact.call(inner, runId, fromSeq, toSeq, this.encCp(into));
|
|
10896
|
+
const getRuns = inner.getRuns;
|
|
10897
|
+
if (getRuns) this.getRuns = async (runIds) => (await getRuns.call(inner, runIds)).map((r) => this.decRun(r));
|
|
10031
10898
|
const byRun = inner.listSignalWaitersByRun;
|
|
10032
10899
|
if (byRun) this.listSignalWaitersByRun = (runIds) => byRun.call(inner, runIds);
|
|
10033
10900
|
const hasWaiter = inner.hasSignalWaiter;
|
|
@@ -10580,6 +11447,16 @@ var InMemoryStateStore = class {
|
|
|
10580
11447
|
...run
|
|
10581
11448
|
} : null;
|
|
10582
11449
|
}
|
|
11450
|
+
async getRuns(runIds) {
|
|
11451
|
+
const out = [];
|
|
11452
|
+
for (const id of new Set(runIds)) {
|
|
11453
|
+
const run = this.runs.get(id);
|
|
11454
|
+
if (run) out.push({
|
|
11455
|
+
...run
|
|
11456
|
+
});
|
|
11457
|
+
}
|
|
11458
|
+
return out;
|
|
11459
|
+
}
|
|
10583
11460
|
async deleteRun(runId) {
|
|
10584
11461
|
this.runs.delete(runId);
|
|
10585
11462
|
for (const [key, cp] of this.checkpoints) {
|
|
@@ -11031,6 +11908,99 @@ var InMemoryTransport = class {
|
|
|
11031
11908
|
this.controlHandlers.add(handler);
|
|
11032
11909
|
}
|
|
11033
11910
|
};
|
|
11911
|
+
|
|
11912
|
+
// src/testing/in-memory-run-queue.ts
|
|
11913
|
+
var InMemoryRunQueue = class {
|
|
11914
|
+
static {
|
|
11915
|
+
__name(this, "InMemoryRunQueue");
|
|
11916
|
+
}
|
|
11917
|
+
lanes = /* @__PURE__ */ new Map();
|
|
11918
|
+
/** A transport on this queue. */
|
|
11919
|
+
transport() {
|
|
11920
|
+
return {
|
|
11921
|
+
dispatch: /* @__PURE__ */ __name(async (_task) => {
|
|
11922
|
+
throw new Error("InMemoryRunQueue carries no remote steps");
|
|
11923
|
+
}, "dispatch"),
|
|
11924
|
+
onResult: /* @__PURE__ */ __name((_handler) => {
|
|
11925
|
+
}, "onResult"),
|
|
11926
|
+
onHeartbeat: /* @__PURE__ */ __name((_handler) => {
|
|
11927
|
+
}, "onHeartbeat"),
|
|
11928
|
+
dispatchRuns: /* @__PURE__ */ __name(async (runs, opts) => this.add(runs, opts), "dispatchRuns"),
|
|
11929
|
+
onDispatchedRun: /* @__PURE__ */ __name((handler, opts) => this.consume(handler, opts), "onDispatchedRun")
|
|
11930
|
+
};
|
|
11931
|
+
}
|
|
11932
|
+
/** Runs waiting in the queue of `namespace`. */
|
|
11933
|
+
waiting(namespace) {
|
|
11934
|
+
return (this.lanes.get(namespace ?? "")?.waiting ?? []).map((r) => r.runId);
|
|
11935
|
+
}
|
|
11936
|
+
lane(namespace) {
|
|
11937
|
+
const key = namespace ?? "";
|
|
11938
|
+
let lane = this.lanes.get(key);
|
|
11939
|
+
if (!lane) {
|
|
11940
|
+
lane = {
|
|
11941
|
+
waiting: [],
|
|
11942
|
+
queued: /* @__PURE__ */ new Map(),
|
|
11943
|
+
consumers: [],
|
|
11944
|
+
next: 0
|
|
11945
|
+
};
|
|
11946
|
+
this.lanes.set(key, lane);
|
|
11947
|
+
}
|
|
11948
|
+
return lane;
|
|
11949
|
+
}
|
|
11950
|
+
add(runs, opts) {
|
|
11951
|
+
const lane = this.lane(opts?.namespace);
|
|
11952
|
+
for (const run of runs) {
|
|
11953
|
+
const waiting = lane.queued.get(run.runId);
|
|
11954
|
+
if (waiting) {
|
|
11955
|
+
if (run.reason === "wake") waiting.reason = "wake";
|
|
11956
|
+
continue;
|
|
11957
|
+
}
|
|
11958
|
+
const entry = {
|
|
11959
|
+
...run
|
|
11960
|
+
};
|
|
11961
|
+
lane.queued.set(run.runId, entry);
|
|
11962
|
+
lane.waiting.push(entry);
|
|
11963
|
+
}
|
|
11964
|
+
lane.waiting.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
11965
|
+
setImmediate(() => this.pump(lane));
|
|
11966
|
+
}
|
|
11967
|
+
consume(handler, opts) {
|
|
11968
|
+
const lane = this.lane(opts.namespace);
|
|
11969
|
+
const consumer = {
|
|
11970
|
+
handler,
|
|
11971
|
+
concurrency: Math.max(1, opts.concurrency),
|
|
11972
|
+
active: 0,
|
|
11973
|
+
stopped: false,
|
|
11974
|
+
idle: []
|
|
11975
|
+
};
|
|
11976
|
+
lane.consumers.push(consumer);
|
|
11977
|
+
setImmediate(() => this.pump(lane));
|
|
11978
|
+
return () => {
|
|
11979
|
+
consumer.stopped = true;
|
|
11980
|
+
lane.consumers = lane.consumers.filter((c) => c !== consumer);
|
|
11981
|
+
if (consumer.active === 0) return Promise.resolve();
|
|
11982
|
+
return new Promise((resolve) => consumer.idle.push(resolve));
|
|
11983
|
+
};
|
|
11984
|
+
}
|
|
11985
|
+
pump(lane) {
|
|
11986
|
+
while (lane.waiting.length > 0) {
|
|
11987
|
+
const free = lane.consumers.filter((c) => !c.stopped && c.active < c.concurrency);
|
|
11988
|
+
if (free.length === 0) return;
|
|
11989
|
+
const consumer = free[lane.next % free.length];
|
|
11990
|
+
lane.next += 1;
|
|
11991
|
+
const run = lane.waiting.shift();
|
|
11992
|
+
lane.queued.delete(run.runId);
|
|
11993
|
+
consumer.active += 1;
|
|
11994
|
+
void consumer.handler(run).catch(() => void 0).then(() => {
|
|
11995
|
+
consumer.active -= 1;
|
|
11996
|
+
if (consumer.active === 0 && consumer.stopped) {
|
|
11997
|
+
for (const resolve of consumer.idle.splice(0)) resolve();
|
|
11998
|
+
}
|
|
11999
|
+
this.pump(lane);
|
|
12000
|
+
});
|
|
12001
|
+
}
|
|
12002
|
+
}
|
|
12003
|
+
};
|
|
11034
12004
|
// Annotate the CommonJS export names for ESM import in node:
|
|
11035
12005
|
0 && (module.exports = {
|
|
11036
12006
|
CHILD_EDGE_PREFIXES,
|
|
@@ -11058,6 +12028,7 @@ var InMemoryTransport = class {
|
|
|
11058
12028
|
GatherError,
|
|
11059
12029
|
HANDOFF_SEQ_BASE,
|
|
11060
12030
|
InMemoryAdmissionBackend,
|
|
12031
|
+
InMemoryRunQueue,
|
|
11061
12032
|
InMemoryStateStore,
|
|
11062
12033
|
InMemoryTransport,
|
|
11063
12034
|
LEGACY_V1_CAPABILITIES,
|
|
@@ -11142,6 +12113,7 @@ var InMemoryTransport = class {
|
|
|
11142
12113
|
fanPageEtag,
|
|
11143
12114
|
findStalledRuns,
|
|
11144
12115
|
foldedCount,
|
|
12116
|
+
getRunsByIds,
|
|
11145
12117
|
handoffHandlerId,
|
|
11146
12118
|
handoffName,
|
|
11147
12119
|
hasDurableExecutionWrappers,
|
|
@@ -11179,6 +12151,7 @@ var InMemoryTransport = class {
|
|
|
11179
12151
|
poolNamespace,
|
|
11180
12152
|
prevCronFireMs,
|
|
11181
12153
|
queryCheckpointsOf,
|
|
12154
|
+
readRunsByIds,
|
|
11182
12155
|
readSearchAttributes,
|
|
11183
12156
|
requiredCapabilities,
|
|
11184
12157
|
resolveConcurrency,
|