diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e103bee..2b09b226 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI on: push: + branches: [main] pull_request: schedule: - cron: '0 8 * * 1' diff --git a/src/site-memory/file-lock.test.ts b/src/site-memory/file-lock.test.ts index d323eab2..bfedb2e9 100644 --- a/src/site-memory/file-lock.test.ts +++ b/src/site-memory/file-lock.test.ts @@ -1,13 +1,19 @@ import { spawnSync } from 'node:child_process'; -import { mkdtemp, readFile, rm, stat, utimes, writeFile } from 'node:fs/promises'; +import { mkdtemp, open, readFile, rm, stat, utimes, writeFile } from 'node:fs/promises'; import { hostname, tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { lockPathFor, withFileLock } from './file-lock.js'; const tempDirs: string[] = []; +vi.mock('node:fs/promises', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, open: vi.fn(actual.open) }; +}); + afterEach(async () => { + vi.restoreAllMocks(); await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); }); @@ -28,6 +34,17 @@ describe('site memory file lock', () => { await expect(exists(lockPathFor(target))).resolves.toBe(false); }); + it('retries a transient Windows EPERM when creating the lock', async () => { + const target = await tempTarget(); + vi.mocked(open).mockRejectedValueOnce( + Object.assign(new Error('transient Windows lock'), { code: 'EPERM' }), + ); + vi.spyOn(process, 'platform', 'get').mockReturnValue('win32'); + + await expect(withFileLock(target, async () => 'written')).resolves.toBe('written'); + await expect(exists(lockPathFor(target))).resolves.toBe(false); + }); + it('releases the lock when the critical section throws', async () => { const target = await tempTarget(); diff --git a/src/site-memory/file-lock.ts b/src/site-memory/file-lock.ts index 5e1761f4..eae2265f 100644 --- a/src/site-memory/file-lock.ts +++ b/src/site-memory/file-lock.ts @@ -77,11 +77,15 @@ async function acquire(lockPath: string, options: FileLockOptions): Promise { let handle; - try { - handle = await open(lockPath, 'wx'); - } catch (err) { - if (isNodeError(err) && err.code === 'EEXIST') return false; - throw err; + for (let attempt = 0; ; attempt += 1) { + try { + handle = await open(lockPath, 'wx'); + break; + } catch (err) { + if (isNodeError(err) && err.code === 'EEXIST') return false; + if (process.platform !== 'win32' || !isNodeError(err) || err.code !== 'EPERM' || attempt >= 2) throw err; + await delay(backoffMs(attempt)); + } } try { await handle.writeFile(body, 'utf8');