@dudousxd/nestjs-durable-core 0.84.1 → 0.85.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 +993 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +259 -4
- package/dist/index.d.ts +259 -4
- package/dist/index.js +990 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -570,6 +570,82 @@ function backoffDelay(attempt, options) {
|
|
|
570
570
|
}
|
|
571
571
|
__name(backoffDelay, "backoffDelay");
|
|
572
572
|
|
|
573
|
+
// src/bounded-runner.ts
|
|
574
|
+
var BoundedRunner = class {
|
|
575
|
+
static {
|
|
576
|
+
__name(this, "BoundedRunner");
|
|
577
|
+
}
|
|
578
|
+
limit;
|
|
579
|
+
active = 0;
|
|
580
|
+
waiting = /* @__PURE__ */ new Map();
|
|
581
|
+
seq = 0;
|
|
582
|
+
constructor(limit) {
|
|
583
|
+
this.limit = limit;
|
|
584
|
+
}
|
|
585
|
+
/** Tasks running now plus tasks waiting for a slot. */
|
|
586
|
+
get size() {
|
|
587
|
+
return this.active + this.waiting.size;
|
|
588
|
+
}
|
|
589
|
+
/** Tasks running now. */
|
|
590
|
+
get running() {
|
|
591
|
+
return this.active;
|
|
592
|
+
}
|
|
593
|
+
submit(key, task) {
|
|
594
|
+
if (this.active < this.limit) {
|
|
595
|
+
this.active += 1;
|
|
596
|
+
queueMicrotask(() => this.execute(task));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (!this.waiting.has(key)) this.waiting.set(key, {
|
|
600
|
+
task
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Run `task` under the bound and resolve once it settled, or once it was dropped by
|
|
605
|
+
* {@link clear} before it got a slot. Never deduplicated.
|
|
606
|
+
*/
|
|
607
|
+
run(task) {
|
|
608
|
+
return new Promise((resolve) => {
|
|
609
|
+
const done = /* @__PURE__ */ __name(() => resolve(), "done");
|
|
610
|
+
const wrapped = /* @__PURE__ */ __name(() => task().then(done, done), "wrapped");
|
|
611
|
+
if (this.active < this.limit) {
|
|
612
|
+
this.active += 1;
|
|
613
|
+
queueMicrotask(() => this.execute(wrapped));
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
this.seq += 1;
|
|
617
|
+
this.waiting.set(`\0run:${this.seq}`, {
|
|
618
|
+
task: wrapped,
|
|
619
|
+
dropped: done
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
/** Forget every task still waiting for a slot. */
|
|
624
|
+
clear() {
|
|
625
|
+
for (const { dropped } of this.waiting.values()) dropped?.();
|
|
626
|
+
this.waiting.clear();
|
|
627
|
+
}
|
|
628
|
+
execute(task) {
|
|
629
|
+
let settled;
|
|
630
|
+
try {
|
|
631
|
+
settled = task();
|
|
632
|
+
} catch {
|
|
633
|
+
settled = Promise.resolve();
|
|
634
|
+
}
|
|
635
|
+
void settled.catch(() => void 0).then(() => {
|
|
636
|
+
const next = this.waiting.entries().next();
|
|
637
|
+
if (next.done) {
|
|
638
|
+
this.active -= 1;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const [key, waiting] = next.value;
|
|
642
|
+
this.waiting.delete(key);
|
|
643
|
+
this.execute(waiting.task);
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
var LOCAL_RUN_CONCURRENCY = 256;
|
|
648
|
+
|
|
573
649
|
// src/execution-hooks.ts
|
|
574
650
|
var wrappers = [];
|
|
575
651
|
function useDurableExecution(wrapper) {
|
|
@@ -2130,6 +2206,61 @@ var RemoteWorkflowExecutor = class {
|
|
|
2130
2206
|
}
|
|
2131
2207
|
};
|
|
2132
2208
|
|
|
2209
|
+
// src/run-dispatch-batcher.ts
|
|
2210
|
+
var RUN_DISPATCH_BATCH = 500;
|
|
2211
|
+
var RUN_DISPATCH_WINDOW_MS = 5;
|
|
2212
|
+
var RunDispatchBatcher = class {
|
|
2213
|
+
static {
|
|
2214
|
+
__name(this, "RunDispatchBatcher");
|
|
2215
|
+
}
|
|
2216
|
+
send;
|
|
2217
|
+
onError;
|
|
2218
|
+
buffer = /* @__PURE__ */ new Map();
|
|
2219
|
+
timer;
|
|
2220
|
+
sending = /* @__PURE__ */ new Set();
|
|
2221
|
+
constructor(send, onError) {
|
|
2222
|
+
this.send = send;
|
|
2223
|
+
this.onError = onError;
|
|
2224
|
+
}
|
|
2225
|
+
/** Nothing buffered and nothing being sent. */
|
|
2226
|
+
get idle() {
|
|
2227
|
+
return this.buffer.size === 0 && this.sending.size === 0;
|
|
2228
|
+
}
|
|
2229
|
+
/** Buffer `run`. A wake for a run already buffered replaces its start: a wake also runs a `pending` run. */
|
|
2230
|
+
add(run) {
|
|
2231
|
+
const prior = this.buffer.get(run.runId);
|
|
2232
|
+
if (prior && run.reason !== "wake") return;
|
|
2233
|
+
this.buffer.set(run.runId, run);
|
|
2234
|
+
if (this.buffer.size >= RUN_DISPATCH_BATCH) {
|
|
2235
|
+
this.flush();
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
if (!this.timer) {
|
|
2239
|
+
this.timer = setTimeout(() => this.flush(), RUN_DISPATCH_WINDOW_MS);
|
|
2240
|
+
this.timer.unref?.();
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
/** Send what is buffered now. */
|
|
2244
|
+
flush() {
|
|
2245
|
+
if (this.timer) clearTimeout(this.timer);
|
|
2246
|
+
this.timer = void 0;
|
|
2247
|
+
if (this.buffer.size === 0) return;
|
|
2248
|
+
const runs = [
|
|
2249
|
+
...this.buffer.values()
|
|
2250
|
+
];
|
|
2251
|
+
this.buffer = /* @__PURE__ */ new Map();
|
|
2252
|
+
const sent = this.send(runs).catch((err) => this.onError(runs, err)).finally(() => this.sending.delete(sent));
|
|
2253
|
+
this.sending.add(sent);
|
|
2254
|
+
}
|
|
2255
|
+
/** Send what is buffered and wait for every send in flight. */
|
|
2256
|
+
async settle() {
|
|
2257
|
+
this.flush();
|
|
2258
|
+
await Promise.allSettled([
|
|
2259
|
+
...this.sending
|
|
2260
|
+
]);
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2133
2264
|
// src/run-waiting.ts
|
|
2134
2265
|
var WEBHOOK_PREFIX = "wh:";
|
|
2135
2266
|
var CHILD_PREFIX = "child:";
|
|
@@ -2200,6 +2331,51 @@ function indexWaitersByRun(waiters) {
|
|
|
2200
2331
|
return byRun;
|
|
2201
2332
|
}
|
|
2202
2333
|
__name(indexWaitersByRun, "indexWaitersByRun");
|
|
2334
|
+
var GET_RUN_FALLBACK_PARALLEL = 50;
|
|
2335
|
+
async function getRunsByIds(store, runIds) {
|
|
2336
|
+
const { runs, unread, error: error2 } = await readRunsByIds(store, runIds);
|
|
2337
|
+
if (unread.length > 0) throw error2;
|
|
2338
|
+
return runs;
|
|
2339
|
+
}
|
|
2340
|
+
__name(getRunsByIds, "getRunsByIds");
|
|
2341
|
+
async function readRunsByIds(store, runIds) {
|
|
2342
|
+
const unique = [
|
|
2343
|
+
...new Set(runIds)
|
|
2344
|
+
];
|
|
2345
|
+
const runs = /* @__PURE__ */ new Map();
|
|
2346
|
+
const unread = [];
|
|
2347
|
+
let error2;
|
|
2348
|
+
const batch = store.getRuns ? RUN_ID_BATCH : GET_RUN_FALLBACK_PARALLEL;
|
|
2349
|
+
for (let at = 0; at < unique.length; at += batch) {
|
|
2350
|
+
const ids = unique.slice(at, at + batch);
|
|
2351
|
+
if (store.getRuns) {
|
|
2352
|
+
try {
|
|
2353
|
+
for (const run of await store.getRuns(ids)) runs.set(run.id, run);
|
|
2354
|
+
} catch (err) {
|
|
2355
|
+
error2 ??= err;
|
|
2356
|
+
unread.push(...ids);
|
|
2357
|
+
}
|
|
2358
|
+
continue;
|
|
2359
|
+
}
|
|
2360
|
+
const reads = await Promise.allSettled(ids.map((id) => store.getRun(id)));
|
|
2361
|
+
reads.forEach((read, k) => {
|
|
2362
|
+
if (read.status === "fulfilled") {
|
|
2363
|
+
if (read.value) runs.set(read.value.id, read.value);
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
error2 ??= read.reason;
|
|
2367
|
+
unread.push(ids[k]);
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
return {
|
|
2371
|
+
runs,
|
|
2372
|
+
unread,
|
|
2373
|
+
...unread.length > 0 ? {
|
|
2374
|
+
error: error2
|
|
2375
|
+
} : {}
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
__name(readRunsByIds, "readRunsByIds");
|
|
2203
2379
|
|
|
2204
2380
|
// src/scheduler.ts
|
|
2205
2381
|
import { createRequire } from "node:module";
|
|
@@ -3092,6 +3268,39 @@ var TransportPool = class {
|
|
|
3092
3268
|
transport.onStartRun?.(handler);
|
|
3093
3269
|
}
|
|
3094
3270
|
}
|
|
3271
|
+
/** Does any transport here carry a run queue ({@link Transport.dispatchRuns} + `onDispatchedRun`)? */
|
|
3272
|
+
get dispatchesRuns() {
|
|
3273
|
+
return this.transports.some(({ transport }) => transport.dispatchRuns && transport.onDispatchedRun);
|
|
3274
|
+
}
|
|
3275
|
+
/**
|
|
3276
|
+
* Queue `runs` on the first transport that carries a run queue, failing over to the next on an
|
|
3277
|
+
* error, like {@link dispatch}. Throws if none accepted them.
|
|
3278
|
+
*/
|
|
3279
|
+
async dispatchRuns(runs, opts) {
|
|
3280
|
+
let lastErr;
|
|
3281
|
+
for (const { transport } of this.transports) {
|
|
3282
|
+
if (!transport.dispatchRuns || !transport.onDispatchedRun) continue;
|
|
3283
|
+
try {
|
|
3284
|
+
await transport.dispatchRuns(runs, opts);
|
|
3285
|
+
return;
|
|
3286
|
+
} catch (err) {
|
|
3287
|
+
lastErr = err;
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
throw lastErr ?? new Error("no transport carries a run queue");
|
|
3291
|
+
}
|
|
3292
|
+
/**
|
|
3293
|
+
* Consume the run queue of every transport that carries one (a run may have been queued on any of
|
|
3294
|
+
* them after a failover). Returns one stop function per consumer.
|
|
3295
|
+
*/
|
|
3296
|
+
onDispatchedRun(handler, opts) {
|
|
3297
|
+
const stops = [];
|
|
3298
|
+
for (const { transport } of this.transports) {
|
|
3299
|
+
if (!transport.dispatchRuns || !transport.onDispatchedRun) continue;
|
|
3300
|
+
stops.push(transport.onDispatchedRun(handler, opts));
|
|
3301
|
+
}
|
|
3302
|
+
return stops;
|
|
3303
|
+
}
|
|
3095
3304
|
/**
|
|
3096
3305
|
* The live handshake descriptors advertised for `group` (design §7.2), merged across every transport
|
|
3097
3306
|
* that can report them. Returns `[]` when NO transport implements
|
|
@@ -3624,21 +3833,28 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3624
3833
|
return inputs.map((_, i) => i).slice(from, from + waves.waveSize);
|
|
3625
3834
|
}, "waveItems");
|
|
3626
3835
|
if (mode === "failFast") {
|
|
3627
|
-
|
|
3628
|
-
const cp = existing[
|
|
3629
|
-
const
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
]
|
|
3836
|
+
const failure = /* @__PURE__ */ __name((i2) => {
|
|
3837
|
+
const cp = existing[i2];
|
|
3838
|
+
const c2 = cp?.status === "completed" ? cp.output : null;
|
|
3839
|
+
return c2 && c2.ok === false ? c2 : null;
|
|
3840
|
+
}, "failure");
|
|
3841
|
+
const first = /* @__PURE__ */ __name((cancelled) => inputs.findIndex((_, i2) => {
|
|
3842
|
+
const c2 = failure(i2);
|
|
3843
|
+
return c2 !== null && c2.code === "cancelled" === cancelled;
|
|
3844
|
+
}), "first");
|
|
3845
|
+
const i = first(false) !== -1 ? first(false) : first(true);
|
|
3846
|
+
const c = i === -1 ? null : failure(i);
|
|
3847
|
+
if (c) {
|
|
3848
|
+
for (let j = 0; j < inputs.length; j += 1) {
|
|
3849
|
+
if (j !== i && existing[j]?.status !== "completed") host.cancelChild?.(id(j));
|
|
3641
3850
|
}
|
|
3851
|
+
throw new GatherError([
|
|
3852
|
+
{
|
|
3853
|
+
index: i,
|
|
3854
|
+
id: id(i),
|
|
3855
|
+
error: c.error ?? "unknown"
|
|
3856
|
+
}
|
|
3857
|
+
], inputs.length);
|
|
3642
3858
|
}
|
|
3643
3859
|
}
|
|
3644
3860
|
if (store.compactCheckpoints && inputs.length >= FOLD_MIN_FAN) {
|
|
@@ -3717,10 +3933,12 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3717
3933
|
await registerChildWaits(entries);
|
|
3718
3934
|
for (const i of chunk) host.startChild(name, inputs[i], id(i));
|
|
3719
3935
|
}
|
|
3720
|
-
|
|
3721
|
-
await
|
|
3722
|
-
|
|
3723
|
-
|
|
3936
|
+
if (toEnsure.length > 0) {
|
|
3937
|
+
const { runs: started, unread } = await readRunsByIds(store, toEnsure.map((i) => id(i)));
|
|
3938
|
+
const skip = new Set(unread);
|
|
3939
|
+
for (const i of toEnsure) {
|
|
3940
|
+
if (!started.has(id(i)) && !skip.has(id(i))) host.startChild(name, inputs[i], id(i));
|
|
3941
|
+
}
|
|
3724
3942
|
}
|
|
3725
3943
|
}
|
|
3726
3944
|
if (active.length > 0) {
|
|
@@ -4180,6 +4398,56 @@ var ANNOUNCED_ROUTES_TTL_MS = 5e3;
|
|
|
4180
4398
|
var VERSION_UNDECLARED_TAG = "version:undeclared";
|
|
4181
4399
|
var versionKey = /* @__PURE__ */ __name((name, version) => `${name}@${version}`, "versionKey");
|
|
4182
4400
|
var STEP_FAN_CHUNK = 200;
|
|
4401
|
+
var PARENT_NOTIFY_ATTEMPTS = 4;
|
|
4402
|
+
var PARENT_NOTIFY_BACKOFF_MS = 250;
|
|
4403
|
+
var LEASE_RECHECK_MAX_MS = 2e3;
|
|
4404
|
+
var LEASE_RECHECK_SLACK_MS = 100;
|
|
4405
|
+
var LEASE_RECHECK_ATTEMPTS = 30;
|
|
4406
|
+
function deferred() {
|
|
4407
|
+
let resolve;
|
|
4408
|
+
let reject;
|
|
4409
|
+
const promise = new Promise((res, rej) => {
|
|
4410
|
+
resolve = res;
|
|
4411
|
+
reject = rej;
|
|
4412
|
+
});
|
|
4413
|
+
promise.catch(() => void 0);
|
|
4414
|
+
return {
|
|
4415
|
+
promise,
|
|
4416
|
+
resolve,
|
|
4417
|
+
reject
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4420
|
+
__name(deferred, "deferred");
|
|
4421
|
+
var pendingStateKey = /* @__PURE__ */ __name((at) => String(new Date(at).getTime()), "pendingStateKey");
|
|
4422
|
+
var freshPendingKey = /* @__PURE__ */ __name(() => `${Date.now()}-${globalThis.crypto.randomUUID().slice(0, 8)}`, "freshPendingKey");
|
|
4423
|
+
var CHILD_SIGNAL_PREFIX = "signal:child:";
|
|
4424
|
+
var DELIVER_CHUNK = 50;
|
|
4425
|
+
function childCompletion(child) {
|
|
4426
|
+
if (child.status === "completed") return {
|
|
4427
|
+
ok: true,
|
|
4428
|
+
value: child.output
|
|
4429
|
+
};
|
|
4430
|
+
if (child.status === "failed") return failedCompletion(child.error ?? {
|
|
4431
|
+
message: "failed"
|
|
4432
|
+
});
|
|
4433
|
+
if (child.status === "dead") return failedCompletion(child.error ?? {
|
|
4434
|
+
message: "dead"
|
|
4435
|
+
});
|
|
4436
|
+
if (child.status === "cancelled") return cancelledCompletion(child.error);
|
|
4437
|
+
return void 0;
|
|
4438
|
+
}
|
|
4439
|
+
__name(childCompletion, "childCompletion");
|
|
4440
|
+
var CHILD_CANCELLED = "cancelled";
|
|
4441
|
+
function cancelledCompletion(error2) {
|
|
4442
|
+
const base = error2 ?? {
|
|
4443
|
+
message: "cancelled"
|
|
4444
|
+
};
|
|
4445
|
+
return failedCompletion({
|
|
4446
|
+
...base,
|
|
4447
|
+
code: base.code ?? CHILD_CANCELLED
|
|
4448
|
+
});
|
|
4449
|
+
}
|
|
4450
|
+
__name(cancelledCompletion, "cancelledCompletion");
|
|
4183
4451
|
var RESULT_BATCH_MAX = 256;
|
|
4184
4452
|
function shuffle(items) {
|
|
4185
4453
|
for (let i = items.length - 1; i > 0; i -= 1) {
|
|
@@ -4356,6 +4624,35 @@ var WorkflowEngine = class {
|
|
|
4356
4624
|
/** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
|
|
4357
4625
|
inflight = /* @__PURE__ */ new Set();
|
|
4358
4626
|
/**
|
|
4627
|
+
* Work a settled turn leaves running in the background (a parent notification, a post-release
|
|
4628
|
+
* re-drive), so a graceful shutdown waits for it too: dropping one on exit strands the run until
|
|
4629
|
+
* its reconcile timer.
|
|
4630
|
+
*/
|
|
4631
|
+
background = /* @__PURE__ */ new Set();
|
|
4632
|
+
/**
|
|
4633
|
+
* Runs with an implicit wake executing on this instance, and whether another wake arrived for the
|
|
4634
|
+
* run meanwhile (see {@link resumeImplicit}).
|
|
4635
|
+
*/
|
|
4636
|
+
wakeTurns = /* @__PURE__ */ new Map();
|
|
4637
|
+
/** Re-checks scheduled for runs whose wake found another instance's lease (see {@link execute}). */
|
|
4638
|
+
leaseRetries = /* @__PURE__ */ new Map();
|
|
4639
|
+
/**
|
|
4640
|
+
* Every run turn and child start this instance takes on itself, under one budget of
|
|
4641
|
+
* {@link LOCAL_RUN_CONCURRENCY}: the runs it dispatches to itself, a fan's deferred child starts,
|
|
4642
|
+
* enqueued pickups and the runs it takes off the transport's run queue.
|
|
4643
|
+
*/
|
|
4644
|
+
localWork;
|
|
4645
|
+
/** The dispatcher the caller supplied, if any; without one the engine picks (see {@link dispatchRun}). */
|
|
4646
|
+
customDispatcher;
|
|
4647
|
+
/** Runs on their way to the transport's run queue (see {@link consumeRuns}). */
|
|
4648
|
+
runBatcher;
|
|
4649
|
+
/** Set while this engine consumes a transport's run queue, and so dispatches its runs to it. */
|
|
4650
|
+
dispatchesOverTransport = false;
|
|
4651
|
+
/** Stops for the run-queue consumers started by {@link consumeRuns}. */
|
|
4652
|
+
runConsumerStops = [];
|
|
4653
|
+
/** The consumers being stopped, so {@link drain} waits for their runs in flight. */
|
|
4654
|
+
runConsumersStopping;
|
|
4655
|
+
/**
|
|
4359
4656
|
* The run lease of each execution in flight on THIS instance, by run id — renewed while it runs
|
|
4360
4657
|
* and checked by every settle (the zombie fence, see {@link ExecutionLease}). Also what `drain()`
|
|
4361
4658
|
* releases when its budget runs out.
|
|
@@ -4411,15 +4708,27 @@ var WorkflowEngine = class {
|
|
|
4411
4708
|
this.rehydrate = deps.rehydrate ?? ((_carrier, fn) => fn());
|
|
4412
4709
|
this.compensationRetries = Math.max(1, deps.compensationRetries ?? 1);
|
|
4413
4710
|
this.trackStepStart = deps.trackStepStart ?? true;
|
|
4414
|
-
this.
|
|
4415
|
-
|
|
4416
|
-
|
|
4711
|
+
this.localWork = new BoundedRunner(LOCAL_RUN_CONCURRENCY);
|
|
4712
|
+
this.customDispatcher = deps.runDispatcher;
|
|
4713
|
+
this.runBatcher = new RunDispatchBatcher((runs) => this.pool.dispatchRuns(runs, {
|
|
4714
|
+
namespace: this.namespace
|
|
4715
|
+
}), (runs, err) => {
|
|
4716
|
+
for (const run of runs) {
|
|
4717
|
+
if (run.reason === "wake" && !this.draining) {
|
|
4718
|
+
this.inBackground(this.resumeImplicit(run.runId));
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
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);
|
|
4722
|
+
});
|
|
4723
|
+
this.runDispatcher = {
|
|
4724
|
+
dispatch: /* @__PURE__ */ __name((runId) => this.dispatchRun(runId), "dispatch")
|
|
4417
4725
|
};
|
|
4418
4726
|
this.singletons = new SingletonGate({
|
|
4419
4727
|
store: this.store,
|
|
4420
4728
|
clock: this.clock,
|
|
4421
4729
|
dispatch: /* @__PURE__ */ __name((runId) => {
|
|
4422
|
-
this.
|
|
4730
|
+
if (this.queuesRunsOverTransport) void this.wake(runId);
|
|
4731
|
+
else this.runDispatcher.dispatch(runId);
|
|
4423
4732
|
}, "dispatch"),
|
|
4424
4733
|
configFor: /* @__PURE__ */ __name((run) => (this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? this.latest.get(run.workflow))?.singleton, "configFor")
|
|
4425
4734
|
});
|
|
@@ -4544,13 +4853,224 @@ var WorkflowEngine = class {
|
|
|
4544
4853
|
}
|
|
4545
4854
|
/**
|
|
4546
4855
|
* Be notified when a run is enqueued on ANOTHER instance (via the control plane), so a worker can
|
|
4547
|
-
* pick it up at once — e.g. `engine.onEnqueued((runId) => engine.
|
|
4856
|
+
* pick it up at once — e.g. `engine.onEnqueued((runId) => engine.runEnqueued(runId))`. Returns an
|
|
4548
4857
|
* unsubscribe function. Only wire this on instances that should execute runs (workers).
|
|
4549
4858
|
*/
|
|
4550
4859
|
onEnqueued(listener) {
|
|
4551
4860
|
this.enqueuedListeners.add(listener);
|
|
4552
4861
|
return () => this.enqueuedListeners.delete(listener);
|
|
4553
4862
|
}
|
|
4863
|
+
/**
|
|
4864
|
+
* Pick up a run another instance announced as enqueued: run it here if it is still `pending`.
|
|
4865
|
+
* Bounded by {@link LOCAL_RUN_CONCURRENCY}, and a run another instance already took is skipped
|
|
4866
|
+
* after one read, instead of being leased and replayed here once more.
|
|
4867
|
+
*/
|
|
4868
|
+
runEnqueued(runId) {
|
|
4869
|
+
this.localWork.submit(`run:${runId}`, () => this.runIfPending(runId));
|
|
4870
|
+
}
|
|
4871
|
+
/**
|
|
4872
|
+
* Consume the run queue of every transport that carries one ({@link Transport.dispatchRuns} and
|
|
4873
|
+
* {@link Transport.onDispatchedRun}), and from then on dispatch the runs this engine starts there
|
|
4874
|
+
* rather than executing them in-process: a run, a `ctx.all` fan's children included, executes on
|
|
4875
|
+
* whichever consuming instance takes it, at most {@link LOCAL_RUN_CONCURRENCY} at once per
|
|
4876
|
+
* instance. Call it on every instance that drives runs; the Nest module does on a driving operator.
|
|
4877
|
+
*
|
|
4878
|
+
* Returns a function that stops consuming and resolves once the runs in flight settled, after
|
|
4879
|
+
* which runs are executed in-process again; {@link beginDrain} stops consuming too. Undefined
|
|
4880
|
+
* when no transport carries a run queue: runs keep executing in-process.
|
|
4881
|
+
*/
|
|
4882
|
+
consumeRuns() {
|
|
4883
|
+
if (!this.pool.dispatchesRuns) return void 0;
|
|
4884
|
+
if (this.runConsumerStops.length === 0) {
|
|
4885
|
+
this.runConsumerStops = this.pool.onDispatchedRun((run) => this.localWork.run(() => this.runFromQueue(run)), {
|
|
4886
|
+
concurrency: LOCAL_RUN_CONCURRENCY,
|
|
4887
|
+
namespace: this.namespace
|
|
4888
|
+
});
|
|
4889
|
+
}
|
|
4890
|
+
this.dispatchesOverTransport = true;
|
|
4891
|
+
return async () => {
|
|
4892
|
+
this.dispatchesOverTransport = false;
|
|
4893
|
+
await this.runBatcher.settle();
|
|
4894
|
+
await this.stopRunConsumers();
|
|
4895
|
+
};
|
|
4896
|
+
}
|
|
4897
|
+
/** Stop every run-queue consumer; resolves once their runs in flight settled. */
|
|
4898
|
+
stopRunConsumers() {
|
|
4899
|
+
if (this.runConsumerStops.length > 0) {
|
|
4900
|
+
const stops = this.runConsumerStops;
|
|
4901
|
+
this.runConsumerStops = [];
|
|
4902
|
+
this.runConsumersStopping = Promise.allSettled(stops.map((stop) => stop()));
|
|
4903
|
+
}
|
|
4904
|
+
return this.runConsumersStopping ?? Promise.resolve();
|
|
4905
|
+
}
|
|
4906
|
+
/** Whether {@link dispatchRun} queues runs on the transport (see {@link consumeRuns}). */
|
|
4907
|
+
get queuesRunsOverTransport() {
|
|
4908
|
+
return this.customDispatcher === void 0 && this.dispatchesOverTransport;
|
|
4909
|
+
}
|
|
4910
|
+
/**
|
|
4911
|
+
* Hand a `pending` run to whatever executes it: the caller's {@link WorkflowEngineDeps.runDispatcher};
|
|
4912
|
+
* else the transport's run queue while this engine consumes it, batched; else this instance,
|
|
4913
|
+
* bounded by {@link LOCAL_RUN_CONCURRENCY}. A run whose queueing fails stays `pending` for a
|
|
4914
|
+
* `runPending` poll.
|
|
4915
|
+
*/
|
|
4916
|
+
dispatchRun(runId, priority, key) {
|
|
4917
|
+
if (this.customDispatcher) return this.customDispatcher.dispatch(runId);
|
|
4918
|
+
if (this.dispatchesOverTransport) {
|
|
4919
|
+
this.runBatcher.add({
|
|
4920
|
+
runId,
|
|
4921
|
+
reason: "start",
|
|
4922
|
+
...priority !== void 0 ? {
|
|
4923
|
+
priority
|
|
4924
|
+
} : {},
|
|
4925
|
+
...key !== void 0 ? {
|
|
4926
|
+
key
|
|
4927
|
+
} : {}
|
|
4928
|
+
});
|
|
4929
|
+
return;
|
|
4930
|
+
}
|
|
4931
|
+
this.localWork.submit(`run:${runId}`, () => this.runOne(runId));
|
|
4932
|
+
}
|
|
4933
|
+
/**
|
|
4934
|
+
* Wake a suspended run that has something new to see (a delivered signal or child completion, a
|
|
4935
|
+
* lease that came free, a freed slot): while this engine consumes the transport's run queue, on
|
|
4936
|
+
* whichever instance takes the wake (the result then only says it was handed over); otherwise
|
|
4937
|
+
* here, now.
|
|
4938
|
+
*/
|
|
4939
|
+
wake(runId) {
|
|
4940
|
+
if (this.queuesRunsOverTransport) {
|
|
4941
|
+
this.runBatcher.add({
|
|
4942
|
+
runId,
|
|
4943
|
+
reason: "wake"
|
|
4944
|
+
});
|
|
4945
|
+
return Promise.resolve({
|
|
4946
|
+
runId,
|
|
4947
|
+
status: "suspended"
|
|
4948
|
+
});
|
|
4949
|
+
}
|
|
4950
|
+
return this.resumeImplicit(runId);
|
|
4951
|
+
}
|
|
4952
|
+
/**
|
|
4953
|
+
* Take a turn of a run taken off the transport's run queue. A `start` runs only while the run is
|
|
4954
|
+
* still `pending` (or `cancelling`, a recovered cancel): any other state means another instance
|
|
4955
|
+
* already took it. A `wake` leases and resumes the run unless it is finished. The run lease admits
|
|
4956
|
+
* one executor either way. Never rejects.
|
|
4957
|
+
*/
|
|
4958
|
+
async runFromQueue(dispatch) {
|
|
4959
|
+
if (this.draining) return;
|
|
4960
|
+
const { runId } = dispatch;
|
|
4961
|
+
try {
|
|
4962
|
+
const run = await this.store.getRun(runId);
|
|
4963
|
+
if (!run) return;
|
|
4964
|
+
if (run.status === "pending" || run.status === "cancelling") {
|
|
4965
|
+
await this.runOne(runId);
|
|
4966
|
+
return;
|
|
4967
|
+
}
|
|
4968
|
+
if (dispatch.reason !== "wake" || TERMINAL_RUN_STATUSES.includes(run.status)) return;
|
|
4969
|
+
const nowMs = this.clock();
|
|
4970
|
+
if (run.wakeAt !== void 0 && run.wakeAt <= nowMs) {
|
|
4971
|
+
const waiters = indexWaitersByRun(await listSignalWaitersOfRuns(this.store, [
|
|
4972
|
+
runId
|
|
4973
|
+
]));
|
|
4974
|
+
const resumed = await this.resumeLeased([
|
|
4975
|
+
run
|
|
4976
|
+
], nowMs, (locked) => this.reconcileSignalWaiter(locked, waiters.get(locked.id)));
|
|
4977
|
+
if (resumed.length === 0) this.recheckAfterLease(runId, run.lockedUntil);
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4980
|
+
await this.resumeImplicit(runId).catch((err) => {
|
|
4981
|
+
if (!(err instanceof NamespaceMismatch)) throw err;
|
|
4982
|
+
});
|
|
4983
|
+
} catch (error2) {
|
|
4984
|
+
console.error(`[nestjs-durable] could not run queued run ${runId}:`, error2);
|
|
4985
|
+
}
|
|
4986
|
+
}
|
|
4987
|
+
/**
|
|
4988
|
+
* Hand due runs (timers, blocked runs due for a fleet re-check) to whichever instance takes them
|
|
4989
|
+
* off the run queue. Each is claimed first, under its lease, by pushing its `wakeAt` one lease
|
|
4990
|
+
* period out, so the next poll (here or elsewhere) does not queue it again while it waits; a wake
|
|
4991
|
+
* that is lost brings it back due after that period. False when the transport refused the wakes,
|
|
4992
|
+
* so the caller resumes them itself.
|
|
4993
|
+
*/
|
|
4994
|
+
async handOffDue(runs, nowMs) {
|
|
4995
|
+
const claimed = [];
|
|
4996
|
+
for (const run of runs) {
|
|
4997
|
+
if (this.draining) break;
|
|
4998
|
+
if (!await this.store.tryLockRun(run.id, this.instanceId, nowMs + this.leaseMs, nowMs)) {
|
|
4999
|
+
continue;
|
|
5000
|
+
}
|
|
5001
|
+
try {
|
|
5002
|
+
const patch = {
|
|
5003
|
+
wakeAt: nowMs + this.leaseMs
|
|
5004
|
+
};
|
|
5005
|
+
if (this.store.updateRunIf) {
|
|
5006
|
+
if (await this.store.updateRunIf(run.id, [
|
|
5007
|
+
run.status
|
|
5008
|
+
], patch)) claimed.push(run);
|
|
5009
|
+
} else {
|
|
5010
|
+
await this.store.updateRun(run.id, patch);
|
|
5011
|
+
claimed.push(run);
|
|
5012
|
+
}
|
|
5013
|
+
} finally {
|
|
5014
|
+
await this.store.releaseRunLock(run.id);
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
try {
|
|
5018
|
+
for (let at = 0; at < claimed.length; at += RUN_DISPATCH_BATCH) {
|
|
5019
|
+
await this.pool.dispatchRuns(claimed.slice(at, at + RUN_DISPATCH_BATCH).map((run) => ({
|
|
5020
|
+
runId: run.id,
|
|
5021
|
+
reason: "wake",
|
|
5022
|
+
...run.priority !== void 0 ? {
|
|
5023
|
+
priority: run.priority
|
|
5024
|
+
} : {}
|
|
5025
|
+
})), {
|
|
5026
|
+
namespace: this.namespace
|
|
5027
|
+
});
|
|
5028
|
+
}
|
|
5029
|
+
return true;
|
|
5030
|
+
} catch (error2) {
|
|
5031
|
+
console.error("[nestjs-durable] could not queue due runs on the transport:", error2);
|
|
5032
|
+
return false;
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
/**
|
|
5036
|
+
* Queue `runs` on the transport for whichever consuming instance takes them, in batches. False
|
|
5037
|
+
* when the transport refused them, so the caller executes them itself instead.
|
|
5038
|
+
*/
|
|
5039
|
+
async queueOnTransport(runs) {
|
|
5040
|
+
try {
|
|
5041
|
+
for (let at = 0; at < runs.length; at += RUN_DISPATCH_BATCH) {
|
|
5042
|
+
await this.pool.dispatchRuns(runs.slice(at, at + RUN_DISPATCH_BATCH).map((run) => ({
|
|
5043
|
+
runId: run.id,
|
|
5044
|
+
reason: "start",
|
|
5045
|
+
key: pendingStateKey(run.updatedAt),
|
|
5046
|
+
...run.priority !== void 0 ? {
|
|
5047
|
+
priority: run.priority
|
|
5048
|
+
} : {}
|
|
5049
|
+
})), {
|
|
5050
|
+
namespace: this.namespace
|
|
5051
|
+
});
|
|
5052
|
+
}
|
|
5053
|
+
return true;
|
|
5054
|
+
} catch (error2) {
|
|
5055
|
+
console.error("[nestjs-durable] could not queue runs on the transport; running them here:", error2);
|
|
5056
|
+
return false;
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
/**
|
|
5060
|
+
* Execute a run announced by another instance, if it is still `pending`: a
|
|
5061
|
+
* run another instance already took is skipped after one read instead of being leased and
|
|
5062
|
+
* replayed here once more. Never rejects.
|
|
5063
|
+
*/
|
|
5064
|
+
async runIfPending(runId) {
|
|
5065
|
+
if (this.draining) return;
|
|
5066
|
+
try {
|
|
5067
|
+
const run = await this.store.getRun(runId);
|
|
5068
|
+
if (run?.status !== "pending") return;
|
|
5069
|
+
await this.runOne(runId);
|
|
5070
|
+
} catch (error2) {
|
|
5071
|
+
console.error(`[nestjs-durable] could not run enqueued run ${runId}:`, error2);
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
4554
5074
|
/** Fire cooperative-cancellation listeners for `runId` (a worker bridge aborts in-flight work). */
|
|
4555
5075
|
notifyCancelled(runId) {
|
|
4556
5076
|
for (const fn of this.cancelListeners) {
|
|
@@ -4946,8 +5466,8 @@ var WorkflowEngine = class {
|
|
|
4946
5466
|
status: "suspended"
|
|
4947
5467
|
};
|
|
4948
5468
|
}
|
|
4949
|
-
await this.
|
|
4950
|
-
if (this.controlPlane) {
|
|
5469
|
+
await this.dispatchRun(runId, run.priority, pendingStateKey(run.updatedAt));
|
|
5470
|
+
if (this.controlPlane && !this.queuesRunsOverTransport) {
|
|
4951
5471
|
void this.controlPlane.publishControl({
|
|
4952
5472
|
kind: "enqueued",
|
|
4953
5473
|
runId,
|
|
@@ -4986,7 +5506,43 @@ var WorkflowEngine = class {
|
|
|
4986
5506
|
* stale waiter or a late result left behind by a failed run silently re-executes its body.
|
|
4987
5507
|
*/
|
|
4988
5508
|
resumeImplicit(runId) {
|
|
4989
|
-
|
|
5509
|
+
const turn = this.wakeTurns.get(runId);
|
|
5510
|
+
if (turn) {
|
|
5511
|
+
turn.again ??= deferred();
|
|
5512
|
+
if (currentWorkflowCtx()?.runId === runId) {
|
|
5513
|
+
return Promise.resolve({
|
|
5514
|
+
runId,
|
|
5515
|
+
status: "running"
|
|
5516
|
+
});
|
|
5517
|
+
}
|
|
5518
|
+
return turn.again.promise;
|
|
5519
|
+
}
|
|
5520
|
+
const mine = {};
|
|
5521
|
+
this.wakeTurns.set(runId, mine);
|
|
5522
|
+
const finish = /* @__PURE__ */ __name(() => {
|
|
5523
|
+
if (this.wakeTurns.get(runId) === mine) this.wakeTurns.delete(runId);
|
|
5524
|
+
const again = mine.again;
|
|
5525
|
+
if (!again) return;
|
|
5526
|
+
if (this.draining) {
|
|
5527
|
+
again.resolve({
|
|
5528
|
+
runId,
|
|
5529
|
+
status: "running"
|
|
5530
|
+
});
|
|
5531
|
+
return;
|
|
5532
|
+
}
|
|
5533
|
+
const next = this.resumeImplicit(runId);
|
|
5534
|
+
next.then(again.resolve, again.reject);
|
|
5535
|
+
this.inBackground(next);
|
|
5536
|
+
}, "finish");
|
|
5537
|
+
const result = this.resumeRun(runId, false);
|
|
5538
|
+
void result.then(finish, finish);
|
|
5539
|
+
return result;
|
|
5540
|
+
}
|
|
5541
|
+
/** Keep `work` in {@link background} until it settles; its failure is swallowed. */
|
|
5542
|
+
inBackground(work) {
|
|
5543
|
+
const tracked = work.catch(() => void 0);
|
|
5544
|
+
this.background.add(tracked);
|
|
5545
|
+
void tracked.then(() => this.background.delete(tracked));
|
|
4990
5546
|
}
|
|
4991
5547
|
/**
|
|
4992
5548
|
* One execution of a run at a time in this process. A resume that lands while the run executes
|
|
@@ -5273,6 +5829,10 @@ var WorkflowEngine = class {
|
|
|
5273
5829
|
*/
|
|
5274
5830
|
beginDrain(spentMs = 0) {
|
|
5275
5831
|
this.draining = true;
|
|
5832
|
+
if (this.runConsumerStops.length > 0) {
|
|
5833
|
+
this.runBatcher.flush();
|
|
5834
|
+
void this.stopRunConsumers();
|
|
5835
|
+
}
|
|
5276
5836
|
if (Number.isFinite(spentMs) && spentMs > 0) {
|
|
5277
5837
|
this.drainSpentMs += spentMs;
|
|
5278
5838
|
for (const rearm of this.drainRearms) rearm();
|
|
@@ -5293,7 +5853,14 @@ var WorkflowEngine = class {
|
|
|
5293
5853
|
*/
|
|
5294
5854
|
async drain(timeoutMs = 1e4) {
|
|
5295
5855
|
this.beginDrain();
|
|
5296
|
-
|
|
5856
|
+
this.localWork.clear();
|
|
5857
|
+
for (const timer2 of this.leaseRetries.values()) clearTimeout(timer2);
|
|
5858
|
+
this.leaseRetries.clear();
|
|
5859
|
+
const consumers = this.runConsumersStopping;
|
|
5860
|
+
if (this.inflight.size === 0 && this.background.size === 0 && !consumers && this.runBatcher.idle) {
|
|
5861
|
+
return;
|
|
5862
|
+
}
|
|
5863
|
+
const queued = this.runBatcher.settle();
|
|
5297
5864
|
const start = Date.now();
|
|
5298
5865
|
let rearm = /* @__PURE__ */ __name(() => {
|
|
5299
5866
|
}, "rearm");
|
|
@@ -5312,7 +5879,10 @@ var WorkflowEngine = class {
|
|
|
5312
5879
|
try {
|
|
5313
5880
|
await Promise.race([
|
|
5314
5881
|
Promise.allSettled([
|
|
5315
|
-
...this.inflight
|
|
5882
|
+
...this.inflight,
|
|
5883
|
+
...this.background,
|
|
5884
|
+
queued,
|
|
5885
|
+
consumers
|
|
5316
5886
|
]).then(() => {
|
|
5317
5887
|
settled = true;
|
|
5318
5888
|
}),
|
|
@@ -5419,6 +5989,7 @@ var WorkflowEngine = class {
|
|
|
5419
5989
|
error: error2
|
|
5420
5990
|
});
|
|
5421
5991
|
await this.cancelChildren(run.id).catch(() => void 0);
|
|
5992
|
+
this.notifyParent(run.id, cancelledCompletion(error2));
|
|
5422
5993
|
}
|
|
5423
5994
|
}
|
|
5424
5995
|
}
|
|
@@ -5433,7 +6004,9 @@ var WorkflowEngine = class {
|
|
|
5433
6004
|
if (this.draining) break;
|
|
5434
6005
|
const outcome = this.isOwnLiveLease(listed, nowMs) ? await this.reclaimOwnLease(listed, nowMs) : await this.recoverOrphan(listed, nowMs);
|
|
5435
6006
|
if (!outcome) continue;
|
|
5436
|
-
if (outcome.dispatch)
|
|
6007
|
+
if (outcome.dispatch) {
|
|
6008
|
+
await this.dispatchRun(outcome.result.runId, listed.priority, freshPendingKey());
|
|
6009
|
+
}
|
|
5437
6010
|
results.push(outcome.result);
|
|
5438
6011
|
}
|
|
5439
6012
|
return results;
|
|
@@ -5625,6 +6198,18 @@ var WorkflowEngine = class {
|
|
|
5625
6198
|
const due = shuffle(await this.store.listDueTimers(nowMs, this.namespace, this.timerBatchSize));
|
|
5626
6199
|
const blocked = await this.dueBlockedRuns(nowMs);
|
|
5627
6200
|
if (due.length === 0 && blocked.length === 0) return [];
|
|
6201
|
+
if (this.queuesRunsOverTransport && !this.draining) {
|
|
6202
|
+
const runs = [
|
|
6203
|
+
...due,
|
|
6204
|
+
...blocked
|
|
6205
|
+
];
|
|
6206
|
+
if (await this.handOffDue(runs, nowMs)) {
|
|
6207
|
+
return runs.map((run) => ({
|
|
6208
|
+
runId: run.id,
|
|
6209
|
+
status: run.status
|
|
6210
|
+
}));
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
5628
6213
|
const waiterByRun = indexWaitersByRun(await listSignalWaitersOfRuns(this.store, [
|
|
5629
6214
|
...due,
|
|
5630
6215
|
...blocked
|
|
@@ -5658,7 +6243,7 @@ var WorkflowEngine = class {
|
|
|
5658
6243
|
const buffered = await this.store.takeBufferedSignal(waiter.token);
|
|
5659
6244
|
if (!buffered) return void 0;
|
|
5660
6245
|
await this.store.removeSignalWaiter(waiter);
|
|
5661
|
-
const settled = await this.deliverSignal(waiter, buffered.payload);
|
|
6246
|
+
const settled = await this.deliverSignal(waiter, buffered.payload, true);
|
|
5662
6247
|
return settled ?? {
|
|
5663
6248
|
runId: run.id,
|
|
5664
6249
|
status: run.status
|
|
@@ -5684,7 +6269,7 @@ var WorkflowEngine = class {
|
|
|
5684
6269
|
if (!eventMatches(candidate.payload, match)) continue;
|
|
5685
6270
|
if (!await this.store.removeBufferedEvent(candidate.id)) continue;
|
|
5686
6271
|
await this.store.removeSignalWaiter(waiter);
|
|
5687
|
-
const settled = await this.deliverSignal(waiter, candidate.payload);
|
|
6272
|
+
const settled = await this.deliverSignal(waiter, candidate.payload, true);
|
|
5688
6273
|
return settled ?? {
|
|
5689
6274
|
runId: run.id,
|
|
5690
6275
|
status: run.status
|
|
@@ -5804,7 +6389,7 @@ var WorkflowEngine = class {
|
|
|
5804
6389
|
} : {},
|
|
5805
6390
|
updatedAt: /* @__PURE__ */ new Date()
|
|
5806
6391
|
});
|
|
5807
|
-
await this.
|
|
6392
|
+
await this.dispatchRun(runId, run.priority, freshPendingKey());
|
|
5808
6393
|
return {
|
|
5809
6394
|
runId,
|
|
5810
6395
|
status: "pending"
|
|
@@ -5879,7 +6464,16 @@ var WorkflowEngine = class {
|
|
|
5879
6464
|
* {@link resumeDueTimers}.
|
|
5880
6465
|
*/
|
|
5881
6466
|
async runPending(nowMs = this.clock()) {
|
|
5882
|
-
|
|
6467
|
+
const pending = await this.store.listPendingRuns(100, this.namespace);
|
|
6468
|
+
if (this.queuesRunsOverTransport && !this.draining && pending.length > 0) {
|
|
6469
|
+
if (await this.queueOnTransport(pending)) {
|
|
6470
|
+
return pending.map((run) => ({
|
|
6471
|
+
runId: run.id,
|
|
6472
|
+
status: run.status
|
|
6473
|
+
}));
|
|
6474
|
+
}
|
|
6475
|
+
}
|
|
6476
|
+
return this.resumeLeased(pending, nowMs);
|
|
5883
6477
|
}
|
|
5884
6478
|
/**
|
|
5885
6479
|
* Resolve once `runId` reaches a settled state — terminal (completed/failed/cancelled/dead) or
|
|
@@ -6056,7 +6650,7 @@ var WorkflowEngine = class {
|
|
|
6056
6650
|
* due-timer reconcile sweep ({@link reconcileSignalWaiter}) — all three resolve a waiter the exact
|
|
6057
6651
|
* same way.
|
|
6058
6652
|
*/
|
|
6059
|
-
async deliverSignal(waiter, payload) {
|
|
6653
|
+
async deliverSignal(waiter, payload, resumeHere = false) {
|
|
6060
6654
|
await this.store.saveCheckpoint(instantCheckpoint({
|
|
6061
6655
|
runId: waiter.runId,
|
|
6062
6656
|
seq: waiter.seq,
|
|
@@ -6075,7 +6669,7 @@ var WorkflowEngine = class {
|
|
|
6075
6669
|
status: "suspended"
|
|
6076
6670
|
};
|
|
6077
6671
|
}
|
|
6078
|
-
return this.resumeImplicit(waiter.runId).catch((err) => {
|
|
6672
|
+
return (resumeHere ? this.resumeImplicit(waiter.runId) : this.wake(waiter.runId)).catch((err) => {
|
|
6079
6673
|
if (err instanceof NamespaceMismatch) return null;
|
|
6080
6674
|
throw err;
|
|
6081
6675
|
});
|
|
@@ -6162,6 +6756,101 @@ var WorkflowEngine = class {
|
|
|
6162
6756
|
return;
|
|
6163
6757
|
}
|
|
6164
6758
|
}
|
|
6759
|
+
/**
|
|
6760
|
+
* Deliver the outcome of every awaited child that already finished but whose completion never
|
|
6761
|
+
* reached this run, into `checkpoints` (this turn's snapshot) and the store.
|
|
6762
|
+
*
|
|
6763
|
+
* A child persists its terminal status first and tells its parent after ({@link notifyParent}). A
|
|
6764
|
+
* crash, a scale-down or a store error between the two leaves the parent's `running` placeholder
|
|
6765
|
+
* and `child:<id>` waiter behind for a child that is done, and nothing else would ever settle
|
|
6766
|
+
* them: the parent's reconcile wake replays straight back to the same wait. So each turn looks up
|
|
6767
|
+
* the children of its `running` placeholders (one batched read per 500) and settles the finished
|
|
6768
|
+
* ones exactly as the child's own notification would have: the `signal:child:<id>` checkpoint
|
|
6769
|
+
* with the child's completion, then the waiter removed. A notification arriving late writes the
|
|
6770
|
+
* same checkpoint and finds no waiter. Best effort: a store error leaves the rest to the next turn.
|
|
6771
|
+
*/
|
|
6772
|
+
async deliverFinishedChildren(runId, checkpoints) {
|
|
6773
|
+
const awaited = [];
|
|
6774
|
+
for (const cp of checkpoints) {
|
|
6775
|
+
if (cp.kind !== "signal" || cp.status !== "running") continue;
|
|
6776
|
+
if (!cp.name.startsWith(CHILD_SIGNAL_PREFIX)) continue;
|
|
6777
|
+
awaited.push({
|
|
6778
|
+
childId: cp.name.slice(CHILD_SIGNAL_PREFIX.length),
|
|
6779
|
+
seq: cp.seq,
|
|
6780
|
+
parallelGroup: cp.parallelGroup
|
|
6781
|
+
});
|
|
6782
|
+
}
|
|
6783
|
+
await this.settleFinishedChildren(runId, awaited, checkpoints);
|
|
6784
|
+
}
|
|
6785
|
+
/**
|
|
6786
|
+
* {@link deliverFinishedChildren} for a remote workflow, whose awaited children (`startChild`
|
|
6787
|
+
* commands) leave a `child:<id>` waiter but no placeholder checkpoint: the run's own waiters say
|
|
6788
|
+
* which children it is waiting on. One indexed read per turn of a remote run.
|
|
6789
|
+
*/
|
|
6790
|
+
async deliverFinishedChildWaiters(runId, checkpoints) {
|
|
6791
|
+
let waiters;
|
|
6792
|
+
try {
|
|
6793
|
+
waiters = await listSignalWaitersOfRuns(this.store, [
|
|
6794
|
+
runId
|
|
6795
|
+
]);
|
|
6796
|
+
} catch {
|
|
6797
|
+
return;
|
|
6798
|
+
}
|
|
6799
|
+
const settled = new Set(checkpoints.filter((cp) => cp.status === "completed").map((cp) => cp.seq));
|
|
6800
|
+
const awaited = waiters.filter((w) => w.token.startsWith("child:") && !settled.has(w.seq)).map((w) => ({
|
|
6801
|
+
childId: w.token.slice("child:".length),
|
|
6802
|
+
seq: w.seq,
|
|
6803
|
+
parallelGroup: w.parallelGroup
|
|
6804
|
+
}));
|
|
6805
|
+
await this.settleFinishedChildren(runId, awaited, checkpoints);
|
|
6806
|
+
}
|
|
6807
|
+
/**
|
|
6808
|
+
* Settle every child in `awaited` that has finished: its `signal:child:<id>` checkpoint with the
|
|
6809
|
+
* child's completion, then its waiter removed, exactly as the child's own notification does, and
|
|
6810
|
+
* the checkpoint put into `checkpoints`. Children are read in batches of 500.
|
|
6811
|
+
*/
|
|
6812
|
+
async settleFinishedChildren(runId, awaited, checkpoints) {
|
|
6813
|
+
if (awaited.length === 0) return;
|
|
6814
|
+
try {
|
|
6815
|
+
const { runs: children } = await readRunsByIds(this.store, awaited.map((a) => a.childId));
|
|
6816
|
+
const finished = awaited.flatMap((a) => {
|
|
6817
|
+
const child = children.get(a.childId);
|
|
6818
|
+
const completion = child ? childCompletion(child) : void 0;
|
|
6819
|
+
return completion ? [
|
|
6820
|
+
{
|
|
6821
|
+
...a,
|
|
6822
|
+
completion
|
|
6823
|
+
}
|
|
6824
|
+
] : [];
|
|
6825
|
+
});
|
|
6826
|
+
for (let at = 0; at < finished.length; at += DELIVER_CHUNK) {
|
|
6827
|
+
await Promise.all(finished.slice(at, at + DELIVER_CHUNK).map(async (a) => {
|
|
6828
|
+
const settled = instantCheckpoint({
|
|
6829
|
+
runId,
|
|
6830
|
+
seq: a.seq,
|
|
6831
|
+
name: `${CHILD_SIGNAL_PREFIX}${a.childId}`,
|
|
6832
|
+
kind: "signal",
|
|
6833
|
+
output: a.completion,
|
|
6834
|
+
parallelGroup: a.parallelGroup
|
|
6835
|
+
});
|
|
6836
|
+
await this.store.saveCheckpoint(settled);
|
|
6837
|
+
await this.store.removeSignalWaiter({
|
|
6838
|
+
token: `child:${a.childId}`,
|
|
6839
|
+
runId,
|
|
6840
|
+
seq: a.seq,
|
|
6841
|
+
...a.parallelGroup !== void 0 ? {
|
|
6842
|
+
parallelGroup: a.parallelGroup
|
|
6843
|
+
} : {}
|
|
6844
|
+
});
|
|
6845
|
+
const index = checkpoints.findIndex((cp) => cp.seq === a.seq);
|
|
6846
|
+
if (index === -1) checkpoints.push(settled);
|
|
6847
|
+
else checkpoints[index] = settled;
|
|
6848
|
+
}));
|
|
6849
|
+
}
|
|
6850
|
+
checkpoints.sort((a, b) => a.seq - b.seq);
|
|
6851
|
+
} catch {
|
|
6852
|
+
}
|
|
6853
|
+
}
|
|
6165
6854
|
async redriveSettledChildren(runId, fan) {
|
|
6166
6855
|
if (!this.store.hasSignalWaiter) return;
|
|
6167
6856
|
for (const wait of fan.waits) {
|
|
@@ -6260,13 +6949,63 @@ var WorkflowEngine = class {
|
|
|
6260
6949
|
* knowing about the child feature.
|
|
6261
6950
|
*/
|
|
6262
6951
|
notifyParent(runId, completion) {
|
|
6263
|
-
|
|
6952
|
+
this.inBackground(this.deliverToParent(`child:${runId}`, completion));
|
|
6264
6953
|
const at = runId.lastIndexOf("~retry~");
|
|
6265
6954
|
if (at !== -1 && completion.ok) {
|
|
6266
|
-
|
|
6955
|
+
this.inBackground(this.deliverToParent(`child:${runId.slice(0, at)}`, completion));
|
|
6267
6956
|
}
|
|
6268
6957
|
}
|
|
6269
6958
|
/**
|
|
6959
|
+
* {@link signal} a child's completion to its parent, retrying a store error a few times. The child
|
|
6960
|
+
* is already persisted terminal, so a failure here would leave the parent waiting on a child that
|
|
6961
|
+
* finished; the parent's own turns reconcile that ({@link deliverFinishedChildren}), but only on
|
|
6962
|
+
* its next wake. A waiter already taken is delivered to directly on a retry, so the completion is
|
|
6963
|
+
* not buffered for a waiter that no longer exists.
|
|
6964
|
+
*/
|
|
6965
|
+
async deliverToParent(token, completion) {
|
|
6966
|
+
let waiter;
|
|
6967
|
+
const cancelled = !completion.ok && completion.code === CHILD_CANCELLED;
|
|
6968
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
6969
|
+
try {
|
|
6970
|
+
if (waiter === void 0) waiter = await this.store.takeSignalWaiter(token);
|
|
6971
|
+
if (waiter) {
|
|
6972
|
+
const parent = cancelled ? await this.store.getRun(waiter.runId) : void 0;
|
|
6973
|
+
if (parent && TERMINAL_RUN_STATUSES.includes(parent.status)) return;
|
|
6974
|
+
await this.deliverSignal(waiter, completion);
|
|
6975
|
+
return;
|
|
6976
|
+
}
|
|
6977
|
+
if (cancelled || await this.parentHoldsCompletion(token, completion)) return;
|
|
6978
|
+
await this.signal(token, completion);
|
|
6979
|
+
return;
|
|
6980
|
+
} catch (err) {
|
|
6981
|
+
if (err instanceof NamespaceMismatch) return;
|
|
6982
|
+
if (attempt >= PARENT_NOTIFY_ATTEMPTS || this.draining) return;
|
|
6983
|
+
await new Promise((resolve) => {
|
|
6984
|
+
const timer = setTimeout(resolve, PARENT_NOTIFY_BACKOFF_MS * 4 ** (attempt - 1));
|
|
6985
|
+
timer.unref?.();
|
|
6986
|
+
});
|
|
6987
|
+
}
|
|
6988
|
+
}
|
|
6989
|
+
}
|
|
6990
|
+
/**
|
|
6991
|
+
* Whether the parent of the child behind `token` (`child:<id>`) already holds `completion` as that
|
|
6992
|
+
* child's settled `signal:child:<id>` checkpoint: its own turn settled the child from the child's
|
|
6993
|
+
* row, so this notification is a duplicate. A different completion (a fix-and-replay run's success
|
|
6994
|
+
* for a child the parent saw fail) is not, and is buffered for the parent's retry. Only for a child
|
|
6995
|
+
* whose id the engine minted (its parent is derivable from it).
|
|
6996
|
+
*/
|
|
6997
|
+
async parentHoldsCompletion(token, completion) {
|
|
6998
|
+
const childId = token.slice("child:".length);
|
|
6999
|
+
const parentId = parentRunIdOf(childId);
|
|
7000
|
+
if (!parentId) return false;
|
|
7001
|
+
const name = `${CHILD_SIGNAL_PREFIX}${childId}`;
|
|
7002
|
+
const checkpoints = this.store.listCheckpointsByNamePrefix ? await this.store.listCheckpointsByNamePrefix(parentId, [
|
|
7003
|
+
name
|
|
7004
|
+
]) : await this.store.listCheckpoints(parentId);
|
|
7005
|
+
const expected = JSON.stringify(completion);
|
|
7006
|
+
return checkpoints.some((cp) => cp.name === name && cp.status === "completed" && JSON.stringify(cp.output) === expected);
|
|
7007
|
+
}
|
|
7008
|
+
/**
|
|
6270
7009
|
* Deferred child start shared by the in-process ctx host and the remote `startChild` command.
|
|
6271
7010
|
* Deferred (microtask) so a fast child can't reentrantly resume a still-suspending parent. A start
|
|
6272
7011
|
* that THROWS (unregistered/unroutable workflow, input validation, singleton back-pressure, store
|
|
@@ -6279,13 +7018,16 @@ var WorkflowEngine = class {
|
|
|
6279
7018
|
* correctly observes the failed start.
|
|
6280
7019
|
*/
|
|
6281
7020
|
startChildDeferred(workflow, input, childId, opts) {
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
this.
|
|
6285
|
-
|
|
6286
|
-
|
|
7021
|
+
this.localWork.submit(`start:${childId}`, async () => {
|
|
7022
|
+
if (this.draining) return;
|
|
7023
|
+
await this.start(workflow, input, childId, opts).catch((err) => {
|
|
7024
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7025
|
+
this.notifyParent(childId, {
|
|
7026
|
+
ok: false,
|
|
7027
|
+
error: `child workflow "${workflow}" failed to start: ${message}`
|
|
7028
|
+
});
|
|
6287
7029
|
});
|
|
6288
|
-
})
|
|
7030
|
+
});
|
|
6289
7031
|
}
|
|
6290
7032
|
/**
|
|
6291
7033
|
* Cancel a run (e.g. from the dashboard). Returns null if the run does not exist. Pass
|
|
@@ -6349,6 +7091,7 @@ var WorkflowEngine = class {
|
|
|
6349
7091
|
error: error2
|
|
6350
7092
|
});
|
|
6351
7093
|
await this.cancelChildren(runId, opts);
|
|
7094
|
+
this.notifyParent(runId, cancelledCompletion(error2));
|
|
6352
7095
|
this.notifyCancelled(runId);
|
|
6353
7096
|
if (this.controlPlane) {
|
|
6354
7097
|
void this.controlPlane.publishControl({
|
|
@@ -6863,6 +7606,7 @@ var WorkflowEngine = class {
|
|
|
6863
7606
|
const lockNow = this.clock();
|
|
6864
7607
|
if (run.lockedBy !== this.instanceId || (run.lockedUntil ?? 0) <= lockNow) {
|
|
6865
7608
|
if (!await this.store.tryLockRun(run.id, this.instanceId, lockNow + this.leaseMs, lockNow)) {
|
|
7609
|
+
this.recheckAfterLease(run.id, run.lockedUntil);
|
|
6866
7610
|
return {
|
|
6867
7611
|
runId: run.id,
|
|
6868
7612
|
status: run.status
|
|
@@ -6881,6 +7625,51 @@ var WorkflowEngine = class {
|
|
|
6881
7625
|
}
|
|
6882
7626
|
}
|
|
6883
7627
|
/**
|
|
7628
|
+
* A wake that found the run leased by another instance is dropped, and normally that is fine: the
|
|
7629
|
+
* holder's turn either sees what the wake wrote, or re-checks once it releases the lease
|
|
7630
|
+
* ({@link redriveSettledSteps}, {@link redriveSettledChildren}). Not if the holder goes down first
|
|
7631
|
+
* (a scale-down mid-turn, an OOM): its lease lapses with the run `suspended` on a step that has
|
|
7632
|
+
* finished, and nothing wakes it before its timer.
|
|
7633
|
+
*
|
|
7634
|
+
* So the dropped wake is kept here: once the lease is free (looked at every
|
|
7635
|
+
* {@link LEASE_RECHECK_MAX_MS} at most, for a bounded number of times), a run still `suspended` is
|
|
7636
|
+
* leased and resumed. A holder that already moved the run on makes this one spare replay that
|
|
7637
|
+
* re-parks. Best effort; the run's timer remains the fallback if this instance goes down too.
|
|
7638
|
+
*/
|
|
7639
|
+
recheckAfterLease(runId, lockedUntil, attempt = 1) {
|
|
7640
|
+
if (this.draining || this.leaseRetries.has(runId)) return;
|
|
7641
|
+
const remaining = Math.max(0, (lockedUntil ?? this.clock()) - this.clock());
|
|
7642
|
+
const timer = setTimeout(() => {
|
|
7643
|
+
this.leaseRetries.delete(runId);
|
|
7644
|
+
this.inBackground(this.retryDroppedWake(runId, attempt));
|
|
7645
|
+
}, Math.min(remaining, LEASE_RECHECK_MAX_MS) + LEASE_RECHECK_SLACK_MS);
|
|
7646
|
+
timer.unref?.();
|
|
7647
|
+
this.leaseRetries.set(runId, timer);
|
|
7648
|
+
}
|
|
7649
|
+
async retryDroppedWake(runId, attempt) {
|
|
7650
|
+
if (this.draining) return;
|
|
7651
|
+
const run = await this.store.getRun(runId);
|
|
7652
|
+
if (run?.status !== "suspended") return;
|
|
7653
|
+
const nowMs = this.clock();
|
|
7654
|
+
if (run.lockedUntil !== void 0 && run.lockedUntil > nowMs) {
|
|
7655
|
+
if (attempt < LEASE_RECHECK_ATTEMPTS) this.recheckAfterLease(runId, run.lockedUntil, attempt + 1);
|
|
7656
|
+
return;
|
|
7657
|
+
}
|
|
7658
|
+
if (this.queuesRunsOverTransport) {
|
|
7659
|
+
await this.wake(runId);
|
|
7660
|
+
return;
|
|
7661
|
+
}
|
|
7662
|
+
if (!await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs)) return;
|
|
7663
|
+
const fresh = await this.store.getRun(runId);
|
|
7664
|
+
if (fresh?.status !== "suspended") {
|
|
7665
|
+
await this.store.releaseRunLock(runId);
|
|
7666
|
+
return;
|
|
7667
|
+
}
|
|
7668
|
+
await this.resumeImplicit(runId).catch((err) => {
|
|
7669
|
+
if (!(err instanceof NamespaceMismatch)) throw err;
|
|
7670
|
+
});
|
|
7671
|
+
}
|
|
7672
|
+
/**
|
|
6884
7673
|
* Renew `runId`'s lease for as long as this execution holds it, and register it in
|
|
6885
7674
|
* {@link executionLeases} so settles can check the zombie fence and `drain()` can release it.
|
|
6886
7675
|
*
|
|
@@ -7244,6 +8033,7 @@ ${run.workflowVersion}`;
|
|
|
7244
8033
|
namespace: run.namespace,
|
|
7245
8034
|
error: error2
|
|
7246
8035
|
});
|
|
8036
|
+
this.notifyParent(run.id, cancelledCompletion(error2));
|
|
7247
8037
|
this.notifyCancelled(run.id);
|
|
7248
8038
|
if (this.controlPlane) {
|
|
7249
8039
|
void this.controlPlane.publishControl({
|
|
@@ -7275,6 +8065,8 @@ ${run.workflowVersion}`;
|
|
|
7275
8065
|
const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
|
|
7276
8066
|
if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
|
|
7277
8067
|
const checkpoints = await this.store.listCheckpoints(run.id);
|
|
8068
|
+
await this.deliverFinishedChildren(run.id, checkpoints);
|
|
8069
|
+
await this.deliverFinishedChildWaiters(run.id, checkpoints);
|
|
7278
8070
|
const handoff = liveHandoff(run.id, checkpoints);
|
|
7279
8071
|
if (handoff) {
|
|
7280
8072
|
const result = await this.settleRun(run, await this.followHandoff(run, handoff));
|
|
@@ -7849,6 +8641,7 @@ ${run.workflowVersion}`;
|
|
|
7849
8641
|
namespace: run.namespace,
|
|
7850
8642
|
error: error2
|
|
7851
8643
|
});
|
|
8644
|
+
this.notifyParent(run.id, cancelledCompletion(error2));
|
|
7852
8645
|
return {
|
|
7853
8646
|
runId: run.id,
|
|
7854
8647
|
status: "cancelled",
|
|
@@ -7895,6 +8688,7 @@ ${run.workflowVersion}`;
|
|
|
7895
8688
|
}
|
|
7896
8689
|
const compensations = [];
|
|
7897
8690
|
const snapshot = await this.store.listCheckpoints(run.id);
|
|
8691
|
+
await this.deliverFinishedChildren(run.id, snapshot);
|
|
7898
8692
|
const replay = /* @__PURE__ */ new Map();
|
|
7899
8693
|
for (const cp of snapshot) replay.set(cp.seq, cp);
|
|
7900
8694
|
let suspendedOnFan;
|
|
@@ -7991,13 +8785,13 @@ ${run.workflowVersion}`;
|
|
|
7991
8785
|
if (this.awaitedStepFans.get(run.id) === awaitedFans) this.awaitedStepFans.delete(run.id);
|
|
7992
8786
|
await this.releaseLeaseBestEffort(run.id);
|
|
7993
8787
|
if (suspendedOnFan) {
|
|
7994
|
-
|
|
8788
|
+
this.inBackground(this.redriveSettledChildren(run.id, suspendedOnFan));
|
|
7995
8789
|
}
|
|
7996
8790
|
if (suspendedOnSteps && awaitedSteps.size > 0) {
|
|
7997
|
-
|
|
8791
|
+
this.inBackground(this.redriveSettledSteps(run.id, awaitedSteps));
|
|
7998
8792
|
}
|
|
7999
8793
|
if (suspendedOnSteps && awaitedFans.length > 0) {
|
|
8000
|
-
|
|
8794
|
+
this.inBackground(this.redriveSettledStepFans(run.id, awaitedFans));
|
|
8001
8795
|
}
|
|
8002
8796
|
}
|
|
8003
8797
|
}
|
|
@@ -8437,9 +9231,11 @@ ${run.workflowVersion}`;
|
|
|
8437
9231
|
})) {
|
|
8438
9232
|
return this.callRemote(runId, seq, step, input, queue, transport, void 0, admission, routing);
|
|
8439
9233
|
}
|
|
8440
|
-
throw new WorkflowSuspended(wakeAt);
|
|
9234
|
+
throw new WorkflowSuspended(this.stepLeaseWakeAt(wakeAt));
|
|
9235
|
+
}
|
|
9236
|
+
if (this.clock() < existing.wakeAt) {
|
|
9237
|
+
throw new WorkflowSuspended(this.stepLeaseWakeAt(existing.wakeAt));
|
|
8441
9238
|
}
|
|
8442
|
-
if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
|
|
8443
9239
|
if (existing.attempts >= this.remoteRedispatchMax) {
|
|
8444
9240
|
throw new RemoteStepError({
|
|
8445
9241
|
message: `remote step "${step.name}" lost \u2014 no result after ${existing.attempts} re-dispatch(es)`,
|
|
@@ -8485,7 +9281,7 @@ ${run.workflowVersion}`;
|
|
|
8485
9281
|
kind: "remote",
|
|
8486
9282
|
redispatched: true
|
|
8487
9283
|
});
|
|
8488
|
-
throw new WorkflowSuspended(nextDeadline);
|
|
9284
|
+
throw new WorkflowSuspended(this.stepLeaseWakeAt(nextDeadline));
|
|
8489
9285
|
}
|
|
8490
9286
|
let attempt = 1;
|
|
8491
9287
|
if (existing?.status === "failed") {
|
|
@@ -8848,6 +9644,7 @@ ${run.workflowVersion}`;
|
|
|
8848
9644
|
});
|
|
8849
9645
|
const reads = await Promise.allSettled(live.map((i) => this.store.getCheckpoint(runId, results[i].seq)));
|
|
8850
9646
|
const pending = [];
|
|
9647
|
+
const resettled = [];
|
|
8851
9648
|
reads.forEach((read, k) => {
|
|
8852
9649
|
const index = live[k];
|
|
8853
9650
|
if (read.status === "rejected") outcomes[index] = {
|
|
@@ -8857,8 +9654,17 @@ ${run.workflowVersion}`;
|
|
|
8857
9654
|
index,
|
|
8858
9655
|
cp: read.value
|
|
8859
9656
|
});
|
|
9657
|
+
else if (read.value?.status === "completed" || read.value?.status === "failed") {
|
|
9658
|
+
resettled.push({
|
|
9659
|
+
index,
|
|
9660
|
+
cp: read.value
|
|
9661
|
+
});
|
|
9662
|
+
}
|
|
8860
9663
|
});
|
|
8861
|
-
if (pending.length === 0)
|
|
9664
|
+
if (pending.length === 0) {
|
|
9665
|
+
if (resettled.length === 0) return outcomes;
|
|
9666
|
+
return this.resumeForRedelivered(runId, resettled, outcomes);
|
|
9667
|
+
}
|
|
8862
9668
|
for (const { cp } of pending) void this.releaseQueueSlot(cp.stepId).catch(() => void 0);
|
|
8863
9669
|
const run = await this.store.getRun(runId);
|
|
8864
9670
|
if (run && (run.status === "cancelled" || run.status === "completed")) return outcomes;
|
|
@@ -8910,6 +9716,29 @@ ${run.workflowVersion}`;
|
|
|
8910
9716
|
return outcomes;
|
|
8911
9717
|
}
|
|
8912
9718
|
/**
|
|
9719
|
+
* The outcomes for a batch of redelivered results (their steps already settled): one of them
|
|
9720
|
+
* resumes the run if it is `suspended` and, for a step of a fan, the fan's barrier is open. A
|
|
9721
|
+
* resume of a run that had in fact moved on replays to its current wait and re-parks.
|
|
9722
|
+
*/
|
|
9723
|
+
async resumeForRedelivered(runId, resettled, outcomes) {
|
|
9724
|
+
const run = await this.store.getRun(runId);
|
|
9725
|
+
if (run?.status !== "suspended") return outcomes;
|
|
9726
|
+
const barriers = /* @__PURE__ */ new Map();
|
|
9727
|
+
for (const { index, cp } of resettled) {
|
|
9728
|
+
const asResult = {
|
|
9729
|
+
runId,
|
|
9730
|
+
seq: cp.seq,
|
|
9731
|
+
stepId: cp.stepId ?? stepId(runId, cp.seq),
|
|
9732
|
+
status: cp.status === "failed" ? "failed" : "completed"
|
|
9733
|
+
};
|
|
9734
|
+
if (!await this.stepFanStillRunning(cp, asResult, barriers)) {
|
|
9735
|
+
outcomes[index] = true;
|
|
9736
|
+
break;
|
|
9737
|
+
}
|
|
9738
|
+
}
|
|
9739
|
+
return outcomes;
|
|
9740
|
+
}
|
|
9741
|
+
/**
|
|
8913
9742
|
* The join barrier of a step fan: a result for one of its steps resumes the parent only once no
|
|
8914
9743
|
* step of the fan is still `pending` (a settled wave), or on a failure (failFast surfaces it, and a
|
|
8915
9744
|
* retry needs the parent to schedule it). Without it every result would replay the whole parent,
|
|
@@ -9033,7 +9862,7 @@ ${run.workflowVersion}`;
|
|
|
9033
9862
|
...waiters
|
|
9034
9863
|
];
|
|
9035
9864
|
waiters.clear();
|
|
9036
|
-
for (const runId of runIds) void this.
|
|
9865
|
+
for (const runId of runIds) void this.wake(runId).catch(() => void 0);
|
|
9037
9866
|
}
|
|
9038
9867
|
/** Release the flow-control slot a dispatched step held (if any), by its stepId. */
|
|
9039
9868
|
async releaseQueueSlot(id) {
|
|
@@ -9775,6 +10604,8 @@ var CodecStateStore = class {
|
|
|
9775
10604
|
registerChildWaits;
|
|
9776
10605
|
/** The folded checkpoint carries the children's outputs, so it is encoded. */
|
|
9777
10606
|
compactCheckpoints;
|
|
10607
|
+
/** Decodes each run like {@link getRun}. Bound only when the inner store has it. */
|
|
10608
|
+
getRuns;
|
|
9778
10609
|
/** Forwarded verbatim — a waiter carries no payload. */
|
|
9779
10610
|
listSignalWaitersByRun;
|
|
9780
10611
|
/** Forwarded verbatim — a waiter carries no payload. */
|
|
@@ -9818,6 +10649,8 @@ var CodecStateStore = class {
|
|
|
9818
10649
|
})));
|
|
9819
10650
|
const compact = inner.compactCheckpoints;
|
|
9820
10651
|
if (compact) this.compactCheckpoints = (runId, fromSeq, toSeq, into) => compact.call(inner, runId, fromSeq, toSeq, this.encCp(into));
|
|
10652
|
+
const getRuns = inner.getRuns;
|
|
10653
|
+
if (getRuns) this.getRuns = async (runIds) => (await getRuns.call(inner, runIds)).map((r) => this.decRun(r));
|
|
9821
10654
|
const byRun = inner.listSignalWaitersByRun;
|
|
9822
10655
|
if (byRun) this.listSignalWaitersByRun = (runIds) => byRun.call(inner, runIds);
|
|
9823
10656
|
const hasWaiter = inner.hasSignalWaiter;
|
|
@@ -10370,6 +11203,16 @@ var InMemoryStateStore = class {
|
|
|
10370
11203
|
...run
|
|
10371
11204
|
} : null;
|
|
10372
11205
|
}
|
|
11206
|
+
async getRuns(runIds) {
|
|
11207
|
+
const out = [];
|
|
11208
|
+
for (const id of new Set(runIds)) {
|
|
11209
|
+
const run = this.runs.get(id);
|
|
11210
|
+
if (run) out.push({
|
|
11211
|
+
...run
|
|
11212
|
+
});
|
|
11213
|
+
}
|
|
11214
|
+
return out;
|
|
11215
|
+
}
|
|
10373
11216
|
async deleteRun(runId) {
|
|
10374
11217
|
this.runs.delete(runId);
|
|
10375
11218
|
for (const [key, cp] of this.checkpoints) {
|
|
@@ -10821,6 +11664,99 @@ var InMemoryTransport = class {
|
|
|
10821
11664
|
this.controlHandlers.add(handler);
|
|
10822
11665
|
}
|
|
10823
11666
|
};
|
|
11667
|
+
|
|
11668
|
+
// src/testing/in-memory-run-queue.ts
|
|
11669
|
+
var InMemoryRunQueue = class {
|
|
11670
|
+
static {
|
|
11671
|
+
__name(this, "InMemoryRunQueue");
|
|
11672
|
+
}
|
|
11673
|
+
lanes = /* @__PURE__ */ new Map();
|
|
11674
|
+
/** A transport on this queue. */
|
|
11675
|
+
transport() {
|
|
11676
|
+
return {
|
|
11677
|
+
dispatch: /* @__PURE__ */ __name(async (_task) => {
|
|
11678
|
+
throw new Error("InMemoryRunQueue carries no remote steps");
|
|
11679
|
+
}, "dispatch"),
|
|
11680
|
+
onResult: /* @__PURE__ */ __name((_handler) => {
|
|
11681
|
+
}, "onResult"),
|
|
11682
|
+
onHeartbeat: /* @__PURE__ */ __name((_handler) => {
|
|
11683
|
+
}, "onHeartbeat"),
|
|
11684
|
+
dispatchRuns: /* @__PURE__ */ __name(async (runs, opts) => this.add(runs, opts), "dispatchRuns"),
|
|
11685
|
+
onDispatchedRun: /* @__PURE__ */ __name((handler, opts) => this.consume(handler, opts), "onDispatchedRun")
|
|
11686
|
+
};
|
|
11687
|
+
}
|
|
11688
|
+
/** Runs waiting in the queue of `namespace`. */
|
|
11689
|
+
waiting(namespace) {
|
|
11690
|
+
return (this.lanes.get(namespace ?? "")?.waiting ?? []).map((r) => r.runId);
|
|
11691
|
+
}
|
|
11692
|
+
lane(namespace) {
|
|
11693
|
+
const key = namespace ?? "";
|
|
11694
|
+
let lane = this.lanes.get(key);
|
|
11695
|
+
if (!lane) {
|
|
11696
|
+
lane = {
|
|
11697
|
+
waiting: [],
|
|
11698
|
+
queued: /* @__PURE__ */ new Map(),
|
|
11699
|
+
consumers: [],
|
|
11700
|
+
next: 0
|
|
11701
|
+
};
|
|
11702
|
+
this.lanes.set(key, lane);
|
|
11703
|
+
}
|
|
11704
|
+
return lane;
|
|
11705
|
+
}
|
|
11706
|
+
add(runs, opts) {
|
|
11707
|
+
const lane = this.lane(opts?.namespace);
|
|
11708
|
+
for (const run of runs) {
|
|
11709
|
+
const waiting = lane.queued.get(run.runId);
|
|
11710
|
+
if (waiting) {
|
|
11711
|
+
if (run.reason === "wake") waiting.reason = "wake";
|
|
11712
|
+
continue;
|
|
11713
|
+
}
|
|
11714
|
+
const entry = {
|
|
11715
|
+
...run
|
|
11716
|
+
};
|
|
11717
|
+
lane.queued.set(run.runId, entry);
|
|
11718
|
+
lane.waiting.push(entry);
|
|
11719
|
+
}
|
|
11720
|
+
lane.waiting.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
11721
|
+
setImmediate(() => this.pump(lane));
|
|
11722
|
+
}
|
|
11723
|
+
consume(handler, opts) {
|
|
11724
|
+
const lane = this.lane(opts.namespace);
|
|
11725
|
+
const consumer = {
|
|
11726
|
+
handler,
|
|
11727
|
+
concurrency: Math.max(1, opts.concurrency),
|
|
11728
|
+
active: 0,
|
|
11729
|
+
stopped: false,
|
|
11730
|
+
idle: []
|
|
11731
|
+
};
|
|
11732
|
+
lane.consumers.push(consumer);
|
|
11733
|
+
setImmediate(() => this.pump(lane));
|
|
11734
|
+
return () => {
|
|
11735
|
+
consumer.stopped = true;
|
|
11736
|
+
lane.consumers = lane.consumers.filter((c) => c !== consumer);
|
|
11737
|
+
if (consumer.active === 0) return Promise.resolve();
|
|
11738
|
+
return new Promise((resolve) => consumer.idle.push(resolve));
|
|
11739
|
+
};
|
|
11740
|
+
}
|
|
11741
|
+
pump(lane) {
|
|
11742
|
+
while (lane.waiting.length > 0) {
|
|
11743
|
+
const free = lane.consumers.filter((c) => !c.stopped && c.active < c.concurrency);
|
|
11744
|
+
if (free.length === 0) return;
|
|
11745
|
+
const consumer = free[lane.next % free.length];
|
|
11746
|
+
lane.next += 1;
|
|
11747
|
+
const run = lane.waiting.shift();
|
|
11748
|
+
lane.queued.delete(run.runId);
|
|
11749
|
+
consumer.active += 1;
|
|
11750
|
+
void consumer.handler(run).catch(() => void 0).then(() => {
|
|
11751
|
+
consumer.active -= 1;
|
|
11752
|
+
if (consumer.active === 0 && consumer.stopped) {
|
|
11753
|
+
for (const resolve of consumer.idle.splice(0)) resolve();
|
|
11754
|
+
}
|
|
11755
|
+
this.pump(lane);
|
|
11756
|
+
});
|
|
11757
|
+
}
|
|
11758
|
+
}
|
|
11759
|
+
};
|
|
10824
11760
|
export {
|
|
10825
11761
|
CHILD_EDGE_PREFIXES,
|
|
10826
11762
|
CONTROL_FLOW_SIGNAL,
|
|
@@ -10847,6 +11783,7 @@ export {
|
|
|
10847
11783
|
GatherError,
|
|
10848
11784
|
HANDOFF_SEQ_BASE,
|
|
10849
11785
|
InMemoryAdmissionBackend,
|
|
11786
|
+
InMemoryRunQueue,
|
|
10850
11787
|
InMemoryStateStore,
|
|
10851
11788
|
InMemoryTransport,
|
|
10852
11789
|
LEGACY_V1_CAPABILITIES,
|
|
@@ -10931,6 +11868,7 @@ export {
|
|
|
10931
11868
|
fanPageEtag,
|
|
10932
11869
|
findStalledRuns,
|
|
10933
11870
|
foldedCount,
|
|
11871
|
+
getRunsByIds,
|
|
10934
11872
|
handoffHandlerId,
|
|
10935
11873
|
handoffName,
|
|
10936
11874
|
hasDurableExecutionWrappers,
|
|
@@ -10968,6 +11906,7 @@ export {
|
|
|
10968
11906
|
poolNamespace,
|
|
10969
11907
|
prevCronFireMs,
|
|
10970
11908
|
queryCheckpointsOf,
|
|
11909
|
+
readRunsByIds,
|
|
10971
11910
|
readSearchAttributes,
|
|
10972
11911
|
requiredCapabilities,
|
|
10973
11912
|
resolveConcurrency,
|