@adonis-agora/durable 0.52.2 → 0.52.4
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 +16 -0
- package/dist/assets/spa/assets/{index-DupjrK7N.js → index-BEk3-95l.js} +3 -3
- package/dist/assets/spa/assets/index-BEk3-95l.js.map +1 -0
- package/dist/assets/spa/index.html +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/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 +4 -1
- package/dist/src/dashboard/openapi.js.map +1 -1
- package/dist/src/dashboard/run-filter.d.ts +1 -1
- package/dist/src/dashboard/run-filter.js +1 -1
- package/dist/src/engine.d.ts +24 -9
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +111 -23
- package/dist/src/engine.js.map +1 -1
- package/dist/src/handshake/descriptor.d.ts +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/interfaces.d.ts +24 -5
- package/dist/src/interfaces.d.ts.map +1 -1
- package/dist/src/interfaces.js.map +1 -1
- package/dist/src/run-value-facets.js +1 -1
- package/dist/src/run-value-facets.js.map +1 -1
- package/dist/src/stores/lucid-mappers.d.ts +2 -0
- package/dist/src/stores/lucid-mappers.d.ts.map +1 -1
- package/dist/src/stores/lucid-mappers.js +5 -0
- package/dist/src/stores/lucid-mappers.js.map +1 -1
- package/dist/src/stores/lucid-schema.d.ts +1 -1
- package/dist/src/stores/lucid-schema.d.ts.map +1 -1
- package/dist/src/stores/lucid-schema.js +13 -2
- package/dist/src/stores/lucid-schema.js.map +1 -1
- package/dist/src/stores/lucid.d.ts +6 -0
- package/dist/src/stores/lucid.d.ts.map +1 -1
- package/dist/src/stores/lucid.js +55 -2
- 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 +11 -2
- package/dist/src/testing/in-memory-state-store.js.map +1 -1
- package/dist/src/testing-kit/run-dispatch-contract.d.ts +86 -1
- package/dist/src/testing-kit/run-dispatch-contract.d.ts.map +1 -1
- package/dist/src/testing-kit/run-dispatch-contract.js +347 -0
- package/dist/src/testing-kit/run-dispatch-contract.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 +39 -0
- package/dist/src/testing-kit/state-store-conformance.js.map +1 -1
- package/dist/src/workflow-ctx.d.ts.map +1 -1
- package/dist/src/workflow-ctx.js +21 -18
- package/dist/src/workflow-ctx.js.map +1 -1
- package/dist/src/workflow-ref.d.ts +1 -1
- package/package.json +2 -2
- package/dist/assets/spa/assets/index-DupjrK7N.js.map +0 -1
package/dist/src/engine.js
CHANGED
|
@@ -110,7 +110,6 @@ const PARENT_NOTIFY_BACKOFF_MS = 250;
|
|
|
110
110
|
/** How often, at most, and how many times a wake dropped on a held lease looks again (see `recheckAfterLease`). */
|
|
111
111
|
const LEASE_RECHECK_MAX_MS = 2_000;
|
|
112
112
|
const LEASE_RECHECK_SLACK_MS = 100;
|
|
113
|
-
const LEASE_RECHECK_ATTEMPTS = 30;
|
|
114
113
|
/**
|
|
115
114
|
* How soon (ms) orphan recovery re-drives a REMOTE workflow run found `running` with a call in flight:
|
|
116
115
|
* its turn was lost (an inline advance timed out, or the replica applying a decision died), so it is
|
|
@@ -2512,8 +2511,11 @@ export class WorkflowEngine {
|
|
|
2512
2511
|
continue;
|
|
2513
2512
|
}
|
|
2514
2513
|
try {
|
|
2514
|
+
// The lease-long `wakeAt` keeps the run due if the wake is lost on the way, so a refused
|
|
2515
|
+
// wake's request it answers is not needed any more (and would keep it due every poll).
|
|
2515
2516
|
const claimed = await this.store.updateRunIf(run.id, [run.status], {
|
|
2516
2517
|
wakeAt: nowMs + this.leaseMs,
|
|
2518
|
+
...(run.wakeRequestedAt !== undefined ? { wakeRequestedAt: undefined } : {}),
|
|
2517
2519
|
});
|
|
2518
2520
|
if (!claimed)
|
|
2519
2521
|
continue;
|
|
@@ -2560,7 +2562,7 @@ export class WorkflowEngine {
|
|
|
2560
2562
|
if (!acquired) {
|
|
2561
2563
|
// A wake (not a start) that lost the lease race may postdate the holder's snapshot.
|
|
2562
2564
|
if (recheck)
|
|
2563
|
-
this.
|
|
2565
|
+
this.trackEffect(this.requestWake(runId, undefined));
|
|
2564
2566
|
return null;
|
|
2565
2567
|
}
|
|
2566
2568
|
// resume() checks the namespace and throws NamespaceMismatch when it doesn't match (releasing the
|
|
@@ -3615,41 +3617,107 @@ export class WorkflowEngine {
|
|
|
3615
3617
|
});
|
|
3616
3618
|
}
|
|
3617
3619
|
/**
|
|
3618
|
-
* A wake that found the run leased by another instance
|
|
3619
|
-
*
|
|
3620
|
-
*
|
|
3621
|
-
*
|
|
3622
|
-
*
|
|
3620
|
+
* A wake that found the run leased by another instance (its turn is executing) asks for another
|
|
3621
|
+
* turn, durably: it stamps {@link WorkflowRun.wakeRequestedAt} on the run. The holder does not let
|
|
3622
|
+
* go of the lease while the stamp is there ({@link releaseSuspendedLease}) and runs another turn,
|
|
3623
|
+
* which sees what the wake wrote; and a `suspended` run carrying it is due for the timer poll, so
|
|
3624
|
+
* the request survives both this instance and the holder going down.
|
|
3623
3625
|
*
|
|
3624
|
-
*
|
|
3625
|
-
*
|
|
3626
|
-
*
|
|
3627
|
-
*
|
|
3626
|
+
* The holder may have let go between the refused lease and the stamp: nobody but the timer poll
|
|
3627
|
+
* would see it then, so the lease is tried once more here. The in-memory re-check
|
|
3628
|
+
* ({@link recheckAfterLease}) runs too, as the fast path when the holder goes down mid-turn; it
|
|
3629
|
+
* stops as soon as a turn served the request.
|
|
3628
3630
|
*/
|
|
3629
|
-
|
|
3631
|
+
async requestWake(runId, lockedUntil) {
|
|
3632
|
+
if (this.draining)
|
|
3633
|
+
return;
|
|
3634
|
+
let marked;
|
|
3635
|
+
try {
|
|
3636
|
+
marked = await this.store.updateRunIf(runId, ['running', 'suspended', 'blocked'], {
|
|
3637
|
+
wakeRequestedAt: this.clock(),
|
|
3638
|
+
});
|
|
3639
|
+
}
|
|
3640
|
+
catch {
|
|
3641
|
+
// Not kept (a schema without the column, a store error): the in-memory re-check is all there is.
|
|
3642
|
+
this.recheckAfterLease(runId, lockedUntil);
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
if (!marked)
|
|
3646
|
+
return;
|
|
3647
|
+
// The fast path for a holder that went down mid-turn: look again in memory too (it stops once a
|
|
3648
|
+
// turn served the request).
|
|
3649
|
+
this.recheckAfterLease(runId, lockedUntil, 1, true);
|
|
3650
|
+
const nowMs = this.clock();
|
|
3651
|
+
if (!(await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs)))
|
|
3652
|
+
return;
|
|
3653
|
+
const fresh = await this.store.getRun(runId);
|
|
3654
|
+
if (fresh?.status !== 'suspended' && fresh?.status !== 'blocked') {
|
|
3655
|
+
await this.store.releaseRunLock(runId, this.instanceId);
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
await this.resumeImplicit(runId).catch((err) => {
|
|
3659
|
+
if (!(err instanceof NamespaceMismatch))
|
|
3660
|
+
throw err;
|
|
3661
|
+
});
|
|
3662
|
+
}
|
|
3663
|
+
/**
|
|
3664
|
+
* Let go of the lease of a run this instance's turn just suspended, unless a wake refused during
|
|
3665
|
+
* the turn asked for another one ({@link requestWake}): then keep it and resolve `false`, and the
|
|
3666
|
+
* caller runs that turn. Through the store's conditional release when it has one; otherwise
|
|
3667
|
+
* release, then read the run back and wake it if a request landed meanwhile.
|
|
3668
|
+
*/
|
|
3669
|
+
async releaseSuspendedLease(runId) {
|
|
3670
|
+
const conditional = this.store.releaseRunLockUnlessWakeRequested;
|
|
3671
|
+
if (conditional)
|
|
3672
|
+
return conditional.call(this.store, runId, this.instanceId);
|
|
3673
|
+
const before = await this.store.getRun(runId);
|
|
3674
|
+
if (before?.wakeRequestedAt !== undefined && before.lockedBy === this.instanceId)
|
|
3675
|
+
return false;
|
|
3676
|
+
await this.store.releaseRunLock(runId, this.instanceId);
|
|
3677
|
+
const after = await this.store.getRun(runId);
|
|
3678
|
+
if (after?.wakeRequestedAt !== undefined)
|
|
3679
|
+
this.trackEffect(this.wake(runId));
|
|
3680
|
+
return true;
|
|
3681
|
+
}
|
|
3682
|
+
/**
|
|
3683
|
+
* The in-memory half of a refused wake, for a store that cannot keep the request durably (see
|
|
3684
|
+
* {@link requestWake}): look again every {@link LEASE_RECHECK_MAX_MS} at most for as long as the
|
|
3685
|
+
* run is leased, whatever its status (a wake that lands mid-turn finds it `running`). Once the lease
|
|
3686
|
+
* is free, a run that is `suspended` or `blocked` is woken. A holder that already moved the run on
|
|
3687
|
+
* makes this one spare replay that re-parks.
|
|
3688
|
+
*/
|
|
3689
|
+
recheckAfterLease(runId, lockedUntil, attempt = 1, stamped = false) {
|
|
3630
3690
|
if (this.draining || this.leaseRetries.has(runId))
|
|
3631
3691
|
return;
|
|
3632
3692
|
const remaining = Math.max(0, (lockedUntil ?? this.clock()) - this.clock());
|
|
3633
3693
|
const timer = setTimeout(() => {
|
|
3634
3694
|
this.leaseRetries.delete(runId);
|
|
3635
|
-
this.trackEffect(this.retryDroppedWake(runId, attempt));
|
|
3695
|
+
this.trackEffect(this.retryDroppedWake(runId, attempt, stamped));
|
|
3636
3696
|
}, Math.min(remaining, LEASE_RECHECK_MAX_MS) + LEASE_RECHECK_SLACK_MS);
|
|
3637
3697
|
timer.unref?.();
|
|
3638
3698
|
this.leaseRetries.set(runId, timer);
|
|
3639
3699
|
}
|
|
3640
|
-
async retryDroppedWake(runId, attempt) {
|
|
3700
|
+
async retryDroppedWake(runId, attempt, stamped) {
|
|
3641
3701
|
if (this.draining)
|
|
3642
3702
|
return;
|
|
3643
3703
|
const run = await this.store.getRun(runId);
|
|
3644
|
-
if (run
|
|
3704
|
+
if (!run || isTerminalRun(run.status))
|
|
3705
|
+
return;
|
|
3706
|
+
// A turn that started after the request served it (it cleared the stamp). Only when the request
|
|
3707
|
+
// was stamped: a wake whose stamp could not be written has nothing else looking for it.
|
|
3708
|
+
if (stamped && run.wakeRequestedAt === undefined)
|
|
3645
3709
|
return;
|
|
3646
3710
|
const nowMs = this.clock();
|
|
3711
|
+
// Still leased: the holder's turn is still executing (status `running`), or another wake's turn
|
|
3712
|
+
// took it. Look again; giving up here is how a wake that landed mid-turn was lost.
|
|
3647
3713
|
if (run.lockedUntil !== undefined && run.lockedUntil > nowMs) {
|
|
3648
|
-
|
|
3649
|
-
this.recheckAfterLease(runId, run.lockedUntil, attempt + 1);
|
|
3650
|
-
}
|
|
3714
|
+
this.recheckAfterLease(runId, run.lockedUntil, attempt + 1, stamped);
|
|
3651
3715
|
return;
|
|
3652
3716
|
}
|
|
3717
|
+
// Free, and `pending` (its dispatch or the pending-run poll runs it) or `running` with no lease
|
|
3718
|
+
// (a holder that died mid-turn: crash recovery takes it): nothing for a wake to do.
|
|
3719
|
+
if (run.status !== 'suspended' && run.status !== 'blocked')
|
|
3720
|
+
return;
|
|
3653
3721
|
if (this.runQueue) {
|
|
3654
3722
|
this.enqueueRun({
|
|
3655
3723
|
runId,
|
|
@@ -3661,7 +3729,7 @@ export class WorkflowEngine {
|
|
|
3661
3729
|
if (!(await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs)))
|
|
3662
3730
|
return;
|
|
3663
3731
|
const fresh = await this.store.getRun(runId);
|
|
3664
|
-
if (fresh?.status !== 'suspended') {
|
|
3732
|
+
if (fresh?.status !== 'suspended' && fresh?.status !== 'blocked') {
|
|
3665
3733
|
await this.store.releaseRunLock(runId, this.instanceId);
|
|
3666
3734
|
return;
|
|
3667
3735
|
}
|
|
@@ -3680,10 +3748,15 @@ export class WorkflowEngine {
|
|
|
3680
3748
|
const lockNow = this.clock();
|
|
3681
3749
|
if (run.lockedBy !== this.instanceId || (run.lockedUntil ?? 0) <= lockNow) {
|
|
3682
3750
|
if (!(await this.store.tryLockRun(run.id, this.instanceId, lockNow + this.leaseMs, lockNow))) {
|
|
3683
|
-
this.
|
|
3751
|
+
this.trackEffect(this.requestWake(run.id, run.lockedUntil));
|
|
3684
3752
|
return { runId: run.id, status: run.status };
|
|
3685
3753
|
}
|
|
3686
3754
|
}
|
|
3755
|
+
// This turn reads the run's checkpoints after this point, so it sees whatever the wakes that
|
|
3756
|
+
// asked for it wrote: their request is served.
|
|
3757
|
+
if (run.wakeRequestedAt !== undefined) {
|
|
3758
|
+
await this.store.updateRun(run.id, { wakeRequestedAt: undefined });
|
|
3759
|
+
}
|
|
3687
3760
|
const lease = { handedOff: false };
|
|
3688
3761
|
this.executionLeases.set(run.id, lease);
|
|
3689
3762
|
const renew = setInterval(() => {
|
|
@@ -5128,6 +5201,7 @@ export class WorkflowEngine {
|
|
|
5128
5201
|
for (const cp of snapshot)
|
|
5129
5202
|
replay.set(cp.seq, cp);
|
|
5130
5203
|
let suspendedOnFan;
|
|
5204
|
+
let settledSuspended = false;
|
|
5131
5205
|
const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow);
|
|
5132
5206
|
// Every settle below is `return await`ed, never a bare `return this.settleRun(...)`: inside
|
|
5133
5207
|
// `try … finally` a bare return runs the `finally` — which RELEASES THE LEASE — as soon as the
|
|
@@ -5220,8 +5294,10 @@ export class WorkflowEngine {
|
|
|
5220
5294
|
if (settled.status === 'suspended' && err.stepFans?.length) {
|
|
5221
5295
|
this.recheckStepFans(run.id, err.stepFans);
|
|
5222
5296
|
}
|
|
5223
|
-
if (settled.status === 'suspended')
|
|
5297
|
+
if (settled.status === 'suspended') {
|
|
5224
5298
|
suspendedOnFan = err.fan;
|
|
5299
|
+
settledSuspended = true;
|
|
5300
|
+
}
|
|
5225
5301
|
return settled;
|
|
5226
5302
|
}
|
|
5227
5303
|
// A cancel in flight that surfaced as a thrown error settles `cancelled`, not `failed`: the saga
|
|
@@ -5262,13 +5338,25 @@ export class WorkflowEngine {
|
|
|
5262
5338
|
// Letting it go is safe by the same argument `releaseInflightLocks` already makes: the
|
|
5263
5339
|
// lease is a lease. It expires, and `recoverIncomplete` re-drives whatever is still held.
|
|
5264
5340
|
// A lost release costs one lease window of latency; a masked error costs the diagnosis.
|
|
5341
|
+
//
|
|
5342
|
+
// A suspended turn keeps the lease instead when a wake it refused asked for another turn, and
|
|
5343
|
+
// runs that turn (see `requestWake`).
|
|
5344
|
+
let released = true;
|
|
5265
5345
|
try {
|
|
5266
|
-
|
|
5346
|
+
if (settledSuspended)
|
|
5347
|
+
released = await this.releaseSuspendedLease(run.id);
|
|
5348
|
+
else
|
|
5349
|
+
await this.store.releaseRunLock(run.id, this.instanceId);
|
|
5267
5350
|
}
|
|
5268
5351
|
catch (releaseErr) {
|
|
5269
5352
|
console.warn(`[adonis-durable] releasing the lease of run ${run.id} failed (it expires on its own and recovery re-drives): ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
|
|
5270
5353
|
}
|
|
5271
|
-
if (
|
|
5354
|
+
if (!released) {
|
|
5355
|
+
this.trackEffect(this.resumeImplicit(run.id).catch((err) => {
|
|
5356
|
+
console.warn(`[adonis-durable] the turn a refused wake asked for of run ${run.id} failed (it is due for the timer poll): ${err instanceof Error ? err.message : String(err)}`);
|
|
5357
|
+
}));
|
|
5358
|
+
}
|
|
5359
|
+
else if (suspendedOnFan) {
|
|
5272
5360
|
const fan = suspendedOnFan;
|
|
5273
5361
|
this.trackEffect(this.redriveSettledChildren(run.id, fan));
|
|
5274
5362
|
}
|