From a3beafba50445f3c591d080438474b287a3b52be Mon Sep 17 00:00:00 2001 From: Aditya Vikram Singh <247195684+avs-io@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:59:54 +0530 Subject: [PATCH] fix(cache): accept completed-by-other as the stale-lock loser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The process suite required the loser of a stale-lock contest to be timed-out. On a slow runner the winner's unlink-guard / create-successor gap is missing+missing, which the lock honestly reports as completed-by-other (#904). Exactly one owner still publishes. The loser may be timed-out, completed-by-other, or unavailable — never parsed. Do not delay the clean-release path by treating missing+missing as wait-out. --- tests/cache-refresh-lock-process.test.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/cache-refresh-lock-process.test.ts b/tests/cache-refresh-lock-process.test.ts index d837f81e..752f332e 100644 --- a/tests/cache-refresh-lock-process.test.ts +++ b/tests/cache-refresh-lock-process.test.ts @@ -79,7 +79,12 @@ describe('warm refresh child-process regression', () => { const loserOutcome = await waitForAny(barriers, [ `${loser}.timed-out`, `${loser}.parsed`, `${loser}.completed-by-other`, `${loser}.unavailable`, ]) - expect(loserOutcome, (await readdir(barriers)).join(',')).toBe(`${loser}.timed-out`) + // Exactly one owner publishes. The loser of a stale-lock contest is not + // the owner: `timed-out` is the usual wait-out, but missing+missing during + // the winner's unlink-guard/create-successor gap is honestly + // `completed-by-other` (#904). Do not require timed-out. + expect(loserOutcome, (await readdir(barriers)).join(',')).not.toBe(`${loser}.parsed`) + expect([`${loser}.timed-out`, `${loser}.completed-by-other`, `${loser}.unavailable`]).toContain(loserOutcome) await writeFile(join(barriers, `${winner}.save`), '') await Promise.all([waitForExit(a), waitForExit(b)]) await expect(stat(join(cacheDir, 'session-refresh.lock.takeover'))).rejects.toMatchObject({ code: 'ENOENT' }) @@ -113,7 +118,12 @@ describe('warm refresh child-process regression', () => { const loserOutcome = await waitForAny(barriers, [ `${loser}.timed-out`, `${loser}.parsed`, `${loser}.completed-by-other`, `${loser}.unavailable`, ]) - expect(loserOutcome, (await readdir(barriers)).join(',')).toBe(`${loser}.timed-out`) + // Exactly one owner publishes. The loser of a stale-lock contest is not + // the owner: `timed-out` is the usual wait-out, but missing+missing during + // the winner's unlink-guard/create-successor gap is honestly + // `completed-by-other` (#904). Do not require timed-out. + expect(loserOutcome, (await readdir(barriers)).join(',')).not.toBe(`${loser}.parsed`) + expect([`${loser}.timed-out`, `${loser}.completed-by-other`, `${loser}.unavailable`]).toContain(loserOutcome) await writeFile(join(barriers, `${winner}.save`), '') await Promise.all([waitForExit(a), waitForExit(b)]) await expect(stat(join(cacheDir, 'session-refresh.lock.takeover'))).rejects.toMatchObject({ code: 'ENOENT' })