@adonis-agora/durable 0.56.5 → 0.56.6
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 +29 -0
- package/dist/src/codec-state-store.d.ts +3 -0
- package/dist/src/codec-state-store.d.ts.map +1 -1
- package/dist/src/codec-state-store.js +7 -0
- package/dist/src/codec-state-store.js.map +1 -1
- package/dist/src/dashboard/index.d.ts +1 -1
- package/dist/src/dashboard/index.js +1 -1
- package/dist/src/dispatch-key.d.ts +27 -0
- package/dist/src/dispatch-key.d.ts.map +1 -0
- package/dist/src/dispatch-key.js +27 -0
- package/dist/src/dispatch-key.js.map +1 -0
- package/dist/src/engine.d.ts +66 -19
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +213 -56
- package/dist/src/engine.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/interfaces.d.ts +13 -0
- package/dist/src/interfaces.d.ts.map +1 -1
- package/dist/src/interfaces.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 +12 -0
- 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 +10 -0
- package/dist/src/testing/in-memory-state-store.js.map +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.d.ts.map +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.js +30 -2
- package/dist/src/transports/bullmq/bullmq-transport.js.map +1 -1
- package/package.json +1 -1
package/dist/src/engine.js
CHANGED
|
@@ -7,6 +7,7 @@ import { instantCheckpoint, lostStepEvent, stepCheckpoint } from './checkpoints.
|
|
|
7
7
|
import { CHILD_DELIVERED_TAG, markChildDelivered } from './child-delivery.js';
|
|
8
8
|
import { completionError, failedCompletion, isFailedCompletion, } from './completion.js';
|
|
9
9
|
import { assertConcurrency, concurrencyTag, countRuns, resolveConcurrency, } from './concurrency.js';
|
|
10
|
+
import { DISPATCH_KEY, dispatchKey } from './dispatch-key.js';
|
|
10
11
|
import { controlPlaneDescriptor, planDispatch, WorkflowBlocked, } from './dispatch-routing.js';
|
|
11
12
|
import { parseDuration } from './duration.js';
|
|
12
13
|
import { Entities } from './entities.js';
|
|
@@ -140,6 +141,19 @@ const BUFFERED_SWEEP_BUDGET_MS = 250;
|
|
|
140
141
|
const BUFFERED_SWEEP_REST_MS = 10 * 60_000;
|
|
141
142
|
/** The most results of one run settled by one batched write (see `completeRemoteResult`). */
|
|
142
143
|
const RESULT_BATCH_MAX = 256;
|
|
144
|
+
/** The most runs one owed-resume write marks (see `WorkflowEngine.markResumeOwed`). */
|
|
145
|
+
const OWED_RESUME_BATCH_MAX = 500;
|
|
146
|
+
/**
|
|
147
|
+
* How long a settled step result's acknowledgement waits for the run's resume before it marks the
|
|
148
|
+
* run as owed one instead (see `WorkflowEngine.resumeOwed`). A turn that ends within it needs no mark.
|
|
149
|
+
*/
|
|
150
|
+
const OWED_RESUME_GRACE_MS = 3_000;
|
|
151
|
+
/**
|
|
152
|
+
* The results `execute` returns when another instance holds the run's lease — no turn ran here, so the
|
|
153
|
+
* wake that asked for it is not served yet — each with the durable request that wake made instead
|
|
154
|
+
* (see `WorkflowEngine.resumeOwed`). By identity, so the result keeps the shape every caller sees.
|
|
155
|
+
*/
|
|
156
|
+
const leaseRefused = new WeakMap();
|
|
143
157
|
/**
|
|
144
158
|
* How many runs this instance executes in-process at once when it runs them itself (the default
|
|
145
159
|
* dispatcher without a run queue, deferred child starts): past it, run ids wait in memory in arrival
|
|
@@ -320,6 +334,10 @@ export class WorkflowEngine {
|
|
|
320
334
|
pending = new Map();
|
|
321
335
|
/** Each run's step results waiting to be settled on this instance — see {@link completeRemoteResult}. */
|
|
322
336
|
resultQueues = new Map();
|
|
337
|
+
/** Runs waiting to be marked as owed a resume by the next batched write — see {@link markResumeOwed}. */
|
|
338
|
+
owedResumes = [];
|
|
339
|
+
/** Whether an owed-resume write is in flight (the runs marked meanwhile go out in the next). */
|
|
340
|
+
markingOwedResumes = false;
|
|
323
341
|
/** Per-step "reset the liveness timer" callbacks, called with the beat's worker timestamp when a
|
|
324
342
|
* heartbeat arrives. */
|
|
325
343
|
heartbeatResets = new Map();
|
|
@@ -2196,8 +2214,13 @@ export class WorkflowEngine {
|
|
|
2196
2214
|
}
|
|
2197
2215
|
// A resume issued from INSIDE this run's own execution (its ambient ctx is this run) can only be
|
|
2198
2216
|
// satisfied once that execution ends — awaiting it there would deadlock. Queue it, answer now.
|
|
2199
|
-
if (workflowAls.getStore()?.runId === runId)
|
|
2200
|
-
|
|
2217
|
+
if (workflowAls.getStore()?.runId === runId) {
|
|
2218
|
+
// Queued, not run: the wake is not served yet (see `resumeOwed`).
|
|
2219
|
+
return this.echoCurrentStatus(runId).then((echoed) => {
|
|
2220
|
+
leaseRefused.set(echoed, Promise.resolve('failed'));
|
|
2221
|
+
return echoed;
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2201
2224
|
return queued.result;
|
|
2202
2225
|
}
|
|
2203
2226
|
/** Drop `gate` once `tail` settles, unless another execution queued behind it meanwhile. */
|
|
@@ -2742,10 +2765,12 @@ export class WorkflowEngine {
|
|
|
2742
2765
|
* ends normally settles after its dispatches, so a step it sent is always older than its settle).
|
|
2743
2766
|
* `sinceInclusive` is for a run still `running`: its `updatedAt` is that same turn's start.
|
|
2744
2767
|
*
|
|
2745
|
-
* Each step goes out again under its own `stepId` and attempt
|
|
2746
|
-
*
|
|
2747
|
-
*
|
|
2748
|
-
*
|
|
2768
|
+
* Each step goes out again under its own `stepId` and attempt, keyed like its original dispatch
|
|
2769
|
+
* (see `dispatch-key.ts`): a broker that deduplicates by key (BullMQ) adds nothing while the original
|
|
2770
|
+
* job still waits or runs, or has just finished. Elsewhere a step whose original dispatch did land
|
|
2771
|
+
* runs twice (at-least-once, as documented), and the second result finds the checkpoint settled and
|
|
2772
|
+
* is dropped. Only on a crash — a healthy run never gets here. Remote (polyglot) workflows are left to
|
|
2773
|
+
* their own turn re-drive. Returns how many steps were sent.
|
|
2749
2774
|
*/
|
|
2750
2775
|
async redispatchCrashedTurnSteps(run, checkpoints, sinceInclusive = false) {
|
|
2751
2776
|
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ??
|
|
@@ -2770,6 +2795,8 @@ export class WorkflowEngine {
|
|
|
2770
2795
|
input: cp.input,
|
|
2771
2796
|
attempt: cp.attempts,
|
|
2772
2797
|
priority: run.priority,
|
|
2798
|
+
// The original dispatch's key: a broker that still holds that job adds nothing.
|
|
2799
|
+
enqueuedAt: cp.enqueuedAt,
|
|
2773
2800
|
});
|
|
2774
2801
|
this.emit({
|
|
2775
2802
|
type: 'step.started',
|
|
@@ -3982,21 +4009,12 @@ export class WorkflowEngine {
|
|
|
3982
4009
|
});
|
|
3983
4010
|
}
|
|
3984
4011
|
/**
|
|
3985
|
-
*
|
|
3986
|
-
*
|
|
3987
|
-
*
|
|
3988
|
-
*
|
|
3989
|
-
* the request survives both this instance and the holder going down.
|
|
3990
|
-
*
|
|
3991
|
-
* The holder may have let go between the refused lease and the stamp: nobody but the timer poll
|
|
3992
|
-
* would see it then, so the lease is tried once more here. The in-memory re-check
|
|
3993
|
-
* ({@link recheckAfterLease}) runs too, as the fast path when the holder goes down mid-turn; it
|
|
3994
|
-
* stops as soon as a turn served the request.
|
|
4012
|
+
* The durable half of {@link requestWake}: keep the wake's reason, then stamp
|
|
4013
|
+
* {@link WorkflowRun.wakeRequestedAt} on the run. `stamped`; `gone` when the run is no longer live
|
|
4014
|
+
* (nothing to wake); `failed` when the stamp could not be kept (a schema without the column, a store
|
|
4015
|
+
* error). Never rejects.
|
|
3995
4016
|
*/
|
|
3996
|
-
async
|
|
3997
|
-
if (this.draining)
|
|
3998
|
-
return;
|
|
3999
|
-
let marked;
|
|
4017
|
+
async stampWakeRequest(runId, reason) {
|
|
4000
4018
|
try {
|
|
4001
4019
|
// The reason first, the stamp second: a holder that finds the stamp finds the reason too.
|
|
4002
4020
|
await this.store
|
|
@@ -4007,16 +4025,37 @@ export class WorkflowEngine {
|
|
|
4007
4025
|
publishedAt: this.clock(),
|
|
4008
4026
|
})
|
|
4009
4027
|
.catch(() => undefined);
|
|
4010
|
-
marked = await this.store.updateRunIf(runId, ['running', 'suspended', 'blocked'], {
|
|
4028
|
+
const marked = await this.store.updateRunIf(runId, ['running', 'suspended', 'blocked'], {
|
|
4011
4029
|
wakeRequestedAt: this.clock(),
|
|
4012
4030
|
});
|
|
4031
|
+
return marked ? 'stamped' : 'gone';
|
|
4013
4032
|
}
|
|
4014
4033
|
catch {
|
|
4034
|
+
return 'failed';
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
/**
|
|
4038
|
+
* A wake that found the run leased by another instance (its turn is executing) asks for another
|
|
4039
|
+
* turn, durably: it stamps {@link WorkflowRun.wakeRequestedAt} on the run. The holder does not let
|
|
4040
|
+
* go of the lease while the stamp is there ({@link releaseSuspendedLease}) and runs another turn,
|
|
4041
|
+
* which sees what the wake wrote; and a `suspended` run carrying it is due for the timer poll, so
|
|
4042
|
+
* the request survives both this instance and the holder going down.
|
|
4043
|
+
*
|
|
4044
|
+
* The holder may have let go between the refused lease and the stamp: nobody but the timer poll
|
|
4045
|
+
* would see it then, so the lease is tried once more here. The in-memory re-check
|
|
4046
|
+
* ({@link recheckAfterLease}) runs too, as the fast path when the holder goes down mid-turn; it
|
|
4047
|
+
* stops as soon as a turn served the request.
|
|
4048
|
+
*/
|
|
4049
|
+
async requestWake(runId, lockedUntil, reason = { generic: true, fans: new Map() }, stamping) {
|
|
4050
|
+
if (!stamping && this.draining)
|
|
4051
|
+
return;
|
|
4052
|
+
const stamp = await (stamping ?? this.stampWakeRequest(runId, reason));
|
|
4053
|
+
if (stamp === 'failed') {
|
|
4015
4054
|
// Not kept (a schema without the column, a store error): the in-memory re-check is all there is.
|
|
4016
4055
|
this.recheckAfterLease(runId, lockedUntil);
|
|
4017
4056
|
return;
|
|
4018
4057
|
}
|
|
4019
|
-
if (
|
|
4058
|
+
if (stamp === 'gone')
|
|
4020
4059
|
return;
|
|
4021
4060
|
// The fast path for a holder that went down mid-turn: look again in memory too (it stops once a
|
|
4022
4061
|
// turn served the request).
|
|
@@ -4160,8 +4199,11 @@ export class WorkflowEngine {
|
|
|
4160
4199
|
this.wakeReasons.delete(run.id);
|
|
4161
4200
|
if (run.lockedBy !== this.instanceId || (run.lockedUntil ?? 0) <= lockNow) {
|
|
4162
4201
|
if (!(await this.store.tryLockRun(run.id, this.instanceId, lockNow + this.leaseMs, lockNow))) {
|
|
4163
|
-
this.
|
|
4164
|
-
|
|
4202
|
+
const stamping = this.draining ? undefined : this.stampWakeRequest(run.id, reason);
|
|
4203
|
+
this.trackEffect(this.requestWake(run.id, run.lockedUntil, reason, stamping));
|
|
4204
|
+
const refused = { runId: run.id, status: run.status };
|
|
4205
|
+
leaseRefused.set(refused, stamping ?? Promise.resolve('failed'));
|
|
4206
|
+
return refused;
|
|
4165
4207
|
}
|
|
4166
4208
|
}
|
|
4167
4209
|
// This turn reads the run's checkpoints after this point, so it sees whatever the wakes that
|
|
@@ -6490,6 +6532,7 @@ export class WorkflowEngine {
|
|
|
6490
6532
|
priority: admission?.priority,
|
|
6491
6533
|
attempt,
|
|
6492
6534
|
transport,
|
|
6535
|
+
enqueuedAt,
|
|
6493
6536
|
});
|
|
6494
6537
|
this.emit({ type: 'step.started', runId, seq, name: step.name, kind: 'remote' });
|
|
6495
6538
|
throw new WorkflowSuspended(undefined, [seq]);
|
|
@@ -6695,6 +6738,7 @@ export class WorkflowEngine {
|
|
|
6695
6738
|
priority: opts.admission?.priority,
|
|
6696
6739
|
lazyPickupBeat: true,
|
|
6697
6740
|
attempt: 1,
|
|
6741
|
+
[DISPATCH_KEY]: dispatchKey(cp.stepId, 1, enqueuedAt),
|
|
6698
6742
|
})), opts.transport);
|
|
6699
6743
|
// Awaited below; a rejection is not left unhandled while the next round is written.
|
|
6700
6744
|
dispatch.catch(() => { });
|
|
@@ -6719,7 +6763,7 @@ export class WorkflowEngine {
|
|
|
6719
6763
|
* {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable.
|
|
6720
6764
|
*/
|
|
6721
6765
|
dispatchRemoteTask(task) {
|
|
6722
|
-
|
|
6766
|
+
const remote = {
|
|
6723
6767
|
runId: task.runId,
|
|
6724
6768
|
seq: task.seq,
|
|
6725
6769
|
name: task.name,
|
|
@@ -6730,7 +6774,10 @@ export class WorkflowEngine {
|
|
|
6730
6774
|
context: this.context?.(),
|
|
6731
6775
|
priority: task.priority,
|
|
6732
6776
|
attempt: task.attempt,
|
|
6733
|
-
}
|
|
6777
|
+
};
|
|
6778
|
+
if (task.enqueuedAt)
|
|
6779
|
+
remote[DISPATCH_KEY] = dispatchKey(task.stepId, task.attempt, task.enqueuedAt);
|
|
6780
|
+
return this.pool.dispatch(remote, task.transport);
|
|
6734
6781
|
}
|
|
6735
6782
|
/** A turn's read of its run's checkpoints: {@link StateStore.listCheckpointsForReplay}, or every
|
|
6736
6783
|
* column through {@link StateStore.listCheckpoints} on a store without it. */
|
|
@@ -6822,10 +6869,9 @@ export class WorkflowEngine {
|
|
|
6822
6869
|
const { cp, run } = settled;
|
|
6823
6870
|
if (await this.stepFanStillRunning(cp, result, queuedIn))
|
|
6824
6871
|
return;
|
|
6825
|
-
await this.markResumeOwed(run);
|
|
6826
6872
|
if (!parseStepFanGroup(cp.parallelGroup))
|
|
6827
6873
|
this.noteSettledHere(result.runId, [cp.seq]);
|
|
6828
|
-
this.
|
|
6874
|
+
await this.resumeOwed(run, result.runId, fanWakeOf(cp, result));
|
|
6829
6875
|
}
|
|
6830
6876
|
/**
|
|
6831
6877
|
* Record `seqs` of `runId` in {@link settledHere}, dropping the entries older than a lease. A key is
|
|
@@ -6846,43 +6892,156 @@ export class WorkflowEngine {
|
|
|
6846
6892
|
}
|
|
6847
6893
|
}
|
|
6848
6894
|
/**
|
|
6849
|
-
*
|
|
6895
|
+
* Resume the run a settled step result owes a turn, and make that turn DURABLE before the result
|
|
6896
|
+
* is acknowledged (the caller's return is the acknowledgement).
|
|
6850
6897
|
*
|
|
6851
6898
|
* Settling a result does two things — settle the checkpoint, then resume the run — and only the
|
|
6852
|
-
* first is durable: the resume runs in the background,
|
|
6853
|
-
*
|
|
6854
|
-
*
|
|
6855
|
-
*
|
|
6856
|
-
*
|
|
6899
|
+
* first is durable on its own: the resume runs in the background, so the transport is never held
|
|
6900
|
+
* by a turn that awaits another result (the self-deadlock the background resume exists for). A pod
|
|
6901
|
+
* SIGKILLed after the acknowledgement and before (or mid-way through) the resumed turn, which keeps
|
|
6902
|
+
* the run `suspended` and so is invisible to the orphan sweep, left the run parked on whatever
|
|
6903
|
+
* `wakeAt` its last turn wrote — for a step that is the reconcile fallback, minutes out — with
|
|
6904
|
+
* nothing else to wake it: the result was already acknowledged, so no redelivery re-drives it.
|
|
6857
6905
|
*
|
|
6858
|
-
* So
|
|
6859
|
-
*
|
|
6860
|
-
*
|
|
6861
|
-
*
|
|
6862
|
-
*
|
|
6863
|
-
*
|
|
6864
|
-
*
|
|
6906
|
+
* So the acknowledgement waits for one of two things, at most {@link OWED_RESUME_GRACE_MS}:
|
|
6907
|
+
* - the resumed turn ending here: it settled or re-parked the run itself, so nothing is owed any
|
|
6908
|
+
* more — the common case, and it costs no write;
|
|
6909
|
+
* - else ({@link OWED_RESUME_GRACE_MS} passed, or the resume failed) a durable mark: for a
|
|
6910
|
+
* `suspended` run {@link markResumeOwed} pulls its timer in to one lease from now, so every
|
|
6911
|
+
* instance's due-timer poll resumes it once the lease a dying turn may hold has lapsed; for a run
|
|
6912
|
+
* whose turn is still in flight, a {@link requestWake} stamp (see {@link owedDurably}). A resume
|
|
6913
|
+
* refused by another instance's lease already made that stamp, which is then enough.
|
|
6914
|
+
* A pod dying while the acknowledgement waits never acknowledged: the transport redelivers the
|
|
6915
|
+
* result, whose settled checkpoint re-drives the resume on a live instance.
|
|
6865
6916
|
*
|
|
6866
|
-
*
|
|
6917
|
+
* The wait is bounded, so a turn that awaits another result in memory holds this one for at most
|
|
6918
|
+
* the grace before it is marked and acknowledged — never a deadlock.
|
|
6919
|
+
*/
|
|
6920
|
+
async resumeOwed(run, runId, fan) {
|
|
6921
|
+
let served = false;
|
|
6922
|
+
let refusal;
|
|
6923
|
+
const resumed = this.resumeImplicit(runId, fan).then((result) => {
|
|
6924
|
+
refusal = leaseRefused.get(result);
|
|
6925
|
+
served = refusal === undefined;
|
|
6926
|
+
}, (err) => {
|
|
6927
|
+
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)}`);
|
|
6928
|
+
});
|
|
6929
|
+
this.trackEffect(resumed);
|
|
6930
|
+
const owed = this.owedDurably(run);
|
|
6931
|
+
if (owed === undefined)
|
|
6932
|
+
return;
|
|
6933
|
+
let timer;
|
|
6934
|
+
await Promise.race([
|
|
6935
|
+
resumed,
|
|
6936
|
+
new Promise((resolve) => {
|
|
6937
|
+
timer = setTimeout(resolve, OWED_RESUME_GRACE_MS);
|
|
6938
|
+
}),
|
|
6939
|
+
]);
|
|
6940
|
+
clearTimeout(timer);
|
|
6941
|
+
if (served)
|
|
6942
|
+
return;
|
|
6943
|
+
// Refused: the wake already asked the lease holder for a turn, durably, unless that failed.
|
|
6944
|
+
if (refusal !== undefined && (await refusal) !== 'failed')
|
|
6945
|
+
return;
|
|
6946
|
+
if (owed === 'mark') {
|
|
6947
|
+
await this.markResumeOwed(run, () => served);
|
|
6948
|
+
return;
|
|
6949
|
+
}
|
|
6950
|
+
if ((await this.stampWakeRequest(runId, { generic: true, fans: new Map() })) === 'failed') {
|
|
6951
|
+
console.warn(`[adonis-durable] could not mark run ${runId} as owed a turn (it recovers on its next reconcile wake)`);
|
|
6952
|
+
}
|
|
6953
|
+
}
|
|
6954
|
+
/**
|
|
6955
|
+
* What makes the turn a result settled for `run` (as read when it settled) owes durable, if the
|
|
6956
|
+
* resume does not end in time: `mark` ({@link markResumeOwed}) for a `suspended` run whose timer is
|
|
6957
|
+
* not already due within a lease; `stamp` (a {@link requestWake} stamp) for a run whose turn is in
|
|
6958
|
+
* flight (`running`, or `blocked`) — its turn may park it after this, overwriting any timer, and a
|
|
6959
|
+
* holder that dies after parking leaves a `suspended` run the orphan sweep never sees, so only the
|
|
6960
|
+
* stamp (kept across the park, and due for the timer poll at once) survives both. Else nothing.
|
|
6867
6961
|
*/
|
|
6868
|
-
|
|
6962
|
+
owedDurably(run) {
|
|
6963
|
+
if (run?.status === 'suspended')
|
|
6964
|
+
return this.owesMark(run) ? 'mark' : undefined;
|
|
6965
|
+
if (run?.status === 'running' || run?.status === 'blocked')
|
|
6966
|
+
return 'stamp';
|
|
6967
|
+
return undefined;
|
|
6968
|
+
}
|
|
6969
|
+
/**
|
|
6970
|
+
* Whether a result settled for `run` (as read when it settled) would need {@link markResumeOwed}: only
|
|
6971
|
+
* a `suspended` run whose timer is not already due within a lease (a run whose turn is in flight
|
|
6972
|
+
* gets a wake request instead, see {@link owedDurably}).
|
|
6973
|
+
*/
|
|
6974
|
+
owesMark(run) {
|
|
6869
6975
|
if (run?.status !== 'suspended')
|
|
6976
|
+
return false;
|
|
6977
|
+
return run.wakeAt === undefined || run.wakeAt > this.clock() + this.leaseMs;
|
|
6978
|
+
}
|
|
6979
|
+
/**
|
|
6980
|
+
* Mark `run` as owed a resume: pull its timer in to one lease from now (only IN — a timer already
|
|
6981
|
+
* due sooner is kept, and only while the run is still `suspended`). Leased like any due run, so a
|
|
6982
|
+
* live turn is never doubled — at worst a resume that lands after the mark replays and re-parks.
|
|
6983
|
+
* `served` reports a resume that ended meanwhile, whose mark is then dropped if not yet written.
|
|
6984
|
+
*
|
|
6985
|
+
* Batched across runs when the store has {@link StateStore.pullRunWakesIn}. Best-effort: a store that
|
|
6986
|
+
* refuses the write leaves the run exactly where it was before.
|
|
6987
|
+
*/
|
|
6988
|
+
async markResumeOwed(run, served = () => false) {
|
|
6989
|
+
if (!this.owesMark(run))
|
|
6870
6990
|
return;
|
|
6871
|
-
|
|
6872
|
-
|
|
6991
|
+
if (!this.store.pullRunWakesIn) {
|
|
6992
|
+
await this.markRunResumeOwed(run.id, this.clock() + this.leaseMs);
|
|
6873
6993
|
return;
|
|
6994
|
+
}
|
|
6995
|
+
// Batched across runs: the results of a busy instance arrive many runs at a time (a results batch
|
|
6996
|
+
// settles each of its runs concurrently), and each would otherwise cost its own UPDATE. The first
|
|
6997
|
+
// mark goes out at once; the ones arriving while it is in flight go out together in the next.
|
|
6998
|
+
await new Promise((done) => {
|
|
6999
|
+
this.owedResumes.push({ runId: run.id, served, done });
|
|
7000
|
+
if (!this.markingOwedResumes)
|
|
7001
|
+
void this.markOwedResumes();
|
|
7002
|
+
});
|
|
7003
|
+
}
|
|
7004
|
+
/** Write the queued owed-resume marks, a batch at a time, until none is left (see {@link markResumeOwed}). */
|
|
7005
|
+
async markOwedResumes() {
|
|
7006
|
+
this.markingOwedResumes = true;
|
|
7007
|
+
try {
|
|
7008
|
+
while (this.owedResumes.length > 0) {
|
|
7009
|
+
const batch = this.owedResumes.splice(0, OWED_RESUME_BATCH_MAX);
|
|
7010
|
+
// One lease from the write, not from each mark's arrival: never later than a single write.
|
|
7011
|
+
const owedBy = this.clock() + this.leaseMs;
|
|
7012
|
+
// A resume that ended while its mark queued owes nothing any more.
|
|
7013
|
+
const runIds = batch.filter((entry) => !entry.served()).map((entry) => entry.runId);
|
|
7014
|
+
if (runIds.length === 0) {
|
|
7015
|
+
for (const entry of batch)
|
|
7016
|
+
entry.done();
|
|
7017
|
+
continue;
|
|
7018
|
+
}
|
|
7019
|
+
try {
|
|
7020
|
+
await this.store.pullRunWakesIn?.(runIds, owedBy);
|
|
7021
|
+
}
|
|
7022
|
+
catch {
|
|
7023
|
+
// A batch the store refused (a deadlock victim, a dropped connection) is retried one run at
|
|
7024
|
+
// a time, so one bad row costs no other run its mark.
|
|
7025
|
+
for (const runId of new Set(runIds))
|
|
7026
|
+
await this.markRunResumeOwed(runId, owedBy);
|
|
7027
|
+
}
|
|
7028
|
+
for (const entry of batch)
|
|
7029
|
+
entry.done();
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
7032
|
+
finally {
|
|
7033
|
+
this.markingOwedResumes = false;
|
|
7034
|
+
}
|
|
7035
|
+
}
|
|
7036
|
+
/** {@link markResumeOwed} for one run, through {@link StateStore.updateRunIf}. Best-effort. */
|
|
7037
|
+
async markRunResumeOwed(runId, owedBy) {
|
|
6874
7038
|
try {
|
|
6875
|
-
await this.store.updateRunIf(
|
|
7039
|
+
await this.store.updateRunIf(runId, ['suspended'], { wakeAt: owedBy });
|
|
6876
7040
|
}
|
|
6877
7041
|
catch (err) {
|
|
6878
|
-
console.warn(`[adonis-durable] could not mark run ${
|
|
7042
|
+
console.warn(`[adonis-durable] could not mark run ${runId} as owed a resume (it recovers on its next reconcile wake): ${err instanceof Error ? err.message : String(err)}`);
|
|
6879
7043
|
}
|
|
6880
7044
|
}
|
|
6881
|
-
resumeInBackground(runId, fan) {
|
|
6882
|
-
this.trackEffect(this.resumeImplicit(runId, fan).catch((err) => {
|
|
6883
|
-
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)}`);
|
|
6884
|
-
}));
|
|
6885
|
-
}
|
|
6886
7045
|
/**
|
|
6887
7046
|
* Settle a batch of one run's step results; per result, `true` (settled or dropped) or the error
|
|
6888
7047
|
* that kept it from settling, which the transport then redelivers. Same outcome per result as
|
|
@@ -6998,9 +7157,8 @@ export class WorkflowEngine {
|
|
|
6998
7157
|
for (const { cp, result } of resumable) {
|
|
6999
7158
|
const fan = parseStepFanGroup(cp.parallelGroup);
|
|
7000
7159
|
if (!fan || result.status === 'failed') {
|
|
7001
|
-
await this.markResumeOwed(run);
|
|
7002
7160
|
this.noteSettledHere(runId, singles);
|
|
7003
|
-
this.
|
|
7161
|
+
await this.resumeOwed(run, runId);
|
|
7004
7162
|
return outcomes;
|
|
7005
7163
|
}
|
|
7006
7164
|
let closed = barriers.get(fan.firstSeq);
|
|
@@ -7012,9 +7170,8 @@ export class WorkflowEngine {
|
|
|
7012
7170
|
barriers.set(fan.firstSeq, closed);
|
|
7013
7171
|
}
|
|
7014
7172
|
if (!(await closed)) {
|
|
7015
|
-
await this.markResumeOwed(run);
|
|
7016
7173
|
this.noteSettledHere(runId, singles);
|
|
7017
|
-
this.
|
|
7174
|
+
await this.resumeOwed(run, runId, {
|
|
7018
7175
|
first: fan.firstSeq,
|
|
7019
7176
|
seq: highest.get(fan.firstSeq) ?? cp.seq,
|
|
7020
7177
|
});
|