@adonis-agora/durable 0.48.0 → 0.49.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/CHANGELOG.md +33 -0
- package/dist/assets/spa/assets/index-6nmSTfT7.css +1 -0
- package/dist/assets/spa/assets/index-DdB9zxuX.js +16 -0
- package/dist/assets/spa/assets/index-DdB9zxuX.js.map +1 -0
- package/dist/assets/spa/index.html +2 -2
- package/dist/providers/dashboard_provider.d.ts +3 -1
- package/dist/providers/dashboard_provider.d.ts.map +1 -1
- package/dist/providers/dashboard_provider.js +7 -2
- package/dist/providers/dashboard_provider.js.map +1 -1
- package/dist/providers/durable_provider.d.ts.map +1 -1
- package/dist/providers/durable_provider.js +1 -0
- package/dist/providers/durable_provider.js.map +1 -1
- package/dist/src/codec-state-store.d.ts +2 -0
- package/dist/src/codec-state-store.d.ts.map +1 -1
- package/dist/src/codec-state-store.js +6 -0
- package/dist/src/codec-state-store.js.map +1 -1
- package/dist/src/dashboard/gateway-adapter.d.ts.map +1 -1
- package/dist/src/dashboard/gateway-adapter.js +10 -0
- package/dist/src/dashboard/gateway-adapter.js.map +1 -1
- package/dist/src/dashboard/handlers.d.ts +22 -1
- package/dist/src/dashboard/handlers.d.ts.map +1 -1
- package/dist/src/dashboard/handlers.js +63 -10
- package/dist/src/dashboard/handlers.js.map +1 -1
- package/dist/src/dashboard/index.d.ts +1 -1
- package/dist/src/dashboard/index.js +1 -1
- package/dist/src/dashboard/openapi.d.ts.map +1 -1
- package/dist/src/dashboard/openapi.js +26 -2
- package/dist/src/dashboard/openapi.js.map +1 -1
- package/dist/src/define_config.d.ts +6 -0
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/engine.d.ts +44 -3
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +226 -28
- package/dist/src/engine.js.map +1 -1
- package/dist/src/errors.d.ts +12 -1
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +8 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/fan-join.d.ts +32 -3
- package/dist/src/fan-join.d.ts.map +1 -1
- package/dist/src/fan-join.js +58 -4
- package/dist/src/fan-join.js.map +1 -1
- package/dist/src/index.d.ts +4 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/interfaces.d.ts +55 -9
- package/dist/src/interfaces.d.ts.map +1 -1
- package/dist/src/interfaces.js.map +1 -1
- package/dist/src/run-detail-summary.d.ts +98 -0
- package/dist/src/run-detail-summary.d.ts.map +1 -0
- package/dist/src/run-detail-summary.js +150 -0
- package/dist/src/run-detail-summary.js.map +1 -0
- package/dist/src/run-gateway/interface.d.ts +11 -0
- package/dist/src/run-gateway/interface.d.ts.map +1 -1
- package/dist/src/run-gateway/proxy-run-gateway.d.ts +3 -0
- package/dist/src/run-gateway/proxy-run-gateway.d.ts.map +1 -1
- package/dist/src/run-gateway/proxy-run-gateway.js +8 -0
- package/dist/src/run-gateway/proxy-run-gateway.js.map +1 -1
- package/dist/src/run-gateway/run-request-responder.d.ts +4 -3
- package/dist/src/run-gateway/run-request-responder.d.ts.map +1 -1
- package/dist/src/run-gateway/run-request-responder.js +23 -3
- package/dist/src/run-gateway/run-request-responder.js.map +1 -1
- package/dist/src/run-gateway/store-run-gateway.d.ts +3 -0
- package/dist/src/run-gateway/store-run-gateway.d.ts.map +1 -1
- package/dist/src/run-gateway/store-run-gateway.js +7 -0
- package/dist/src/run-gateway/store-run-gateway.js.map +1 -1
- package/dist/src/stores/lucid.d.ts +1 -0
- package/dist/src/stores/lucid.d.ts.map +1 -1
- package/dist/src/stores/lucid.js +16 -3
- package/dist/src/stores/lucid.js.map +1 -1
- package/dist/src/testing/in-memory-state-store.d.ts +1 -0
- package/dist/src/testing/in-memory-state-store.d.ts.map +1 -1
- package/dist/src/testing/in-memory-state-store.js +14 -6
- package/dist/src/testing/in-memory-state-store.js.map +1 -1
- package/dist/src/testing-kit/state-store-conformance.d.ts.map +1 -1
- package/dist/src/testing-kit/state-store-conformance.js +59 -0
- package/dist/src/testing-kit/state-store-conformance.js.map +1 -1
- package/dist/src/transports/bullmq/adaptive-concurrency.d.ts +144 -0
- package/dist/src/transports/bullmq/adaptive-concurrency.d.ts.map +1 -0
- package/dist/src/transports/bullmq/adaptive-concurrency.js +312 -0
- package/dist/src/transports/bullmq/adaptive-concurrency.js.map +1 -0
- package/dist/src/transports/bullmq/bullmq-transport.d.ts +18 -1
- package/dist/src/transports/bullmq/bullmq-transport.d.ts.map +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.js +50 -49
- package/dist/src/transports/bullmq/bullmq-transport.js.map +1 -1
- package/dist/src/transports/bullmq/deps.d.ts +11 -1
- package/dist/src/transports/bullmq/deps.d.ts.map +1 -1
- package/dist/src/transports/bullmq/deps.js +12 -1
- package/dist/src/transports/bullmq/deps.js.map +1 -1
- package/dist/src/transports/bullmq/index.d.ts +1 -0
- package/dist/src/transports/bullmq/index.d.ts.map +1 -1
- package/dist/src/transports/bullmq/index.js +1 -0
- package/dist/src/transports/bullmq/index.js.map +1 -1
- package/dist/src/transports/factory.d.ts +21 -2
- package/dist/src/transports/factory.d.ts.map +1 -1
- package/dist/src/transports/factory.js +5 -1
- package/dist/src/transports/factory.js.map +1 -1
- package/dist/src/workflow-ctx.d.ts +13 -1
- package/dist/src/workflow-ctx.d.ts.map +1 -1
- package/dist/src/workflow-ctx.js +25 -1
- package/dist/src/workflow-ctx.js.map +1 -1
- package/package.json +3 -3
- package/skills/durable-cluster/SKILL.md +10 -0
- package/skills/durable-transports-stores/SKILL.md +37 -0
- package/skills/durable-workflows/SKILL.md +19 -1
- package/dist/assets/spa/assets/index-DtGMBUDm.css +0 -1
- package/dist/assets/spa/assets/index-_VISH9Xg.js +0 -16
- package/dist/assets/spa/assets/index-_VISH9Xg.js.map +0 -1
package/dist/src/engine.js
CHANGED
|
@@ -6,10 +6,10 @@ import { assertConcurrency, concurrencyTag, countRuns, resolveConcurrency, } fro
|
|
|
6
6
|
import { controlPlaneDescriptor, planDispatch, WorkflowBlocked, } from './dispatch-routing.js';
|
|
7
7
|
import { parseDuration } from './duration.js';
|
|
8
8
|
import { Entities } from './entities.js';
|
|
9
|
-
import { ContinueAsNew, NonDeterminismError, RemoteStepTimeout, WorkflowSuspended, } from './errors.js';
|
|
9
|
+
import { ContinueAsNew, GatherError, NonDeterminismError, RemoteStepTimeout, WorkflowSuspended, } from './errors.js';
|
|
10
10
|
import { EventAccumulators } from './event-accumulators.js';
|
|
11
11
|
import { eventMatches, eventMatchOf, eventPrefix } from './events.js';
|
|
12
|
-
import { FOLDED_PREFIX, fanOfWaiter,
|
|
12
|
+
import { childIdsOf, FOLDED_PREFIX, fanOfWaiter, parseStepFanGroup, stepFanGroup, } from './fan-join.js';
|
|
13
13
|
import { aggregateAnnouncements } from './handshake/announced.js';
|
|
14
14
|
import { isNonEmptyRunScope, TERMINAL_RUN_STATUSES } from './interfaces.js';
|
|
15
15
|
import { asHeartbeat } from './observability-scope.js';
|
|
@@ -70,6 +70,18 @@ const versionKey = (name, version) => `${name}@${version}`;
|
|
|
70
70
|
* un-checkpointed `-1` undo dispatches of older engine versions.
|
|
71
71
|
*/
|
|
72
72
|
const COMPENSATION_SEQ_BASE = -2;
|
|
73
|
+
/** How many checkpoint writes / dispatches a step fan sends at once. */
|
|
74
|
+
const STEP_FAN_CHUNK = 200;
|
|
75
|
+
/** The default {@link WorkflowEngineDeps.timerBatchSize}. */
|
|
76
|
+
const DEFAULT_TIMER_BATCH = 500;
|
|
77
|
+
/** Fisher–Yates, in place. */
|
|
78
|
+
function shuffle(items) {
|
|
79
|
+
for (let i = items.length - 1; i > 0; i -= 1) {
|
|
80
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
81
|
+
[items[i], items[j]] = [items[j], items[i]];
|
|
82
|
+
}
|
|
83
|
+
return items;
|
|
84
|
+
}
|
|
73
85
|
/**
|
|
74
86
|
* The routing token a dispatched step lands on: the step's own (broker-safe) name, suffixed by the
|
|
75
87
|
* tenant the RUN belongs to. `runNamespace` — not the executing engine's namespace — is the tenant
|
|
@@ -133,6 +145,7 @@ export class WorkflowEngine {
|
|
|
133
145
|
leaseMs;
|
|
134
146
|
maxRecoveryAttempts;
|
|
135
147
|
reconcileMs;
|
|
148
|
+
timerBatchSize;
|
|
136
149
|
remoteRedispatchMs;
|
|
137
150
|
remoteRedispatchMax;
|
|
138
151
|
webhookUrl;
|
|
@@ -284,6 +297,12 @@ export class WorkflowEngine {
|
|
|
284
297
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
285
298
|
// Default 5min; an explicit 0 disables the fallback (opt back into wake-forever-on-lost-event).
|
|
286
299
|
this.reconcileMs = deps.reconcileMs === undefined ? 300_000 : deps.reconcileMs || undefined;
|
|
300
|
+
// A non-finite or sub-1 value falls back to the default: a NaN limit would stall timer recovery.
|
|
301
|
+
const batch = deps.timerBatchSize;
|
|
302
|
+
this.timerBatchSize =
|
|
303
|
+
batch !== undefined && Number.isFinite(batch) && batch >= 1
|
|
304
|
+
? Math.floor(batch)
|
|
305
|
+
: DEFAULT_TIMER_BATCH;
|
|
287
306
|
// Opt-in: unset (or 0) leaves the by-design "re-suspend a lost dispatch, never re-dispatch" behavior.
|
|
288
307
|
this.remoteRedispatchMs = deps.remoteRedispatchMs || undefined;
|
|
289
308
|
this.remoteRedispatchMax = Math.max(1, deps.remoteRedispatchMax ?? 10);
|
|
@@ -1361,6 +1380,31 @@ export class WorkflowEngine {
|
|
|
1361
1380
|
}
|
|
1362
1381
|
})());
|
|
1363
1382
|
}
|
|
1383
|
+
/**
|
|
1384
|
+
* {@link recheckWaitSeqs} for the step fans a suspended turn waits on: re-drive the run once a fan
|
|
1385
|
+
* has no step left `pending`. The last result of a fan may have opened the join barrier while this
|
|
1386
|
+
* turn held the lease, and its resume no-oped against it. A fan with a step still `pending` is left
|
|
1387
|
+
* to that step's result.
|
|
1388
|
+
*/
|
|
1389
|
+
recheckStepFans(runId, fans) {
|
|
1390
|
+
this.trackEffect((async () => {
|
|
1391
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1392
|
+
for (const fan of fans) {
|
|
1393
|
+
const pending = await this.countPendingSteps(runId, fan.from, fan.to).catch(() => 1);
|
|
1394
|
+
if (pending === 0) {
|
|
1395
|
+
await this.resumeImplicit(runId).catch(() => undefined);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
})());
|
|
1400
|
+
}
|
|
1401
|
+
/** {@link StateStore.countCheckpoints} of `pending` steps, or a JS count on a store without it. */
|
|
1402
|
+
async countPendingSteps(runId, from, to) {
|
|
1403
|
+
if (this.store.countCheckpoints) {
|
|
1404
|
+
return this.store.countCheckpoints(runId, from, to, ['pending']);
|
|
1405
|
+
}
|
|
1406
|
+
return (await this.store.listCheckpoints(runId)).filter((c) => c.seq >= from && c.seq <= to && c.status === 'pending').length;
|
|
1407
|
+
}
|
|
1364
1408
|
/** Track an in-flight execution so {@link drain} can wait for it. */
|
|
1365
1409
|
track(runId, p) {
|
|
1366
1410
|
this.inflight.set(p, runId);
|
|
@@ -1636,8 +1680,10 @@ export class WorkflowEngine {
|
|
|
1636
1680
|
* not due re-suspends cheaply without running new work.
|
|
1637
1681
|
*/
|
|
1638
1682
|
async resumeDueTimers(nowMs = this.clock()) {
|
|
1639
|
-
// Capped per tick (a backlog drains over several polls) — a store may ignore the cap.
|
|
1640
|
-
|
|
1683
|
+
// Capped per tick (a backlog drains over several polls) — a store may ignore the cap. Shuffled:
|
|
1684
|
+
// every instance loads the same most-overdue batch, and walking it in the same order would have
|
|
1685
|
+
// them all contend for the same leases one row at a time.
|
|
1686
|
+
const due = shuffle(await asHeartbeat(() => this.store.listDueTimers(nowMs, this.namespace, this.timerBatchSize)));
|
|
1641
1687
|
const blocked = await this.dueBlockedRuns(nowMs);
|
|
1642
1688
|
return this.resumeLeased([...due, ...blocked], nowMs);
|
|
1643
1689
|
}
|
|
@@ -3642,6 +3688,9 @@ export class WorkflowEngine {
|
|
|
3642
3688
|
if (settled.status === 'suspended' && err.waitSeqs?.length) {
|
|
3643
3689
|
this.recheckWaitSeqs(run.id, err.waitSeqs);
|
|
3644
3690
|
}
|
|
3691
|
+
if (settled.status === 'suspended' && err.stepFans?.length) {
|
|
3692
|
+
this.recheckStepFans(run.id, err.stepFans);
|
|
3693
|
+
}
|
|
3645
3694
|
if (settled.status === 'suspended')
|
|
3646
3695
|
suspendedOnFan = err.fan;
|
|
3647
3696
|
return settled;
|
|
@@ -3928,6 +3977,7 @@ export class WorkflowEngine {
|
|
|
3928
3977
|
completeStep: (s) => this.completeStep(s),
|
|
3929
3978
|
failStep: (s) => this.failStep(s),
|
|
3930
3979
|
callRemote: (runId, seq, step, input, queue, transport, admission) => this.callRemote(runId, seq, step, input, queue, transport, replay, admission, runNamespace),
|
|
3980
|
+
callRemoteAll: (runId, positions, step, inputs, opts) => this.callRemoteAll(runId, positions, step, inputs, opts, runNamespace),
|
|
3931
3981
|
// Defer so a fast child can't reentrantly resume a still-running parent. The child inherits the
|
|
3932
3982
|
// PARENT RUN's namespace, not this engine's: an operator legitimately executes every tenant's
|
|
3933
3983
|
// parents, and without this the child would fall to the operator's own pool.
|
|
@@ -4121,7 +4171,7 @@ export class WorkflowEngine {
|
|
|
4121
4171
|
}));
|
|
4122
4172
|
return blocked;
|
|
4123
4173
|
}
|
|
4124
|
-
async callRemote(runId, seq, step, input, queue, transport, replay, admission, runNamespace) {
|
|
4174
|
+
async callRemote(runId, seq, step, input, queue, transport, replay, admission, runNamespace, parallelGroup) {
|
|
4125
4175
|
// Read the prefix from the per-execution snapshot (avoids the O(N²) replay SELECTs); a seq absent
|
|
4126
4176
|
// from the snapshot — not yet dispatched, or written after the snapshot — falls back to the store.
|
|
4127
4177
|
let existing = replay?.get(seq) ?? (await this.store.getCheckpoint(runId, seq));
|
|
@@ -4164,7 +4214,7 @@ export class WorkflowEngine {
|
|
|
4164
4214
|
// The result may have settled the row since it was read: then nothing was written, and the
|
|
4165
4215
|
// step is re-read from the store.
|
|
4166
4216
|
if (!(await this.savePendingCheckpoint({ ...existing, wakeAt }))) {
|
|
4167
|
-
return this.callRemote(runId, seq, step, input, queue, transport, undefined, admission, runNamespace);
|
|
4217
|
+
return this.callRemote(runId, seq, step, input, queue, transport, undefined, admission, runNamespace, parallelGroup);
|
|
4168
4218
|
}
|
|
4169
4219
|
throw new WorkflowSuspended(wakeAt, [seq]);
|
|
4170
4220
|
}
|
|
@@ -4196,7 +4246,7 @@ export class WorkflowEngine {
|
|
|
4196
4246
|
],
|
|
4197
4247
|
});
|
|
4198
4248
|
if (!claimed) {
|
|
4199
|
-
return this.callRemote(runId, seq, step, input, queue, transport, undefined, admission, runNamespace);
|
|
4249
|
+
return this.callRemote(runId, seq, step, input, queue, transport, undefined, admission, runNamespace, parallelGroup);
|
|
4200
4250
|
}
|
|
4201
4251
|
await this.dispatchRemoteTask({
|
|
4202
4252
|
runId,
|
|
@@ -4292,6 +4342,7 @@ export class WorkflowEngine {
|
|
|
4292
4342
|
// released by WHICHEVER instance receives the result — the in-memory stepQueue map only
|
|
4293
4343
|
// covers the dispatching pod, and a slot released by no one starves the queue fleet-wide.
|
|
4294
4344
|
queue: admittedQueue,
|
|
4345
|
+
...(parallelGroup !== undefined ? { parallelGroup } : {}),
|
|
4295
4346
|
enqueuedAt,
|
|
4296
4347
|
startedAt: enqueuedAt, // placeholders until the worker result lands
|
|
4297
4348
|
finishedAt: enqueuedAt,
|
|
@@ -4310,6 +4361,150 @@ export class WorkflowEngine {
|
|
|
4310
4361
|
this.emit({ type: 'step.started', runId, seq, name: step.name, kind: 'remote' });
|
|
4311
4362
|
throw new WorkflowSuspended(undefined, [seq]);
|
|
4312
4363
|
}
|
|
4364
|
+
/**
|
|
4365
|
+
* `ctx.all` over a `@Step`: one durable step per input, dispatched together, joined once. The ctx
|
|
4366
|
+
* reserves the whole position block up front, so replay re-derives the same seqs.
|
|
4367
|
+
*
|
|
4368
|
+
* Each turn reads the fan's checkpoints fresh, hands every step that already has one to
|
|
4369
|
+
* {@link callRemote} (which owns the completed / pending / failed-retry rules), and sends the steps
|
|
4370
|
+
* nobody has sent yet: all of them, or with `concurrency`, the next wave once the last one has
|
|
4371
|
+
* drained. New steps are written `pending` BEFORE any is dispatched, so a turn that runs after a
|
|
4372
|
+
* fast result finds them already sent instead of sending them again. The suspension carries the
|
|
4373
|
+
* fan's block, not its seqs, so the join barrier ({@link stepFanStillRunning}) and
|
|
4374
|
+
* {@link recheckStepFans} wake the parent once per wave, not once per result.
|
|
4375
|
+
*/
|
|
4376
|
+
async callRemoteAll(runId, positions, step, inputs, opts, runNamespace) {
|
|
4377
|
+
if (step.timeoutMs) {
|
|
4378
|
+
throw new Error(`ctx.all: step "${step.name}" has a liveness timeoutMs; a step fan needs durable steps`);
|
|
4379
|
+
}
|
|
4380
|
+
if (opts.concurrency !== undefined &&
|
|
4381
|
+
(!Number.isInteger(opts.concurrency) || opts.concurrency < 1)) {
|
|
4382
|
+
throw new Error(`ctx.all: concurrency must be a positive integer, got ${opts.concurrency}`);
|
|
4383
|
+
}
|
|
4384
|
+
if (this.pool.size === 0)
|
|
4385
|
+
throw new Error('remote steps require a Transport');
|
|
4386
|
+
const first = positions[0];
|
|
4387
|
+
const last = positions[positions.length - 1];
|
|
4388
|
+
const group = stepFanGroup(first, positions.length, opts.mode);
|
|
4389
|
+
const bySeq = new Map();
|
|
4390
|
+
for (const cp of await this.store.listCheckpoints(runId)) {
|
|
4391
|
+
if (cp.seq >= first && cp.seq <= last)
|
|
4392
|
+
bySeq.set(cp.seq, cp);
|
|
4393
|
+
}
|
|
4394
|
+
const outputs = new Array(inputs.length);
|
|
4395
|
+
const failures = [];
|
|
4396
|
+
const unsent = [];
|
|
4397
|
+
let inFlight = 0;
|
|
4398
|
+
let wakeAt;
|
|
4399
|
+
for (let i = 0; i < inputs.length; i += 1) {
|
|
4400
|
+
const seq = positions[i];
|
|
4401
|
+
const cp = bySeq.get(seq);
|
|
4402
|
+
if (!cp) {
|
|
4403
|
+
unsent.push(i);
|
|
4404
|
+
continue;
|
|
4405
|
+
}
|
|
4406
|
+
if (cp.name !== step.name)
|
|
4407
|
+
throw new NonDeterminismError(runId, seq, step.name, cp.name);
|
|
4408
|
+
if (cp.status === 'completed') {
|
|
4409
|
+
outputs[i] = cp.output;
|
|
4410
|
+
continue;
|
|
4411
|
+
}
|
|
4412
|
+
try {
|
|
4413
|
+
outputs[i] = await this.callRemote(runId, seq, step, inputs[i], undefined, opts.transport, undefined, opts.admission, runNamespace, group);
|
|
4414
|
+
}
|
|
4415
|
+
catch (err) {
|
|
4416
|
+
if (err instanceof WorkflowSuspended) {
|
|
4417
|
+
inFlight += 1;
|
|
4418
|
+
if (err.wakeAt !== undefined)
|
|
4419
|
+
wakeAt = Math.min(wakeAt ?? err.wakeAt, err.wakeAt);
|
|
4420
|
+
}
|
|
4421
|
+
else if (err instanceof RemoteStepError) {
|
|
4422
|
+
const failure = {
|
|
4423
|
+
index: i,
|
|
4424
|
+
id: stepId(runId, seq),
|
|
4425
|
+
error: err.message,
|
|
4426
|
+
...codeAndDetails(err),
|
|
4427
|
+
};
|
|
4428
|
+
if (opts.mode === 'failFast')
|
|
4429
|
+
throw new GatherError([failure], inputs.length);
|
|
4430
|
+
failures.push(failure);
|
|
4431
|
+
}
|
|
4432
|
+
else {
|
|
4433
|
+
throw err;
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
const batch = inFlight > 0
|
|
4438
|
+
? []
|
|
4439
|
+
: opts.concurrency === undefined
|
|
4440
|
+
? unsent
|
|
4441
|
+
: unsent.slice(0, opts.concurrency);
|
|
4442
|
+
if (batch.length > 0) {
|
|
4443
|
+
await this.sendStepFan(runId, positions, step, inputs, batch, group, opts, runNamespace);
|
|
4444
|
+
}
|
|
4445
|
+
if (inFlight + batch.length > 0) {
|
|
4446
|
+
throw new WorkflowSuspended(wakeAt, undefined, undefined, [{ from: first, to: last }]);
|
|
4447
|
+
}
|
|
4448
|
+
if (opts.settle) {
|
|
4449
|
+
const failed = new Map(failures.map((f) => [f.index, f]));
|
|
4450
|
+
// `outputs` has a hole at every failed index, which `map` would skip.
|
|
4451
|
+
return Array.from(outputs, (value, i) => {
|
|
4452
|
+
const f = failed.get(i);
|
|
4453
|
+
if (!f)
|
|
4454
|
+
return { status: 'fulfilled', value };
|
|
4455
|
+
const { index: _index, ...rejected } = f;
|
|
4456
|
+
return { status: 'rejected', ...rejected };
|
|
4457
|
+
});
|
|
4458
|
+
}
|
|
4459
|
+
if (failures.length > 0)
|
|
4460
|
+
throw new GatherError(failures, inputs.length);
|
|
4461
|
+
return outputs;
|
|
4462
|
+
}
|
|
4463
|
+
/** Write the `pending` checkpoints of a step fan's new steps, then dispatch them. */
|
|
4464
|
+
async sendStepFan(runId, positions, step, inputs, batch, group, opts, runNamespace) {
|
|
4465
|
+
const token = stepGroup(step, runNamespace);
|
|
4466
|
+
await this.ensureRoutable(token, step.requires);
|
|
4467
|
+
const input = (i) => step.input ? step.input.parse(inputs[i]) : inputs[i];
|
|
4468
|
+
const validInputs = new Map(batch.map((i) => [i, input(i)]));
|
|
4469
|
+
const enqueuedAt = new Date();
|
|
4470
|
+
for (let at = 0; at < batch.length; at += STEP_FAN_CHUNK) {
|
|
4471
|
+
await Promise.all(batch.slice(at, at + STEP_FAN_CHUNK).map((i) => {
|
|
4472
|
+
const seq = positions[i];
|
|
4473
|
+
return this.store.saveCheckpoint({
|
|
4474
|
+
runId,
|
|
4475
|
+
seq,
|
|
4476
|
+
name: step.name,
|
|
4477
|
+
kind: 'remote',
|
|
4478
|
+
stepId: stepId(runId, seq),
|
|
4479
|
+
status: 'pending',
|
|
4480
|
+
input: validInputs.get(i),
|
|
4481
|
+
attempts: 1,
|
|
4482
|
+
workerGroup: token,
|
|
4483
|
+
parallelGroup: group,
|
|
4484
|
+
enqueuedAt,
|
|
4485
|
+
startedAt: enqueuedAt,
|
|
4486
|
+
finishedAt: enqueuedAt,
|
|
4487
|
+
});
|
|
4488
|
+
}));
|
|
4489
|
+
}
|
|
4490
|
+
for (let at = 0; at < batch.length; at += STEP_FAN_CHUNK) {
|
|
4491
|
+
await Promise.all(batch.slice(at, at + STEP_FAN_CHUNK).map(async (i) => {
|
|
4492
|
+
const seq = positions[i];
|
|
4493
|
+
await this.dispatchRemoteTask({
|
|
4494
|
+
runId,
|
|
4495
|
+
seq,
|
|
4496
|
+
name: step.name,
|
|
4497
|
+
stepId: stepId(runId, seq),
|
|
4498
|
+
group: token,
|
|
4499
|
+
input: validInputs.get(i),
|
|
4500
|
+
priority: opts.admission?.priority,
|
|
4501
|
+
attempt: 1,
|
|
4502
|
+
transport: opts.transport,
|
|
4503
|
+
});
|
|
4504
|
+
this.emit({ type: 'step.started', runId, seq, name: step.name, kind: 'remote' });
|
|
4505
|
+
}));
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4313
4508
|
/**
|
|
4314
4509
|
* Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
|
|
4315
4510
|
* dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
|
|
@@ -4358,18 +4553,34 @@ export class WorkflowEngine {
|
|
|
4358
4553
|
return true;
|
|
4359
4554
|
}
|
|
4360
4555
|
async completeRemoteResult(result) {
|
|
4361
|
-
const
|
|
4362
|
-
if (
|
|
4556
|
+
const cp = await this.settleRemoteCheckpoint(result);
|
|
4557
|
+
if (cp === undefined)
|
|
4363
4558
|
return;
|
|
4559
|
+
if (await this.stepFanStillRunning(cp, result))
|
|
4560
|
+
return;
|
|
4561
|
+
const runId = result.runId;
|
|
4364
4562
|
this.trackEffect(this.resumeImplicit(runId).catch((err) => {
|
|
4365
4563
|
console.warn(`[adonis-durable] background resume of run ${runId} failed (the run recovers on its next reconcile wake): ${err instanceof Error ? err.message : String(err)}`);
|
|
4366
4564
|
}));
|
|
4367
4565
|
}
|
|
4566
|
+
/**
|
|
4567
|
+
* The join barrier of a step fan: a result for one of its steps resumes the parent only once no
|
|
4568
|
+
* step of the fan is still `pending` (a settled wave), or on a failure (failFast surfaces it, and a
|
|
4569
|
+
* retry needs the parent to schedule it). Without it every result would replay the whole parent,
|
|
4570
|
+
* which makes a fan of N steps cost O(N²) checkpoint reads. A redelivered result goes through it
|
|
4571
|
+
* too: its re-driven resume is only owed once the fan has settled.
|
|
4572
|
+
*/
|
|
4573
|
+
async stepFanStillRunning(cp, result) {
|
|
4574
|
+
const fan = parseStepFanGroup(cp.parallelGroup);
|
|
4575
|
+
if (!fan || result.status === 'failed')
|
|
4576
|
+
return false;
|
|
4577
|
+
return ((await this.countPendingSteps(result.runId, fan.firstSeq, fan.firstSeq + fan.size - 1)) > 0);
|
|
4578
|
+
}
|
|
4368
4579
|
/**
|
|
4369
4580
|
* The durable half of {@link completeRemoteResult}: settle `result` onto its checkpoint (or re-drive
|
|
4370
|
-
* the resume when a redelivered result finds it already settled). Returns the
|
|
4371
|
-
* `undefined` when no resume is wanted (no checkpoint, or a
|
|
4372
|
-
* terminal is terminal).
|
|
4581
|
+
* the resume when a redelivered result finds it already settled). Returns the step's checkpoint as
|
|
4582
|
+
* read when its run should be resumed, or `undefined` when no resume is wanted (no checkpoint, or a
|
|
4583
|
+
* late result for a terminal run — terminal is terminal).
|
|
4373
4584
|
*/
|
|
4374
4585
|
async settleRemoteCheckpoint(result) {
|
|
4375
4586
|
const cp = await this.store.getCheckpoint(result.runId, result.seq);
|
|
@@ -4397,7 +4608,7 @@ export class WorkflowEngine {
|
|
|
4397
4608
|
// explicit `requeue`/`durable:retry`, never to a stray result.
|
|
4398
4609
|
if (cp.status !== 'pending') {
|
|
4399
4610
|
if (!terminal)
|
|
4400
|
-
return
|
|
4611
|
+
return cp;
|
|
4401
4612
|
return undefined;
|
|
4402
4613
|
}
|
|
4403
4614
|
// A result settling this step frees its flow-control slot (no-op if it wasn't queued). Released
|
|
@@ -4440,7 +4651,7 @@ export class WorkflowEngine {
|
|
|
4440
4651
|
finishedAt,
|
|
4441
4652
|
});
|
|
4442
4653
|
if (!claimed)
|
|
4443
|
-
return terminal ? undefined :
|
|
4654
|
+
return terminal ? undefined : cp;
|
|
4444
4655
|
await this.releaseQueueSlot(cp);
|
|
4445
4656
|
this.emit({
|
|
4446
4657
|
type: result.status === 'completed' ? 'step.completed' : 'step.failed',
|
|
@@ -4453,7 +4664,7 @@ export class WorkflowEngine {
|
|
|
4453
4664
|
queueMs: startedAt.getTime() - cp.enqueuedAt.getTime(),
|
|
4454
4665
|
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
4455
4666
|
});
|
|
4456
|
-
return
|
|
4667
|
+
return cp;
|
|
4457
4668
|
}
|
|
4458
4669
|
/**
|
|
4459
4670
|
* A slot freed on `queue` (a fleet-wide signal): resume this instance's runs blocked on it so they
|
|
@@ -4681,17 +4892,4 @@ export class RemoteStepError extends Error {
|
|
|
4681
4892
|
}
|
|
4682
4893
|
/** Checkpoint names that record a parent→child edge: an awaited child, a folded group of them, a spawn. */
|
|
4683
4894
|
const CHILD_EDGE_PREFIXES = ['signal:child:', FOLDED_PREFIX, 'spawn:'];
|
|
4684
|
-
/** The child run ids a parent's {@link CHILD_EDGE_PREFIXES} checkpoints point at. */
|
|
4685
|
-
function childIdsOf(checkpoints) {
|
|
4686
|
-
const ids = [];
|
|
4687
|
-
for (const cp of checkpoints) {
|
|
4688
|
-
if (cp.name.startsWith('signal:child:'))
|
|
4689
|
-
ids.push(cp.name.slice('signal:child:'.length));
|
|
4690
|
-
if (cp.name.startsWith(FOLDED_PREFIX))
|
|
4691
|
-
ids.push(...foldedChildIds(cp));
|
|
4692
|
-
if (cp.name.startsWith('spawn:') && typeof cp.output === 'string')
|
|
4693
|
-
ids.push(cp.output);
|
|
4694
|
-
}
|
|
4695
|
-
return ids;
|
|
4696
|
-
}
|
|
4697
4895
|
//# sourceMappingURL=engine.js.map
|