From 787d785439cb3162cc6b5c77cab2060097c5dd96 Mon Sep 17 00:00:00 2001 From: Lourince Daging Date: Wed, 22 Jul 2026 16:12:19 +0200 Subject: [PATCH] fix(miner): fail closed on an unparseable lease in portfolio-queue findStuckItems findStuckItems skipped any in_progress item whose leasedAt couldn't be parsed (leaseAgeMs returns null -> the old 'if (ageMs === null) continue'), so a corrupted or hand-edited lease timestamp stranded that item 'in_progress' forever, never reclaimed by the stuck-item sweep. claim-ledger-expiry.ts's findExpiredClaims already fails closed on the same shape post-#7732 (an unparseable claimedAt is treated as expired). Match that posture: an item whose leasedAt is missing/unparseable is now returned as stuck (swept back to queued) rather than silently skipped. Updated the doc comment and split the existing test so unparseable/missing leases assert the fail-closed sweep instead of being bundled into the 'ignored' case. Closes #8007 --- packages/loopover-miner/lib/portfolio-queue-expiry.ts | 10 ++++++---- test/unit/miner-portfolio-queue-expiry.test.ts | 10 ++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/loopover-miner/lib/portfolio-queue-expiry.ts b/packages/loopover-miner/lib/portfolio-queue-expiry.ts index 1b23da008e..656a0ce609 100644 --- a/packages/loopover-miner/lib/portfolio-queue-expiry.ts +++ b/packages/loopover-miner/lib/portfolio-queue-expiry.ts @@ -21,8 +21,9 @@ function leaseAgeMs(item: QueueLeaseEntry, nowMs: number): number | null { /** * Return in-flight items whose lease age is strictly greater than `maxLeaseMs`. An item whose age equals - * `maxLeaseMs` exactly is still within the window (not stuck). Items that are not 'in_progress', or whose - * `leasedAt` is missing/unparseable, are never returned. + * `maxLeaseMs` exactly is still within the window (not stuck). Items that are not 'in_progress' are never + * returned; an item whose `leasedAt` is missing/unparseable fails closed and IS returned (swept), so a + * corrupted lease can't strand an item 'in_progress' forever (#8007, matching claim-ledger-expiry post-#7732). */ export function findStuckItems(items: QueueLeaseEntry[], nowMs: number, maxLeaseMs: number): QueueLeaseEntry[] { if (!Number.isFinite(nowMs) || nowMs < 0) throw new Error("invalid_now_ms"); @@ -33,8 +34,9 @@ export function findStuckItems(items: QueueLeaseEntry[], nowMs: number, maxLease for (const item of items) { if (item?.status !== "in_progress") continue; const ageMs = leaseAgeMs(item, nowMs); - if (ageMs === null) continue; - if (ageMs > maxLeaseMs) stuck.push(item); + // Fail closed on an unparseable leasedAt (#8007): a corrupted/hand-edited row whose age can't be computed + // must still be reclaimable, not left 'in_progress' forever -- mirroring findExpiredClaims post-#7732. + if (ageMs === null || ageMs > maxLeaseMs) stuck.push(item); } return stuck; } diff --git a/test/unit/miner-portfolio-queue-expiry.test.ts b/test/unit/miner-portfolio-queue-expiry.test.ts index 2a5a3ca87c..66ed5f3968 100644 --- a/test/unit/miner-portfolio-queue-expiry.test.ts +++ b/test/unit/miner-portfolio-queue-expiry.test.ts @@ -112,12 +112,18 @@ describe("findStuckItems (#4827)", () => { expect(findStuckItems([atBound], now, max)).toEqual([]); }); - it("ignores fresh, non-in_progress, and unparseable-lease items", () => { + it("ignores fresh and non-in_progress items", () => { const fresh = leaseItem({ identifier: "fresh", leasedAt: new Date(now - 1).toISOString() }); const queued = leaseItem({ identifier: "queued", status: "queued", leasedAt: new Date(now - max - 5).toISOString() }); + expect(findStuckItems([fresh, queued], now, max)).toEqual([]); + }); + + it("fails closed: sweeps an in_progress item whose leasedAt is missing or unparseable (#8007)", () => { + // A corrupted/hand-edited lease whose age can't be computed must be reclaimable, not stranded + // 'in_progress' forever -- matching claim-ledger-expiry's post-#7732 posture. const noLease = leaseItem({ identifier: "nolease", leasedAt: null }); const bogus = leaseItem({ identifier: "bogus", leasedAt: "not-a-date" }); - expect(findStuckItems([fresh, queued, noLease, bogus], now, max)).toEqual([]); + expect(findStuckItems([noLease, bogus], now, max)).toEqual([noLease, bogus]); }); it("validates its arguments", () => {