@dudousxd/nestjs-durable-core 0.81.0 → 0.82.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 +285 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +191 -14
- package/dist/index.d.ts +191 -14
- package/dist/index.js +282 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2816,6 +2816,66 @@ var SingletonGate = class {
|
|
|
2816
2816
|
}
|
|
2817
2817
|
};
|
|
2818
2818
|
|
|
2819
|
+
// src/stalled-runs.ts
|
|
2820
|
+
var DEFAULT_STALLED_AFTER_MS = 15 * 6e4;
|
|
2821
|
+
async function findStalledRuns(store, options = {}) {
|
|
2822
|
+
const now = options.now ?? Date.now();
|
|
2823
|
+
const thresholdMs = options.thresholdMs ?? DEFAULT_STALLED_AFTER_MS;
|
|
2824
|
+
const scan = Math.max(1, options.scan ?? 1e3);
|
|
2825
|
+
const pageSize = Math.min(200, scan);
|
|
2826
|
+
const stalled = [];
|
|
2827
|
+
for (let offset = 0; offset < scan; offset += pageSize) {
|
|
2828
|
+
const limit = Math.min(pageSize, scan - offset);
|
|
2829
|
+
const page = await store.listRuns({
|
|
2830
|
+
statuses: [
|
|
2831
|
+
"running",
|
|
2832
|
+
"suspended"
|
|
2833
|
+
],
|
|
2834
|
+
...options.namespace !== void 0 ? {
|
|
2835
|
+
namespace: options.namespace
|
|
2836
|
+
} : {},
|
|
2837
|
+
limit,
|
|
2838
|
+
offset
|
|
2839
|
+
});
|
|
2840
|
+
for (const run of page) {
|
|
2841
|
+
const ageMs = now - run.updatedAt.getTime();
|
|
2842
|
+
if (ageMs < thresholdMs) continue;
|
|
2843
|
+
const info2 = await stalledInfo(store, run, now, thresholdMs, ageMs);
|
|
2844
|
+
if (info2) stalled.push(info2);
|
|
2845
|
+
}
|
|
2846
|
+
if (page.length < limit) break;
|
|
2847
|
+
}
|
|
2848
|
+
return stalled;
|
|
2849
|
+
}
|
|
2850
|
+
__name(findStalledRuns, "findStalledRuns");
|
|
2851
|
+
async function stalledInfo(store, run, now, thresholdMs, ageMs) {
|
|
2852
|
+
let stalePending = null;
|
|
2853
|
+
for (const cp of await store.listCheckpoints(run.id)) {
|
|
2854
|
+
if (cp.kind !== "remote" || cp.status !== "pending") continue;
|
|
2855
|
+
const stepAgeMs = now - cp.enqueuedAt.getTime();
|
|
2856
|
+
if (stepAgeMs < thresholdMs) continue;
|
|
2857
|
+
if (cp.wakeAt != null && cp.wakeAt > now) continue;
|
|
2858
|
+
if (!stalePending || stepAgeMs > stalePending.ageMs) {
|
|
2859
|
+
stalePending = {
|
|
2860
|
+
seq: cp.seq,
|
|
2861
|
+
name: cp.name,
|
|
2862
|
+
attempts: cp.attempts,
|
|
2863
|
+
ageMs: stepAgeMs,
|
|
2864
|
+
leaseUntil: cp.wakeAt ?? null
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
const wakeForever = run.status === "suspended" && run.wakeAt == null;
|
|
2869
|
+
if (!wakeForever && !stalePending) return null;
|
|
2870
|
+
return {
|
|
2871
|
+
run,
|
|
2872
|
+
ageMs,
|
|
2873
|
+
wakeForever,
|
|
2874
|
+
stalePending
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
2877
|
+
__name(stalledInfo, "stalledInfo");
|
|
2878
|
+
|
|
2819
2879
|
// src/transport-pool.ts
|
|
2820
2880
|
var TransportPool = class {
|
|
2821
2881
|
static {
|
|
@@ -4173,6 +4233,18 @@ var WorkflowEngine = class {
|
|
|
4173
4233
|
stepQueue = /* @__PURE__ */ new Map();
|
|
4174
4234
|
/** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
|
|
4175
4235
|
inflight = /* @__PURE__ */ new Set();
|
|
4236
|
+
/**
|
|
4237
|
+
* The run lease of each execution in flight on THIS instance, by run id — renewed while it runs
|
|
4238
|
+
* and checked by every settle (the zombie fence, see {@link ExecutionLease}). Also what `drain()`
|
|
4239
|
+
* releases when its budget runs out.
|
|
4240
|
+
*/
|
|
4241
|
+
executionLeases = /* @__PURE__ */ new Map();
|
|
4242
|
+
/** Pager hooks for stranded runs (see {@link onStalled}). */
|
|
4243
|
+
stalledListeners = /* @__PURE__ */ new Set();
|
|
4244
|
+
/** Runs already paged about, by id → the `updatedAt` of the paged episode (see {@link sweepStalled}). */
|
|
4245
|
+
notifiedStalled = /* @__PURE__ */ new Map();
|
|
4246
|
+
lastStalledSweepAt = Number.NEGATIVE_INFINITY;
|
|
4247
|
+
stalledAfterMs;
|
|
4176
4248
|
draining = false;
|
|
4177
4249
|
/** Wait already spent on this shutdown before `drain()` (see `beginDrain`). */
|
|
4178
4250
|
drainSpentMs = 0;
|
|
@@ -4197,6 +4269,8 @@ var WorkflowEngine = class {
|
|
|
4197
4269
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
4198
4270
|
this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
|
|
4199
4271
|
this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
|
|
4272
|
+
const stalledAfter = deps.stalledAfterMs;
|
|
4273
|
+
this.stalledAfterMs = stalledAfter !== void 0 && Number.isFinite(stalledAfter) ? Math.max(6e4, stalledAfter) : DEFAULT_STALLED_AFTER_MS;
|
|
4200
4274
|
const batch = deps.timerBatchSize;
|
|
4201
4275
|
this.timerBatchSize = batch !== void 0 && Number.isFinite(batch) && batch >= 1 ? Math.floor(batch) : 500;
|
|
4202
4276
|
this.eventBufferTtlMs = deps.eventBufferTtlMs || void 0;
|
|
@@ -4540,6 +4614,64 @@ var WorkflowEngine = class {
|
|
|
4540
4614
|
this.deadListeners.add(listener);
|
|
4541
4615
|
return () => this.deadListeners.delete(listener);
|
|
4542
4616
|
}
|
|
4617
|
+
/**
|
|
4618
|
+
* Register a pager hook for STRANDED runs: an in-flight run untouched past `stalledAfterMs` that is
|
|
4619
|
+
* either suspended with NO wake timer (wake-forever: nothing will ever re-drive it) or parked on an
|
|
4620
|
+
* old pending remote step with no live lease (dispatched and, most likely, lost). The timer poller
|
|
4621
|
+
* sweeps for these ({@link sweepStalled}, throttled to one pass a minute) and notifies each episode
|
|
4622
|
+
* ONCE per instance: a run is paged again only after it makes progress and strands again. Wire your
|
|
4623
|
+
* alerting here like {@link onDead}. A listener that throws never breaks the sweep. Returns an
|
|
4624
|
+
* unsubscribe function.
|
|
4625
|
+
*/
|
|
4626
|
+
onStalled(listener) {
|
|
4627
|
+
this.stalledListeners.add(listener);
|
|
4628
|
+
return () => this.stalledListeners.delete(listener);
|
|
4629
|
+
}
|
|
4630
|
+
/**
|
|
4631
|
+
* Detect stranded in-flight runs and notify the {@link onStalled} listeners. Self-throttled to one
|
|
4632
|
+
* pass a minute, skipped while draining, and a no-op without listeners — call it every tick (the
|
|
4633
|
+
* NestJS timer poller does). Scoped to this engine's `namespace` (an operator engine with no
|
|
4634
|
+
* namespace sweeps every namespace). Returns the runs notified THIS pass.
|
|
4635
|
+
*/
|
|
4636
|
+
async sweepStalled(now = this.clock()) {
|
|
4637
|
+
if (this.stalledListeners.size === 0 || this.draining) return [];
|
|
4638
|
+
if (now - this.lastStalledSweepAt < 6e4) return [];
|
|
4639
|
+
this.lastStalledSweepAt = now;
|
|
4640
|
+
const notified = [];
|
|
4641
|
+
for (const info2 of await this.listStalledRuns({
|
|
4642
|
+
now
|
|
4643
|
+
})) {
|
|
4644
|
+
const episode = info2.run.updatedAt.getTime();
|
|
4645
|
+
if (this.notifiedStalled.get(info2.run.id) === episode) continue;
|
|
4646
|
+
this.notifiedStalled.set(info2.run.id, episode);
|
|
4647
|
+
if (this.notifiedStalled.size > 1e3) this.notifiedStalled.clear();
|
|
4648
|
+
for (const listener of this.stalledListeners) {
|
|
4649
|
+
try {
|
|
4650
|
+
listener(info2);
|
|
4651
|
+
} catch {
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
notified.push(info2);
|
|
4655
|
+
}
|
|
4656
|
+
return notified;
|
|
4657
|
+
}
|
|
4658
|
+
/**
|
|
4659
|
+
* The stranded runs right now, without notifying anyone: the query behind {@link sweepStalled} and
|
|
4660
|
+
* the CLI's `durable inspect --stale`. A `running`/`suspended` run qualifies when it has been
|
|
4661
|
+
* untouched for `thresholdMs` (default `stalledAfterMs`) AND either it is suspended with no wake
|
|
4662
|
+
* timer, or its oldest pending remote step is older than the threshold with no lease in the future
|
|
4663
|
+
* (a step with a `timeoutMs`, a heartbeat or `remoteRedispatchMs` keeps a lease; a long
|
|
4664
|
+
* `ctx.sleep` has a future `wakeAt`; neither is stranded). Scans up to `scan` candidates (default
|
|
4665
|
+
* 1000), newest first, in this engine's namespace.
|
|
4666
|
+
*/
|
|
4667
|
+
async listStalledRuns(options = {}) {
|
|
4668
|
+
return findStalledRuns(this.store, {
|
|
4669
|
+
now: options.now ?? this.clock(),
|
|
4670
|
+
thresholdMs: options.thresholdMs ?? this.stalledAfterMs,
|
|
4671
|
+
scan: options.scan,
|
|
4672
|
+
namespace: this.namespace
|
|
4673
|
+
});
|
|
4674
|
+
}
|
|
4543
4675
|
notifyDead(run) {
|
|
4544
4676
|
for (const fn of this.deadListeners) {
|
|
4545
4677
|
try {
|
|
@@ -4698,6 +4830,11 @@ var WorkflowEngine = class {
|
|
|
4698
4830
|
getRun(runId) {
|
|
4699
4831
|
return this.store.getRun(runId);
|
|
4700
4832
|
}
|
|
4833
|
+
/** A run's full checkpoint timeline, ordered by seq. A thin pass-through to the store (it is what
|
|
4834
|
+
* the testing kit's `captureHistory` reads, next to {@link getRun}). */
|
|
4835
|
+
listCheckpoints(runId) {
|
|
4836
|
+
return this.store.listCheckpoints(runId);
|
|
4837
|
+
}
|
|
4701
4838
|
/**
|
|
4702
4839
|
* Re-execute a run's body (replaying its checkpoints). This is the EXPLICIT entry point: besides
|
|
4703
4840
|
* driving a live run, it re-drives a `failed` run — the manual retry (prefer {@link requeue}, which
|
|
@@ -4783,6 +4920,8 @@ var WorkflowEngine = class {
|
|
|
4783
4920
|
* diagnosis.
|
|
4784
4921
|
*/
|
|
4785
4922
|
async releaseLeaseBestEffort(runId) {
|
|
4923
|
+
if (this.isFenced(runId)) return;
|
|
4924
|
+
this.handOffLease(runId);
|
|
4786
4925
|
try {
|
|
4787
4926
|
await this.store.releaseRunLock(runId);
|
|
4788
4927
|
} catch (err) {
|
|
@@ -5032,17 +5171,47 @@ var WorkflowEngine = class {
|
|
|
5032
5171
|
rearm();
|
|
5033
5172
|
});
|
|
5034
5173
|
this.drainRearms.add(rearm);
|
|
5174
|
+
let settled = false;
|
|
5035
5175
|
try {
|
|
5036
5176
|
await Promise.race([
|
|
5037
5177
|
Promise.allSettled([
|
|
5038
5178
|
...this.inflight
|
|
5039
|
-
])
|
|
5179
|
+
]).then(() => {
|
|
5180
|
+
settled = true;
|
|
5181
|
+
}),
|
|
5040
5182
|
timer
|
|
5041
5183
|
]);
|
|
5042
5184
|
} finally {
|
|
5043
5185
|
this.drainRearms.delete(rearm);
|
|
5044
5186
|
clearTimeout(timeout);
|
|
5045
5187
|
}
|
|
5188
|
+
if (!settled) await this.releaseInflightLeases();
|
|
5189
|
+
}
|
|
5190
|
+
/**
|
|
5191
|
+
* The drain budget ran out with executions still in flight: release their recovery leases so
|
|
5192
|
+
* another instance re-drives those runs on its next recovery pass, instead of waiting out
|
|
5193
|
+
* `leaseMs` after this process exits. Each still-running turn is FENCED first, so whatever it
|
|
5194
|
+
* computes after this point is never written over the new owner's work (see settleRun). Its
|
|
5195
|
+
* completed steps are checkpointed, so the new owner replays them; a step still executing here
|
|
5196
|
+
* may run again there, exactly as after a crash. The release is owner-scoped where the store
|
|
5197
|
+
* supports it (only a lease this instance still holds is cleared). Best effort: never throws.
|
|
5198
|
+
*/
|
|
5199
|
+
async releaseInflightLeases() {
|
|
5200
|
+
const released = [];
|
|
5201
|
+
for (const [runId, lease] of [
|
|
5202
|
+
...this.executionLeases
|
|
5203
|
+
]) {
|
|
5204
|
+
if (lease.lost || lease.handedOff || lease.done) continue;
|
|
5205
|
+
lease.lost = true;
|
|
5206
|
+
try {
|
|
5207
|
+
await this.store.releaseRunLock(runId, this.instanceId);
|
|
5208
|
+
released.push(runId);
|
|
5209
|
+
} catch {
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
if (released.length > 0) {
|
|
5213
|
+
console.warn(`[nestjs-durable] drain timed out with ${released.length} run(s) still executing; released their leases so another instance recovers them now: ${released.join(", ")}`);
|
|
5214
|
+
}
|
|
5046
5215
|
}
|
|
5047
5216
|
/**
|
|
5048
5217
|
* Cancel in-flight runs that have outlived their workflow's `executionTimeout`. Call it from the
|
|
@@ -6532,7 +6701,10 @@ var WorkflowEngine = class {
|
|
|
6532
6701
|
}
|
|
6533
6702
|
async singletonWaitResult(run) {
|
|
6534
6703
|
const current = await this.store.getRun(run.id);
|
|
6535
|
-
|
|
6704
|
+
if (!this.isFenced(run.id)) {
|
|
6705
|
+
this.handOffLease(run.id);
|
|
6706
|
+
await this.store.releaseRunLock(run.id);
|
|
6707
|
+
}
|
|
6536
6708
|
if (!current) throw new Error(`run ${run.id} was deleted during singleton admission`);
|
|
6537
6709
|
if (current.status === "suspended") {
|
|
6538
6710
|
this.emit({
|
|
@@ -6560,10 +6732,7 @@ var WorkflowEngine = class {
|
|
|
6560
6732
|
};
|
|
6561
6733
|
}
|
|
6562
6734
|
}
|
|
6563
|
-
const
|
|
6564
|
-
void this.store.renewRunLock(run.id, this.instanceId, this.clock() + this.leaseMs).catch(() => void 0);
|
|
6565
|
-
}, Math.max(50, Math.floor(this.leaseMs / 2)));
|
|
6566
|
-
renew.unref?.();
|
|
6735
|
+
const held = this.holdExecutionLease(run.id);
|
|
6567
6736
|
try {
|
|
6568
6737
|
const result = registered?.remote ? await this.runRemoteExecution(run, registered) : await this.runExecution(run, fn, registered);
|
|
6569
6738
|
if (registered?.singleton && (result.status === "completed" || result.status === "failed" || result.status === "cancelled" || result.status === "dead")) {
|
|
@@ -6571,10 +6740,59 @@ var WorkflowEngine = class {
|
|
|
6571
6740
|
}
|
|
6572
6741
|
return result;
|
|
6573
6742
|
} finally {
|
|
6574
|
-
|
|
6743
|
+
held.stop();
|
|
6575
6744
|
}
|
|
6576
6745
|
}
|
|
6577
6746
|
/**
|
|
6747
|
+
* Renew `runId`'s lease for as long as this execution holds it, and register it in
|
|
6748
|
+
* {@link executionLeases} so settles can check the zombie fence and `drain()` can release it.
|
|
6749
|
+
*
|
|
6750
|
+
* A renewal that returns `false` means the lease is gone: this turn stalled past `leaseMs` and
|
|
6751
|
+
* another instance reclaimed the run, so this executor is now a zombie. It is FENCED (`lost`): its
|
|
6752
|
+
* settles degrade to an echo of the store's state instead of clobbering the new owner's writes, and
|
|
6753
|
+
* its own lease release is skipped (it would free the new owner's lease). A renewal that THROWS (a
|
|
6754
|
+
* transient store error) is not evidence of a takeover, so it stays swallowed and the next
|
|
6755
|
+
* interval retries. A lease this execution released on purpose (`handedOff`) is not renewed, and a
|
|
6756
|
+
* renewal resolving after the execution ended (`done`) fences nothing.
|
|
6757
|
+
*/
|
|
6758
|
+
holdExecutionLease(runId) {
|
|
6759
|
+
const lease = {
|
|
6760
|
+
lost: false,
|
|
6761
|
+
handedOff: false,
|
|
6762
|
+
done: false
|
|
6763
|
+
};
|
|
6764
|
+
this.executionLeases.set(runId, lease);
|
|
6765
|
+
const renew = setInterval(() => {
|
|
6766
|
+
if (lease.handedOff || lease.lost || lease.done) return;
|
|
6767
|
+
void this.store.renewRunLock(runId, this.instanceId, this.clock() + this.leaseMs).then((renewed) => {
|
|
6768
|
+
if (renewed || lease.handedOff || lease.lost || lease.done) return;
|
|
6769
|
+
lease.lost = true;
|
|
6770
|
+
console.warn(`[nestjs-durable] lost the lease of run ${runId} while executing it (another instance took it over after it expired); this instance will not write the run's outcome`);
|
|
6771
|
+
}).catch(() => void 0);
|
|
6772
|
+
}, Math.max(50, Math.floor(this.leaseMs / 2)));
|
|
6773
|
+
renew.unref?.();
|
|
6774
|
+
return {
|
|
6775
|
+
lease,
|
|
6776
|
+
stop: /* @__PURE__ */ __name(() => {
|
|
6777
|
+
clearInterval(renew);
|
|
6778
|
+
lease.done = true;
|
|
6779
|
+
if (this.executionLeases.get(runId) === lease) this.executionLeases.delete(runId);
|
|
6780
|
+
}, "stop")
|
|
6781
|
+
};
|
|
6782
|
+
}
|
|
6783
|
+
/** Whether `runId`'s execution on this instance lost its lease (see {@link holdExecutionLease}). */
|
|
6784
|
+
isFenced(runId) {
|
|
6785
|
+
return this.executionLeases.get(runId)?.lost === true;
|
|
6786
|
+
}
|
|
6787
|
+
/**
|
|
6788
|
+
* Mark `runId`'s lease as given away on purpose by its own execution (released mid-turn), so a
|
|
6789
|
+
* renewal that now fails is not read as a takeover.
|
|
6790
|
+
*/
|
|
6791
|
+
handOffLease(runId) {
|
|
6792
|
+
const lease = this.executionLeases.get(runId);
|
|
6793
|
+
if (lease) lease.handedOff = true;
|
|
6794
|
+
}
|
|
6795
|
+
/**
|
|
6578
6796
|
* Advance a remote (cross-SDK) workflow one turn: hand its history to the executor (which dispatches
|
|
6579
6797
|
* a workflow task to the worker and awaits its replay) and apply the decision. Mirrors
|
|
6580
6798
|
* {@link runExecution}'s settle/suspend; the lease is held by {@link execute}. The result that lands
|
|
@@ -6791,6 +7009,7 @@ ${run.workflowVersion}`;
|
|
|
6791
7009
|
* keeps its own lease handling (the TS executor's `finally`, the remote path's caller).
|
|
6792
7010
|
*/
|
|
6793
7011
|
async settleRun(run, outcome) {
|
|
7012
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
6794
7013
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
6795
7014
|
if (outcome.kind === "completed") {
|
|
6796
7015
|
const applied2 = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SETTLE_ELIGIBLE_STATUSES, {
|
|
@@ -6871,6 +7090,7 @@ ${run.workflowVersion}`;
|
|
|
6871
7090
|
}
|
|
6872
7091
|
async runRemoteExecution(run, registered) {
|
|
6873
7092
|
if (run.status === "cancelling") {
|
|
7093
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
6874
7094
|
const error2 = {
|
|
6875
7095
|
message: "cancelled"
|
|
6876
7096
|
};
|
|
@@ -6879,6 +7099,7 @@ ${run.workflowVersion}`;
|
|
|
6879
7099
|
error: error2,
|
|
6880
7100
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6881
7101
|
});
|
|
7102
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
6882
7103
|
this.emit({
|
|
6883
7104
|
type: "run.failed",
|
|
6884
7105
|
runId: run.id,
|
|
@@ -6925,6 +7146,7 @@ ${run.workflowVersion}`;
|
|
|
6925
7146
|
}
|
|
6926
7147
|
const history = await this.remoteHistory(run.id, checkpoints);
|
|
6927
7148
|
if (remote.executor.dispatch) {
|
|
7149
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
6928
7150
|
const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
|
|
6929
7151
|
const silenceMs = this.remoteAdvanceSilenceMs;
|
|
6930
7152
|
const wakeAt = silenceMs != null ? this.clock() + silenceMs : void 0;
|
|
@@ -6940,6 +7162,7 @@ ${run.workflowVersion}`;
|
|
|
6940
7162
|
workflow: run.workflow,
|
|
6941
7163
|
namespace: run.namespace
|
|
6942
7164
|
});
|
|
7165
|
+
this.handOffLease(run.id);
|
|
6943
7166
|
await this.store.releaseRunLock(run.id);
|
|
6944
7167
|
await remote.executor.dispatch(run, history, taskId);
|
|
6945
7168
|
return {
|
|
@@ -6968,6 +7191,7 @@ ${run.workflowVersion}`;
|
|
|
6968
7191
|
message: err instanceof Error ? err.message : String(err),
|
|
6969
7192
|
stack: err instanceof Error ? err.stack : void 0
|
|
6970
7193
|
};
|
|
7194
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
6971
7195
|
await this.store.updateRun(run.id, {
|
|
6972
7196
|
status: "failed",
|
|
6973
7197
|
error: error2,
|
|
@@ -6996,6 +7220,7 @@ ${run.workflowVersion}`;
|
|
|
6996
7220
|
* — the caller has already ensured this is the decision for the currently-awaited turn.
|
|
6997
7221
|
*/
|
|
6998
7222
|
async applyDecision(run, decision) {
|
|
7223
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
6999
7224
|
if (decision.status === "completed") {
|
|
7000
7225
|
await this.applyCommands(run, decision.commands);
|
|
7001
7226
|
return this.settleRun(run, {
|
|
@@ -7465,6 +7690,7 @@ ${run.workflowVersion}`;
|
|
|
7465
7690
|
* cancel flag stays too, re-derived from the status on any later re-drive regardless.
|
|
7466
7691
|
*/
|
|
7467
7692
|
async compensateAndCancel(run, compensations, replay) {
|
|
7693
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
7468
7694
|
const pending = await this.unwindCompensations(run, compensations, replay);
|
|
7469
7695
|
if (pending) return {
|
|
7470
7696
|
runId: run.id,
|
|
@@ -7556,6 +7782,7 @@ ${run.workflowVersion}`;
|
|
|
7556
7782
|
});
|
|
7557
7783
|
} catch (err) {
|
|
7558
7784
|
if (err instanceof ContinueAsNew) {
|
|
7785
|
+
if (this.isFenced(run.id)) return await this.echoCurrentStatus(run.id);
|
|
7559
7786
|
if (this.cancelRequested.has(run.id)) {
|
|
7560
7787
|
return await this.compensateAndCancel(run, compensations, replay);
|
|
7561
7788
|
}
|
|
@@ -8007,6 +8234,7 @@ ${run.workflowVersion}`;
|
|
|
8007
8234
|
* own `finally` also releases (the run-execution path) is safe.
|
|
8008
8235
|
*/
|
|
8009
8236
|
async parkBlocked(run, blocked) {
|
|
8237
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
8010
8238
|
const error2 = {
|
|
8011
8239
|
message: blocked.reason,
|
|
8012
8240
|
code: blocked.code,
|
|
@@ -8593,10 +8821,7 @@ ${run.workflowVersion}`;
|
|
|
8593
8821
|
if (!await this.store.tryLockRun(decision.runId, this.instanceId, nowMs + this.leaseMs, nowMs)) {
|
|
8594
8822
|
return;
|
|
8595
8823
|
}
|
|
8596
|
-
const
|
|
8597
|
-
void this.store.renewRunLock(decision.runId, this.instanceId, this.clock() + this.leaseMs).catch(() => void 0);
|
|
8598
|
-
}, Math.max(50, Math.floor(this.leaseMs / 2)));
|
|
8599
|
-
renew.unref?.();
|
|
8824
|
+
const held = this.holdExecutionLease(decision.runId);
|
|
8600
8825
|
try {
|
|
8601
8826
|
const fresh = await this.store.getRun(decision.runId);
|
|
8602
8827
|
if (!fresh || fresh.awaitingDecisionTaskId !== decision.taskId) return;
|
|
@@ -8611,8 +8836,9 @@ ${run.workflowVersion}`;
|
|
|
8611
8836
|
fresh.awaitingDecisionTaskId = void 0;
|
|
8612
8837
|
await this.applyDecision(fresh, decision);
|
|
8613
8838
|
} finally {
|
|
8614
|
-
|
|
8615
|
-
|
|
8839
|
+
const fenced = held.lease.lost;
|
|
8840
|
+
held.stop();
|
|
8841
|
+
if (!fenced) await this.store.releaseRunLock(decision.runId);
|
|
8616
8842
|
}
|
|
8617
8843
|
}
|
|
8618
8844
|
/** Earliest clock time a suspended turn's deadline is worth LOOKING at again (`runId` → clock ms) —
|
|
@@ -8808,6 +9034,9 @@ function childIdsOf(checkpoints) {
|
|
|
8808
9034
|
}
|
|
8809
9035
|
__name(childIdsOf, "childIdsOf");
|
|
8810
9036
|
|
|
9037
|
+
// src/run-history.ts
|
|
9038
|
+
var RUN_HISTORY_FIXTURE_VERSION = 1;
|
|
9039
|
+
|
|
8811
9040
|
// src/run-facets.ts
|
|
8812
9041
|
function facetOrigin(origin) {
|
|
8813
9042
|
return origin?.trim() ? origin : null;
|
|
@@ -9361,13 +9590,14 @@ var CodecStateStore = class {
|
|
|
9361
9590
|
}
|
|
9362
9591
|
inner;
|
|
9363
9592
|
codec;
|
|
9593
|
+
extended;
|
|
9364
9594
|
/** Atomic claims touch only searchable metadata and bypass payload encoding. */
|
|
9365
9595
|
tryAdmitSingleton;
|
|
9366
9596
|
/**
|
|
9367
9597
|
* Forwarded verbatim — a facet cell counts `status`/`origin`, neither of which the codec touches
|
|
9368
|
-
* (it encodes payloads: input/output/error). Bound as a
|
|
9369
|
-
* actually aggregates, so `store.runFacets?.()` still
|
|
9370
|
-
* of resolving to a silently wrong empty result.
|
|
9598
|
+
* (it encodes payloads: input/output, plus events/error under `extended` coverage). Bound as a
|
|
9599
|
+
* PROPERTY, and only when the inner store actually aggregates, so `store.runFacets?.()` still
|
|
9600
|
+
* reports absence through the wrapper instead of resolving to a silently wrong empty result.
|
|
9371
9601
|
*/
|
|
9372
9602
|
runFacets;
|
|
9373
9603
|
/**
|
|
@@ -9419,9 +9649,10 @@ var CodecStateStore = class {
|
|
|
9419
9649
|
updateSchedule;
|
|
9420
9650
|
deleteSchedule;
|
|
9421
9651
|
listSchedules;
|
|
9422
|
-
constructor(inner, codec) {
|
|
9652
|
+
constructor(inner, codec, options = {}) {
|
|
9423
9653
|
this.inner = inner;
|
|
9424
9654
|
this.codec = codec;
|
|
9655
|
+
this.extended = options.coverage === "extended";
|
|
9425
9656
|
if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
|
|
9426
9657
|
const deleteRuns = inner.deleteRuns;
|
|
9427
9658
|
if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
|
|
@@ -9479,32 +9710,52 @@ var CodecStateStore = class {
|
|
|
9479
9710
|
return v === void 0 ? void 0 : this.codec.decode(v);
|
|
9480
9711
|
}
|
|
9481
9712
|
encRun(run) {
|
|
9482
|
-
|
|
9713
|
+
const next = {
|
|
9483
9714
|
...run,
|
|
9484
9715
|
input: this.enc(run.input),
|
|
9485
9716
|
output: this.enc(run.output)
|
|
9486
9717
|
};
|
|
9718
|
+
if (this.extended && run.error !== void 0) {
|
|
9719
|
+
next.error = this.enc(run.error);
|
|
9720
|
+
}
|
|
9721
|
+
return next;
|
|
9487
9722
|
}
|
|
9488
9723
|
decRun(run) {
|
|
9489
|
-
|
|
9724
|
+
if (!run) return run;
|
|
9725
|
+
const next = {
|
|
9490
9726
|
...run,
|
|
9491
9727
|
input: this.dec(run.input),
|
|
9492
9728
|
output: this.dec(run.output)
|
|
9493
9729
|
};
|
|
9730
|
+
if (this.extended && run.error !== void 0) {
|
|
9731
|
+
next.error = this.dec(run.error);
|
|
9732
|
+
}
|
|
9733
|
+
return next;
|
|
9494
9734
|
}
|
|
9495
9735
|
encCp(cp) {
|
|
9496
|
-
|
|
9736
|
+
const next = {
|
|
9497
9737
|
...cp,
|
|
9498
9738
|
input: this.enc(cp.input),
|
|
9499
9739
|
output: this.enc(cp.output)
|
|
9500
9740
|
};
|
|
9741
|
+
if (this.extended) {
|
|
9742
|
+
if (cp.events !== void 0) next.events = this.enc(cp.events);
|
|
9743
|
+
if (cp.error !== void 0) next.error = this.enc(cp.error);
|
|
9744
|
+
}
|
|
9745
|
+
return next;
|
|
9501
9746
|
}
|
|
9502
9747
|
decCp(cp) {
|
|
9503
|
-
|
|
9748
|
+
if (!cp) return cp;
|
|
9749
|
+
const next = {
|
|
9504
9750
|
...cp,
|
|
9505
9751
|
input: this.dec(cp.input),
|
|
9506
9752
|
output: this.dec(cp.output)
|
|
9507
9753
|
};
|
|
9754
|
+
if (this.extended) {
|
|
9755
|
+
if (cp.events !== void 0) next.events = this.dec(cp.events);
|
|
9756
|
+
if (cp.error !== void 0) next.error = this.dec(cp.error);
|
|
9757
|
+
}
|
|
9758
|
+
return next;
|
|
9508
9759
|
}
|
|
9509
9760
|
encSpec(spec) {
|
|
9510
9761
|
return "input" in spec ? {
|
|
@@ -9543,6 +9794,9 @@ var CodecStateStore = class {
|
|
|
9543
9794
|
};
|
|
9544
9795
|
if ("input" in patch) next.input = this.enc(patch.input);
|
|
9545
9796
|
if ("output" in patch) next.output = this.enc(patch.output);
|
|
9797
|
+
if (this.extended && patch.error !== void 0) {
|
|
9798
|
+
next.error = this.enc(patch.error);
|
|
9799
|
+
}
|
|
9546
9800
|
return next;
|
|
9547
9801
|
}
|
|
9548
9802
|
async getRun(runId) {
|
|
@@ -9578,8 +9832,8 @@ var CodecStateStore = class {
|
|
|
9578
9832
|
tryLockRun(runId, owner, leaseUntilMs, nowMs) {
|
|
9579
9833
|
return this.inner.tryLockRun(runId, owner, leaseUntilMs, nowMs);
|
|
9580
9834
|
}
|
|
9581
|
-
releaseRunLock(runId) {
|
|
9582
|
-
return this.inner.releaseRunLock(runId);
|
|
9835
|
+
releaseRunLock(runId, owner) {
|
|
9836
|
+
return owner === void 0 ? this.inner.releaseRunLock(runId) : this.inner.releaseRunLock(runId, owner);
|
|
9583
9837
|
}
|
|
9584
9838
|
renewRunLock(runId, owner, leaseUntilMs) {
|
|
9585
9839
|
return this.inner.renewRunLock(runId, owner, leaseUntilMs);
|
|
@@ -10087,9 +10341,9 @@ var InMemoryStateStore = class {
|
|
|
10087
10341
|
run.lockedUntil = leaseUntilMs;
|
|
10088
10342
|
return true;
|
|
10089
10343
|
}
|
|
10090
|
-
async releaseRunLock(runId) {
|
|
10344
|
+
async releaseRunLock(runId, owner) {
|
|
10091
10345
|
const run = this.runs.get(runId);
|
|
10092
|
-
if (run) {
|
|
10346
|
+
if (run && (owner === void 0 || run.lockedBy === owner)) {
|
|
10093
10347
|
run.lockedBy = void 0;
|
|
10094
10348
|
run.lockedUntil = void 0;
|
|
10095
10349
|
}
|
|
@@ -10439,6 +10693,7 @@ export {
|
|
|
10439
10693
|
ContinueAsNew,
|
|
10440
10694
|
DEFAULT_CONCURRENCY_STATUSES,
|
|
10441
10695
|
DEFAULT_PARTITION,
|
|
10696
|
+
DEFAULT_STALLED_AFTER_MS,
|
|
10442
10697
|
DURABLE_OPTIONS,
|
|
10443
10698
|
DURABLE_OPTIONS_CANONICAL,
|
|
10444
10699
|
DURABLE_STEP_CONFIG,
|
|
@@ -10462,6 +10717,7 @@ export {
|
|
|
10462
10717
|
NestedWorkflowCallError,
|
|
10463
10718
|
NonDeterminismError,
|
|
10464
10719
|
QueueController,
|
|
10720
|
+
RUN_HISTORY_FIXTURE_VERSION,
|
|
10465
10721
|
RUN_VALUE_FACET_LIMIT,
|
|
10466
10722
|
RUN_VALUE_FACET_SCAN,
|
|
10467
10723
|
RemoteStepError,
|
|
@@ -10535,6 +10791,7 @@ export {
|
|
|
10535
10791
|
facetOrigin,
|
|
10536
10792
|
fanPageBounds,
|
|
10537
10793
|
fanPageEtag,
|
|
10794
|
+
findStalledRuns,
|
|
10538
10795
|
foldedCount,
|
|
10539
10796
|
handoffHandlerId,
|
|
10540
10797
|
handoffName,
|