@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.cjs
CHANGED
|
@@ -30,6 +30,7 @@ __export(index_exports, {
|
|
|
30
30
|
ContinueAsNew: () => ContinueAsNew,
|
|
31
31
|
DEFAULT_CONCURRENCY_STATUSES: () => DEFAULT_CONCURRENCY_STATUSES,
|
|
32
32
|
DEFAULT_PARTITION: () => DEFAULT_PARTITION,
|
|
33
|
+
DEFAULT_STALLED_AFTER_MS: () => DEFAULT_STALLED_AFTER_MS,
|
|
33
34
|
DURABLE_OPTIONS: () => DURABLE_OPTIONS,
|
|
34
35
|
DURABLE_OPTIONS_CANONICAL: () => DURABLE_OPTIONS_CANONICAL,
|
|
35
36
|
DURABLE_STEP_CONFIG: () => DURABLE_STEP_CONFIG,
|
|
@@ -53,6 +54,7 @@ __export(index_exports, {
|
|
|
53
54
|
NestedWorkflowCallError: () => NestedWorkflowCallError,
|
|
54
55
|
NonDeterminismError: () => NonDeterminismError,
|
|
55
56
|
QueueController: () => QueueController,
|
|
57
|
+
RUN_HISTORY_FIXTURE_VERSION: () => RUN_HISTORY_FIXTURE_VERSION,
|
|
56
58
|
RUN_VALUE_FACET_LIMIT: () => RUN_VALUE_FACET_LIMIT,
|
|
57
59
|
RUN_VALUE_FACET_SCAN: () => RUN_VALUE_FACET_SCAN,
|
|
58
60
|
RemoteStepError: () => RemoteStepError,
|
|
@@ -126,6 +128,7 @@ __export(index_exports, {
|
|
|
126
128
|
facetOrigin: () => facetOrigin,
|
|
127
129
|
fanPageBounds: () => fanPageBounds,
|
|
128
130
|
fanPageEtag: () => fanPageEtag,
|
|
131
|
+
findStalledRuns: () => findStalledRuns,
|
|
129
132
|
foldedCount: () => foldedCount,
|
|
130
133
|
handoffHandlerId: () => handoffHandlerId,
|
|
131
134
|
handoffName: () => handoffName,
|
|
@@ -3017,6 +3020,66 @@ var SingletonGate = class {
|
|
|
3017
3020
|
}
|
|
3018
3021
|
};
|
|
3019
3022
|
|
|
3023
|
+
// src/stalled-runs.ts
|
|
3024
|
+
var DEFAULT_STALLED_AFTER_MS = 15 * 6e4;
|
|
3025
|
+
async function findStalledRuns(store, options = {}) {
|
|
3026
|
+
const now = options.now ?? Date.now();
|
|
3027
|
+
const thresholdMs = options.thresholdMs ?? DEFAULT_STALLED_AFTER_MS;
|
|
3028
|
+
const scan = Math.max(1, options.scan ?? 1e3);
|
|
3029
|
+
const pageSize = Math.min(200, scan);
|
|
3030
|
+
const stalled = [];
|
|
3031
|
+
for (let offset = 0; offset < scan; offset += pageSize) {
|
|
3032
|
+
const limit = Math.min(pageSize, scan - offset);
|
|
3033
|
+
const page = await store.listRuns({
|
|
3034
|
+
statuses: [
|
|
3035
|
+
"running",
|
|
3036
|
+
"suspended"
|
|
3037
|
+
],
|
|
3038
|
+
...options.namespace !== void 0 ? {
|
|
3039
|
+
namespace: options.namespace
|
|
3040
|
+
} : {},
|
|
3041
|
+
limit,
|
|
3042
|
+
offset
|
|
3043
|
+
});
|
|
3044
|
+
for (const run of page) {
|
|
3045
|
+
const ageMs = now - run.updatedAt.getTime();
|
|
3046
|
+
if (ageMs < thresholdMs) continue;
|
|
3047
|
+
const info2 = await stalledInfo(store, run, now, thresholdMs, ageMs);
|
|
3048
|
+
if (info2) stalled.push(info2);
|
|
3049
|
+
}
|
|
3050
|
+
if (page.length < limit) break;
|
|
3051
|
+
}
|
|
3052
|
+
return stalled;
|
|
3053
|
+
}
|
|
3054
|
+
__name(findStalledRuns, "findStalledRuns");
|
|
3055
|
+
async function stalledInfo(store, run, now, thresholdMs, ageMs) {
|
|
3056
|
+
let stalePending = null;
|
|
3057
|
+
for (const cp of await store.listCheckpoints(run.id)) {
|
|
3058
|
+
if (cp.kind !== "remote" || cp.status !== "pending") continue;
|
|
3059
|
+
const stepAgeMs = now - cp.enqueuedAt.getTime();
|
|
3060
|
+
if (stepAgeMs < thresholdMs) continue;
|
|
3061
|
+
if (cp.wakeAt != null && cp.wakeAt > now) continue;
|
|
3062
|
+
if (!stalePending || stepAgeMs > stalePending.ageMs) {
|
|
3063
|
+
stalePending = {
|
|
3064
|
+
seq: cp.seq,
|
|
3065
|
+
name: cp.name,
|
|
3066
|
+
attempts: cp.attempts,
|
|
3067
|
+
ageMs: stepAgeMs,
|
|
3068
|
+
leaseUntil: cp.wakeAt ?? null
|
|
3069
|
+
};
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
const wakeForever = run.status === "suspended" && run.wakeAt == null;
|
|
3073
|
+
if (!wakeForever && !stalePending) return null;
|
|
3074
|
+
return {
|
|
3075
|
+
run,
|
|
3076
|
+
ageMs,
|
|
3077
|
+
wakeForever,
|
|
3078
|
+
stalePending
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
__name(stalledInfo, "stalledInfo");
|
|
3082
|
+
|
|
3020
3083
|
// src/transport-pool.ts
|
|
3021
3084
|
var TransportPool = class {
|
|
3022
3085
|
static {
|
|
@@ -4374,6 +4437,18 @@ var WorkflowEngine = class {
|
|
|
4374
4437
|
stepQueue = /* @__PURE__ */ new Map();
|
|
4375
4438
|
/** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
|
|
4376
4439
|
inflight = /* @__PURE__ */ new Set();
|
|
4440
|
+
/**
|
|
4441
|
+
* The run lease of each execution in flight on THIS instance, by run id — renewed while it runs
|
|
4442
|
+
* and checked by every settle (the zombie fence, see {@link ExecutionLease}). Also what `drain()`
|
|
4443
|
+
* releases when its budget runs out.
|
|
4444
|
+
*/
|
|
4445
|
+
executionLeases = /* @__PURE__ */ new Map();
|
|
4446
|
+
/** Pager hooks for stranded runs (see {@link onStalled}). */
|
|
4447
|
+
stalledListeners = /* @__PURE__ */ new Set();
|
|
4448
|
+
/** Runs already paged about, by id → the `updatedAt` of the paged episode (see {@link sweepStalled}). */
|
|
4449
|
+
notifiedStalled = /* @__PURE__ */ new Map();
|
|
4450
|
+
lastStalledSweepAt = Number.NEGATIVE_INFINITY;
|
|
4451
|
+
stalledAfterMs;
|
|
4377
4452
|
draining = false;
|
|
4378
4453
|
/** Wait already spent on this shutdown before `drain()` (see `beginDrain`). */
|
|
4379
4454
|
drainSpentMs = 0;
|
|
@@ -4398,6 +4473,8 @@ var WorkflowEngine = class {
|
|
|
4398
4473
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
4399
4474
|
this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
|
|
4400
4475
|
this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
|
|
4476
|
+
const stalledAfter = deps.stalledAfterMs;
|
|
4477
|
+
this.stalledAfterMs = stalledAfter !== void 0 && Number.isFinite(stalledAfter) ? Math.max(6e4, stalledAfter) : DEFAULT_STALLED_AFTER_MS;
|
|
4401
4478
|
const batch = deps.timerBatchSize;
|
|
4402
4479
|
this.timerBatchSize = batch !== void 0 && Number.isFinite(batch) && batch >= 1 ? Math.floor(batch) : 500;
|
|
4403
4480
|
this.eventBufferTtlMs = deps.eventBufferTtlMs || void 0;
|
|
@@ -4741,6 +4818,64 @@ var WorkflowEngine = class {
|
|
|
4741
4818
|
this.deadListeners.add(listener);
|
|
4742
4819
|
return () => this.deadListeners.delete(listener);
|
|
4743
4820
|
}
|
|
4821
|
+
/**
|
|
4822
|
+
* Register a pager hook for STRANDED runs: an in-flight run untouched past `stalledAfterMs` that is
|
|
4823
|
+
* either suspended with NO wake timer (wake-forever: nothing will ever re-drive it) or parked on an
|
|
4824
|
+
* old pending remote step with no live lease (dispatched and, most likely, lost). The timer poller
|
|
4825
|
+
* sweeps for these ({@link sweepStalled}, throttled to one pass a minute) and notifies each episode
|
|
4826
|
+
* ONCE per instance: a run is paged again only after it makes progress and strands again. Wire your
|
|
4827
|
+
* alerting here like {@link onDead}. A listener that throws never breaks the sweep. Returns an
|
|
4828
|
+
* unsubscribe function.
|
|
4829
|
+
*/
|
|
4830
|
+
onStalled(listener) {
|
|
4831
|
+
this.stalledListeners.add(listener);
|
|
4832
|
+
return () => this.stalledListeners.delete(listener);
|
|
4833
|
+
}
|
|
4834
|
+
/**
|
|
4835
|
+
* Detect stranded in-flight runs and notify the {@link onStalled} listeners. Self-throttled to one
|
|
4836
|
+
* pass a minute, skipped while draining, and a no-op without listeners — call it every tick (the
|
|
4837
|
+
* NestJS timer poller does). Scoped to this engine's `namespace` (an operator engine with no
|
|
4838
|
+
* namespace sweeps every namespace). Returns the runs notified THIS pass.
|
|
4839
|
+
*/
|
|
4840
|
+
async sweepStalled(now = this.clock()) {
|
|
4841
|
+
if (this.stalledListeners.size === 0 || this.draining) return [];
|
|
4842
|
+
if (now - this.lastStalledSweepAt < 6e4) return [];
|
|
4843
|
+
this.lastStalledSweepAt = now;
|
|
4844
|
+
const notified = [];
|
|
4845
|
+
for (const info2 of await this.listStalledRuns({
|
|
4846
|
+
now
|
|
4847
|
+
})) {
|
|
4848
|
+
const episode = info2.run.updatedAt.getTime();
|
|
4849
|
+
if (this.notifiedStalled.get(info2.run.id) === episode) continue;
|
|
4850
|
+
this.notifiedStalled.set(info2.run.id, episode);
|
|
4851
|
+
if (this.notifiedStalled.size > 1e3) this.notifiedStalled.clear();
|
|
4852
|
+
for (const listener of this.stalledListeners) {
|
|
4853
|
+
try {
|
|
4854
|
+
listener(info2);
|
|
4855
|
+
} catch {
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
notified.push(info2);
|
|
4859
|
+
}
|
|
4860
|
+
return notified;
|
|
4861
|
+
}
|
|
4862
|
+
/**
|
|
4863
|
+
* The stranded runs right now, without notifying anyone: the query behind {@link sweepStalled} and
|
|
4864
|
+
* the CLI's `durable inspect --stale`. A `running`/`suspended` run qualifies when it has been
|
|
4865
|
+
* untouched for `thresholdMs` (default `stalledAfterMs`) AND either it is suspended with no wake
|
|
4866
|
+
* timer, or its oldest pending remote step is older than the threshold with no lease in the future
|
|
4867
|
+
* (a step with a `timeoutMs`, a heartbeat or `remoteRedispatchMs` keeps a lease; a long
|
|
4868
|
+
* `ctx.sleep` has a future `wakeAt`; neither is stranded). Scans up to `scan` candidates (default
|
|
4869
|
+
* 1000), newest first, in this engine's namespace.
|
|
4870
|
+
*/
|
|
4871
|
+
async listStalledRuns(options = {}) {
|
|
4872
|
+
return findStalledRuns(this.store, {
|
|
4873
|
+
now: options.now ?? this.clock(),
|
|
4874
|
+
thresholdMs: options.thresholdMs ?? this.stalledAfterMs,
|
|
4875
|
+
scan: options.scan,
|
|
4876
|
+
namespace: this.namespace
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4744
4879
|
notifyDead(run) {
|
|
4745
4880
|
for (const fn of this.deadListeners) {
|
|
4746
4881
|
try {
|
|
@@ -4899,6 +5034,11 @@ var WorkflowEngine = class {
|
|
|
4899
5034
|
getRun(runId) {
|
|
4900
5035
|
return this.store.getRun(runId);
|
|
4901
5036
|
}
|
|
5037
|
+
/** A run's full checkpoint timeline, ordered by seq. A thin pass-through to the store (it is what
|
|
5038
|
+
* the testing kit's `captureHistory` reads, next to {@link getRun}). */
|
|
5039
|
+
listCheckpoints(runId) {
|
|
5040
|
+
return this.store.listCheckpoints(runId);
|
|
5041
|
+
}
|
|
4902
5042
|
/**
|
|
4903
5043
|
* Re-execute a run's body (replaying its checkpoints). This is the EXPLICIT entry point: besides
|
|
4904
5044
|
* driving a live run, it re-drives a `failed` run — the manual retry (prefer {@link requeue}, which
|
|
@@ -4984,6 +5124,8 @@ var WorkflowEngine = class {
|
|
|
4984
5124
|
* diagnosis.
|
|
4985
5125
|
*/
|
|
4986
5126
|
async releaseLeaseBestEffort(runId) {
|
|
5127
|
+
if (this.isFenced(runId)) return;
|
|
5128
|
+
this.handOffLease(runId);
|
|
4987
5129
|
try {
|
|
4988
5130
|
await this.store.releaseRunLock(runId);
|
|
4989
5131
|
} catch (err) {
|
|
@@ -5233,17 +5375,47 @@ var WorkflowEngine = class {
|
|
|
5233
5375
|
rearm();
|
|
5234
5376
|
});
|
|
5235
5377
|
this.drainRearms.add(rearm);
|
|
5378
|
+
let settled = false;
|
|
5236
5379
|
try {
|
|
5237
5380
|
await Promise.race([
|
|
5238
5381
|
Promise.allSettled([
|
|
5239
5382
|
...this.inflight
|
|
5240
|
-
])
|
|
5383
|
+
]).then(() => {
|
|
5384
|
+
settled = true;
|
|
5385
|
+
}),
|
|
5241
5386
|
timer
|
|
5242
5387
|
]);
|
|
5243
5388
|
} finally {
|
|
5244
5389
|
this.drainRearms.delete(rearm);
|
|
5245
5390
|
clearTimeout(timeout);
|
|
5246
5391
|
}
|
|
5392
|
+
if (!settled) await this.releaseInflightLeases();
|
|
5393
|
+
}
|
|
5394
|
+
/**
|
|
5395
|
+
* The drain budget ran out with executions still in flight: release their recovery leases so
|
|
5396
|
+
* another instance re-drives those runs on its next recovery pass, instead of waiting out
|
|
5397
|
+
* `leaseMs` after this process exits. Each still-running turn is FENCED first, so whatever it
|
|
5398
|
+
* computes after this point is never written over the new owner's work (see settleRun). Its
|
|
5399
|
+
* completed steps are checkpointed, so the new owner replays them; a step still executing here
|
|
5400
|
+
* may run again there, exactly as after a crash. The release is owner-scoped where the store
|
|
5401
|
+
* supports it (only a lease this instance still holds is cleared). Best effort: never throws.
|
|
5402
|
+
*/
|
|
5403
|
+
async releaseInflightLeases() {
|
|
5404
|
+
const released = [];
|
|
5405
|
+
for (const [runId, lease] of [
|
|
5406
|
+
...this.executionLeases
|
|
5407
|
+
]) {
|
|
5408
|
+
if (lease.lost || lease.handedOff || lease.done) continue;
|
|
5409
|
+
lease.lost = true;
|
|
5410
|
+
try {
|
|
5411
|
+
await this.store.releaseRunLock(runId, this.instanceId);
|
|
5412
|
+
released.push(runId);
|
|
5413
|
+
} catch {
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
if (released.length > 0) {
|
|
5417
|
+
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(", ")}`);
|
|
5418
|
+
}
|
|
5247
5419
|
}
|
|
5248
5420
|
/**
|
|
5249
5421
|
* Cancel in-flight runs that have outlived their workflow's `executionTimeout`. Call it from the
|
|
@@ -6733,7 +6905,10 @@ var WorkflowEngine = class {
|
|
|
6733
6905
|
}
|
|
6734
6906
|
async singletonWaitResult(run) {
|
|
6735
6907
|
const current = await this.store.getRun(run.id);
|
|
6736
|
-
|
|
6908
|
+
if (!this.isFenced(run.id)) {
|
|
6909
|
+
this.handOffLease(run.id);
|
|
6910
|
+
await this.store.releaseRunLock(run.id);
|
|
6911
|
+
}
|
|
6737
6912
|
if (!current) throw new Error(`run ${run.id} was deleted during singleton admission`);
|
|
6738
6913
|
if (current.status === "suspended") {
|
|
6739
6914
|
this.emit({
|
|
@@ -6761,10 +6936,7 @@ var WorkflowEngine = class {
|
|
|
6761
6936
|
};
|
|
6762
6937
|
}
|
|
6763
6938
|
}
|
|
6764
|
-
const
|
|
6765
|
-
void this.store.renewRunLock(run.id, this.instanceId, this.clock() + this.leaseMs).catch(() => void 0);
|
|
6766
|
-
}, Math.max(50, Math.floor(this.leaseMs / 2)));
|
|
6767
|
-
renew.unref?.();
|
|
6939
|
+
const held = this.holdExecutionLease(run.id);
|
|
6768
6940
|
try {
|
|
6769
6941
|
const result = registered?.remote ? await this.runRemoteExecution(run, registered) : await this.runExecution(run, fn, registered);
|
|
6770
6942
|
if (registered?.singleton && (result.status === "completed" || result.status === "failed" || result.status === "cancelled" || result.status === "dead")) {
|
|
@@ -6772,10 +6944,59 @@ var WorkflowEngine = class {
|
|
|
6772
6944
|
}
|
|
6773
6945
|
return result;
|
|
6774
6946
|
} finally {
|
|
6775
|
-
|
|
6947
|
+
held.stop();
|
|
6776
6948
|
}
|
|
6777
6949
|
}
|
|
6778
6950
|
/**
|
|
6951
|
+
* Renew `runId`'s lease for as long as this execution holds it, and register it in
|
|
6952
|
+
* {@link executionLeases} so settles can check the zombie fence and `drain()` can release it.
|
|
6953
|
+
*
|
|
6954
|
+
* A renewal that returns `false` means the lease is gone: this turn stalled past `leaseMs` and
|
|
6955
|
+
* another instance reclaimed the run, so this executor is now a zombie. It is FENCED (`lost`): its
|
|
6956
|
+
* settles degrade to an echo of the store's state instead of clobbering the new owner's writes, and
|
|
6957
|
+
* its own lease release is skipped (it would free the new owner's lease). A renewal that THROWS (a
|
|
6958
|
+
* transient store error) is not evidence of a takeover, so it stays swallowed and the next
|
|
6959
|
+
* interval retries. A lease this execution released on purpose (`handedOff`) is not renewed, and a
|
|
6960
|
+
* renewal resolving after the execution ended (`done`) fences nothing.
|
|
6961
|
+
*/
|
|
6962
|
+
holdExecutionLease(runId) {
|
|
6963
|
+
const lease = {
|
|
6964
|
+
lost: false,
|
|
6965
|
+
handedOff: false,
|
|
6966
|
+
done: false
|
|
6967
|
+
};
|
|
6968
|
+
this.executionLeases.set(runId, lease);
|
|
6969
|
+
const renew = setInterval(() => {
|
|
6970
|
+
if (lease.handedOff || lease.lost || lease.done) return;
|
|
6971
|
+
void this.store.renewRunLock(runId, this.instanceId, this.clock() + this.leaseMs).then((renewed) => {
|
|
6972
|
+
if (renewed || lease.handedOff || lease.lost || lease.done) return;
|
|
6973
|
+
lease.lost = true;
|
|
6974
|
+
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`);
|
|
6975
|
+
}).catch(() => void 0);
|
|
6976
|
+
}, Math.max(50, Math.floor(this.leaseMs / 2)));
|
|
6977
|
+
renew.unref?.();
|
|
6978
|
+
return {
|
|
6979
|
+
lease,
|
|
6980
|
+
stop: /* @__PURE__ */ __name(() => {
|
|
6981
|
+
clearInterval(renew);
|
|
6982
|
+
lease.done = true;
|
|
6983
|
+
if (this.executionLeases.get(runId) === lease) this.executionLeases.delete(runId);
|
|
6984
|
+
}, "stop")
|
|
6985
|
+
};
|
|
6986
|
+
}
|
|
6987
|
+
/** Whether `runId`'s execution on this instance lost its lease (see {@link holdExecutionLease}). */
|
|
6988
|
+
isFenced(runId) {
|
|
6989
|
+
return this.executionLeases.get(runId)?.lost === true;
|
|
6990
|
+
}
|
|
6991
|
+
/**
|
|
6992
|
+
* Mark `runId`'s lease as given away on purpose by its own execution (released mid-turn), so a
|
|
6993
|
+
* renewal that now fails is not read as a takeover.
|
|
6994
|
+
*/
|
|
6995
|
+
handOffLease(runId) {
|
|
6996
|
+
const lease = this.executionLeases.get(runId);
|
|
6997
|
+
if (lease) lease.handedOff = true;
|
|
6998
|
+
}
|
|
6999
|
+
/**
|
|
6779
7000
|
* Advance a remote (cross-SDK) workflow one turn: hand its history to the executor (which dispatches
|
|
6780
7001
|
* a workflow task to the worker and awaits its replay) and apply the decision. Mirrors
|
|
6781
7002
|
* {@link runExecution}'s settle/suspend; the lease is held by {@link execute}. The result that lands
|
|
@@ -6992,6 +7213,7 @@ ${run.workflowVersion}`;
|
|
|
6992
7213
|
* keeps its own lease handling (the TS executor's `finally`, the remote path's caller).
|
|
6993
7214
|
*/
|
|
6994
7215
|
async settleRun(run, outcome) {
|
|
7216
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
6995
7217
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
6996
7218
|
if (outcome.kind === "completed") {
|
|
6997
7219
|
const applied2 = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SETTLE_ELIGIBLE_STATUSES, {
|
|
@@ -7072,6 +7294,7 @@ ${run.workflowVersion}`;
|
|
|
7072
7294
|
}
|
|
7073
7295
|
async runRemoteExecution(run, registered) {
|
|
7074
7296
|
if (run.status === "cancelling") {
|
|
7297
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
7075
7298
|
const error2 = {
|
|
7076
7299
|
message: "cancelled"
|
|
7077
7300
|
};
|
|
@@ -7080,6 +7303,7 @@ ${run.workflowVersion}`;
|
|
|
7080
7303
|
error: error2,
|
|
7081
7304
|
updatedAt: /* @__PURE__ */ new Date()
|
|
7082
7305
|
});
|
|
7306
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
7083
7307
|
this.emit({
|
|
7084
7308
|
type: "run.failed",
|
|
7085
7309
|
runId: run.id,
|
|
@@ -7126,6 +7350,7 @@ ${run.workflowVersion}`;
|
|
|
7126
7350
|
}
|
|
7127
7351
|
const history = await this.remoteHistory(run.id, checkpoints);
|
|
7128
7352
|
if (remote.executor.dispatch) {
|
|
7353
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
7129
7354
|
const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
|
|
7130
7355
|
const silenceMs = this.remoteAdvanceSilenceMs;
|
|
7131
7356
|
const wakeAt = silenceMs != null ? this.clock() + silenceMs : void 0;
|
|
@@ -7141,6 +7366,7 @@ ${run.workflowVersion}`;
|
|
|
7141
7366
|
workflow: run.workflow,
|
|
7142
7367
|
namespace: run.namespace
|
|
7143
7368
|
});
|
|
7369
|
+
this.handOffLease(run.id);
|
|
7144
7370
|
await this.store.releaseRunLock(run.id);
|
|
7145
7371
|
await remote.executor.dispatch(run, history, taskId);
|
|
7146
7372
|
return {
|
|
@@ -7169,6 +7395,7 @@ ${run.workflowVersion}`;
|
|
|
7169
7395
|
message: err instanceof Error ? err.message : String(err),
|
|
7170
7396
|
stack: err instanceof Error ? err.stack : void 0
|
|
7171
7397
|
};
|
|
7398
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
7172
7399
|
await this.store.updateRun(run.id, {
|
|
7173
7400
|
status: "failed",
|
|
7174
7401
|
error: error2,
|
|
@@ -7197,6 +7424,7 @@ ${run.workflowVersion}`;
|
|
|
7197
7424
|
* — the caller has already ensured this is the decision for the currently-awaited turn.
|
|
7198
7425
|
*/
|
|
7199
7426
|
async applyDecision(run, decision) {
|
|
7427
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
7200
7428
|
if (decision.status === "completed") {
|
|
7201
7429
|
await this.applyCommands(run, decision.commands);
|
|
7202
7430
|
return this.settleRun(run, {
|
|
@@ -7666,6 +7894,7 @@ ${run.workflowVersion}`;
|
|
|
7666
7894
|
* cancel flag stays too, re-derived from the status on any later re-drive regardless.
|
|
7667
7895
|
*/
|
|
7668
7896
|
async compensateAndCancel(run, compensations, replay) {
|
|
7897
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
7669
7898
|
const pending = await this.unwindCompensations(run, compensations, replay);
|
|
7670
7899
|
if (pending) return {
|
|
7671
7900
|
runId: run.id,
|
|
@@ -7757,6 +7986,7 @@ ${run.workflowVersion}`;
|
|
|
7757
7986
|
});
|
|
7758
7987
|
} catch (err) {
|
|
7759
7988
|
if (err instanceof ContinueAsNew) {
|
|
7989
|
+
if (this.isFenced(run.id)) return await this.echoCurrentStatus(run.id);
|
|
7760
7990
|
if (this.cancelRequested.has(run.id)) {
|
|
7761
7991
|
return await this.compensateAndCancel(run, compensations, replay);
|
|
7762
7992
|
}
|
|
@@ -8208,6 +8438,7 @@ ${run.workflowVersion}`;
|
|
|
8208
8438
|
* own `finally` also releases (the run-execution path) is safe.
|
|
8209
8439
|
*/
|
|
8210
8440
|
async parkBlocked(run, blocked) {
|
|
8441
|
+
if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
|
|
8211
8442
|
const error2 = {
|
|
8212
8443
|
message: blocked.reason,
|
|
8213
8444
|
code: blocked.code,
|
|
@@ -8794,10 +9025,7 @@ ${run.workflowVersion}`;
|
|
|
8794
9025
|
if (!await this.store.tryLockRun(decision.runId, this.instanceId, nowMs + this.leaseMs, nowMs)) {
|
|
8795
9026
|
return;
|
|
8796
9027
|
}
|
|
8797
|
-
const
|
|
8798
|
-
void this.store.renewRunLock(decision.runId, this.instanceId, this.clock() + this.leaseMs).catch(() => void 0);
|
|
8799
|
-
}, Math.max(50, Math.floor(this.leaseMs / 2)));
|
|
8800
|
-
renew.unref?.();
|
|
9028
|
+
const held = this.holdExecutionLease(decision.runId);
|
|
8801
9029
|
try {
|
|
8802
9030
|
const fresh = await this.store.getRun(decision.runId);
|
|
8803
9031
|
if (!fresh || fresh.awaitingDecisionTaskId !== decision.taskId) return;
|
|
@@ -8812,8 +9040,9 @@ ${run.workflowVersion}`;
|
|
|
8812
9040
|
fresh.awaitingDecisionTaskId = void 0;
|
|
8813
9041
|
await this.applyDecision(fresh, decision);
|
|
8814
9042
|
} finally {
|
|
8815
|
-
|
|
8816
|
-
|
|
9043
|
+
const fenced = held.lease.lost;
|
|
9044
|
+
held.stop();
|
|
9045
|
+
if (!fenced) await this.store.releaseRunLock(decision.runId);
|
|
8817
9046
|
}
|
|
8818
9047
|
}
|
|
8819
9048
|
/** Earliest clock time a suspended turn's deadline is worth LOOKING at again (`runId` → clock ms) —
|
|
@@ -9009,6 +9238,9 @@ function childIdsOf(checkpoints) {
|
|
|
9009
9238
|
}
|
|
9010
9239
|
__name(childIdsOf, "childIdsOf");
|
|
9011
9240
|
|
|
9241
|
+
// src/run-history.ts
|
|
9242
|
+
var RUN_HISTORY_FIXTURE_VERSION = 1;
|
|
9243
|
+
|
|
9012
9244
|
// src/run-facets.ts
|
|
9013
9245
|
function facetOrigin(origin) {
|
|
9014
9246
|
return origin?.trim() ? origin : null;
|
|
@@ -9562,13 +9794,14 @@ var CodecStateStore = class {
|
|
|
9562
9794
|
}
|
|
9563
9795
|
inner;
|
|
9564
9796
|
codec;
|
|
9797
|
+
extended;
|
|
9565
9798
|
/** Atomic claims touch only searchable metadata and bypass payload encoding. */
|
|
9566
9799
|
tryAdmitSingleton;
|
|
9567
9800
|
/**
|
|
9568
9801
|
* Forwarded verbatim — a facet cell counts `status`/`origin`, neither of which the codec touches
|
|
9569
|
-
* (it encodes payloads: input/output/error). Bound as a
|
|
9570
|
-
* actually aggregates, so `store.runFacets?.()` still
|
|
9571
|
-
* of resolving to a silently wrong empty result.
|
|
9802
|
+
* (it encodes payloads: input/output, plus events/error under `extended` coverage). Bound as a
|
|
9803
|
+
* PROPERTY, and only when the inner store actually aggregates, so `store.runFacets?.()` still
|
|
9804
|
+
* reports absence through the wrapper instead of resolving to a silently wrong empty result.
|
|
9572
9805
|
*/
|
|
9573
9806
|
runFacets;
|
|
9574
9807
|
/**
|
|
@@ -9620,9 +9853,10 @@ var CodecStateStore = class {
|
|
|
9620
9853
|
updateSchedule;
|
|
9621
9854
|
deleteSchedule;
|
|
9622
9855
|
listSchedules;
|
|
9623
|
-
constructor(inner, codec) {
|
|
9856
|
+
constructor(inner, codec, options = {}) {
|
|
9624
9857
|
this.inner = inner;
|
|
9625
9858
|
this.codec = codec;
|
|
9859
|
+
this.extended = options.coverage === "extended";
|
|
9626
9860
|
if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
|
|
9627
9861
|
const deleteRuns = inner.deleteRuns;
|
|
9628
9862
|
if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
|
|
@@ -9680,32 +9914,52 @@ var CodecStateStore = class {
|
|
|
9680
9914
|
return v === void 0 ? void 0 : this.codec.decode(v);
|
|
9681
9915
|
}
|
|
9682
9916
|
encRun(run) {
|
|
9683
|
-
|
|
9917
|
+
const next = {
|
|
9684
9918
|
...run,
|
|
9685
9919
|
input: this.enc(run.input),
|
|
9686
9920
|
output: this.enc(run.output)
|
|
9687
9921
|
};
|
|
9922
|
+
if (this.extended && run.error !== void 0) {
|
|
9923
|
+
next.error = this.enc(run.error);
|
|
9924
|
+
}
|
|
9925
|
+
return next;
|
|
9688
9926
|
}
|
|
9689
9927
|
decRun(run) {
|
|
9690
|
-
|
|
9928
|
+
if (!run) return run;
|
|
9929
|
+
const next = {
|
|
9691
9930
|
...run,
|
|
9692
9931
|
input: this.dec(run.input),
|
|
9693
9932
|
output: this.dec(run.output)
|
|
9694
9933
|
};
|
|
9934
|
+
if (this.extended && run.error !== void 0) {
|
|
9935
|
+
next.error = this.dec(run.error);
|
|
9936
|
+
}
|
|
9937
|
+
return next;
|
|
9695
9938
|
}
|
|
9696
9939
|
encCp(cp) {
|
|
9697
|
-
|
|
9940
|
+
const next = {
|
|
9698
9941
|
...cp,
|
|
9699
9942
|
input: this.enc(cp.input),
|
|
9700
9943
|
output: this.enc(cp.output)
|
|
9701
9944
|
};
|
|
9945
|
+
if (this.extended) {
|
|
9946
|
+
if (cp.events !== void 0) next.events = this.enc(cp.events);
|
|
9947
|
+
if (cp.error !== void 0) next.error = this.enc(cp.error);
|
|
9948
|
+
}
|
|
9949
|
+
return next;
|
|
9702
9950
|
}
|
|
9703
9951
|
decCp(cp) {
|
|
9704
|
-
|
|
9952
|
+
if (!cp) return cp;
|
|
9953
|
+
const next = {
|
|
9705
9954
|
...cp,
|
|
9706
9955
|
input: this.dec(cp.input),
|
|
9707
9956
|
output: this.dec(cp.output)
|
|
9708
9957
|
};
|
|
9958
|
+
if (this.extended) {
|
|
9959
|
+
if (cp.events !== void 0) next.events = this.dec(cp.events);
|
|
9960
|
+
if (cp.error !== void 0) next.error = this.dec(cp.error);
|
|
9961
|
+
}
|
|
9962
|
+
return next;
|
|
9709
9963
|
}
|
|
9710
9964
|
encSpec(spec) {
|
|
9711
9965
|
return "input" in spec ? {
|
|
@@ -9744,6 +9998,9 @@ var CodecStateStore = class {
|
|
|
9744
9998
|
};
|
|
9745
9999
|
if ("input" in patch) next.input = this.enc(patch.input);
|
|
9746
10000
|
if ("output" in patch) next.output = this.enc(patch.output);
|
|
10001
|
+
if (this.extended && patch.error !== void 0) {
|
|
10002
|
+
next.error = this.enc(patch.error);
|
|
10003
|
+
}
|
|
9747
10004
|
return next;
|
|
9748
10005
|
}
|
|
9749
10006
|
async getRun(runId) {
|
|
@@ -9779,8 +10036,8 @@ var CodecStateStore = class {
|
|
|
9779
10036
|
tryLockRun(runId, owner, leaseUntilMs, nowMs) {
|
|
9780
10037
|
return this.inner.tryLockRun(runId, owner, leaseUntilMs, nowMs);
|
|
9781
10038
|
}
|
|
9782
|
-
releaseRunLock(runId) {
|
|
9783
|
-
return this.inner.releaseRunLock(runId);
|
|
10039
|
+
releaseRunLock(runId, owner) {
|
|
10040
|
+
return owner === void 0 ? this.inner.releaseRunLock(runId) : this.inner.releaseRunLock(runId, owner);
|
|
9784
10041
|
}
|
|
9785
10042
|
renewRunLock(runId, owner, leaseUntilMs) {
|
|
9786
10043
|
return this.inner.renewRunLock(runId, owner, leaseUntilMs);
|
|
@@ -10288,9 +10545,9 @@ var InMemoryStateStore = class {
|
|
|
10288
10545
|
run.lockedUntil = leaseUntilMs;
|
|
10289
10546
|
return true;
|
|
10290
10547
|
}
|
|
10291
|
-
async releaseRunLock(runId) {
|
|
10548
|
+
async releaseRunLock(runId, owner) {
|
|
10292
10549
|
const run = this.runs.get(runId);
|
|
10293
|
-
if (run) {
|
|
10550
|
+
if (run && (owner === void 0 || run.lockedBy === owner)) {
|
|
10294
10551
|
run.lockedBy = void 0;
|
|
10295
10552
|
run.lockedUntil = void 0;
|
|
10296
10553
|
}
|
|
@@ -10641,6 +10898,7 @@ var InMemoryTransport = class {
|
|
|
10641
10898
|
ContinueAsNew,
|
|
10642
10899
|
DEFAULT_CONCURRENCY_STATUSES,
|
|
10643
10900
|
DEFAULT_PARTITION,
|
|
10901
|
+
DEFAULT_STALLED_AFTER_MS,
|
|
10644
10902
|
DURABLE_OPTIONS,
|
|
10645
10903
|
DURABLE_OPTIONS_CANONICAL,
|
|
10646
10904
|
DURABLE_STEP_CONFIG,
|
|
@@ -10664,6 +10922,7 @@ var InMemoryTransport = class {
|
|
|
10664
10922
|
NestedWorkflowCallError,
|
|
10665
10923
|
NonDeterminismError,
|
|
10666
10924
|
QueueController,
|
|
10925
|
+
RUN_HISTORY_FIXTURE_VERSION,
|
|
10667
10926
|
RUN_VALUE_FACET_LIMIT,
|
|
10668
10927
|
RUN_VALUE_FACET_SCAN,
|
|
10669
10928
|
RemoteStepError,
|
|
@@ -10737,6 +10996,7 @@ var InMemoryTransport = class {
|
|
|
10737
10996
|
facetOrigin,
|
|
10738
10997
|
fanPageBounds,
|
|
10739
10998
|
fanPageEtag,
|
|
10999
|
+
findStalledRuns,
|
|
10740
11000
|
foldedCount,
|
|
10741
11001
|
handoffHandlerId,
|
|
10742
11002
|
handoffName,
|