You
The journal test fails on Windows CI about one run in five and never locally. Find out why and fix it.
Assistant
91.4 tok/s
TTFT 0.412s
gen 0.777s
13,785 tokens
SearchacquireLock|openJournal7 matches · 4 files
Readsrc/store/lock.ts13 lines
Readtest/journal.test.ts23 lines
Assistant
88.7 tok/s
TTFT 0.301s
gen 0.812s
14,557 tokens
Runnode --test --test-name-pattern="reopens" test/journal.test.tsexit 0
Assistant
It passes on macOS, which fits the pattern. The race is in acquireLock():
close()closes the handle and removes.tern/lock.- On Windows that file stays pending delete for a few milliseconds.
- The next
openJournal()callsopen(path, 'wx')inside that window and getsEPERMorEBUSY.
Nothing retries, so the test fails whenever CI is fast enough to land in the gap. I will retry only those two codes, with a short backoff, and keep failing fast on everything else.
87.9 tok/s
TTFT 0.355s
gen 1.206s
14,822 tokens
Edit+16−1
- search
/** An exclusive lock file: only one tern process may write a journal. */ export async function acquireLock(dir: string): Promise<() => Promise<void>> { const path = join(dir, '.tern', 'lock'); const handle: FileHandle = await open(path, 'wx'); await handle.write(String(process.pid)); return async () => { await handle.close(); await rm(path, { force: true }); }; }- replace
/** Codes Windows returns while a just-closed lock file is still being released. */ const RELEASING = new Set(['EBUSY', 'EPERM']); /** An exclusive lock file: only one tern process may write a journal. */ export async function acquireLock(dir: string): Promise<() => Promise<void>> { const path = join(dir, '.tern', 'lock'); const handle = await openExclusive(path); await handle.write(String(process.pid)); return async () => { await handle.close(); await rm(path, { force: true }); }; } async function openExclusive(path: string, attempts = 5): Promise<FileHandle> { for (let attempt = 0; ; attempt++) { try { return await open(path, 'wx'); } catch (err) { const code = (err as NodeJS.ErrnoException).code ?? ''; if (!RELEASING.has(code) || attempt === attempts - 1) throw err; await new Promise((r) => setTimeout(r, 20 * 2 ** attempt)); } } }
4+/** Codes Windows returns while a just-closed lock file is still being released. */5+const RELEASING = new Set(['EBUSY', 'EPERM']);6+7- const handle: FileHandle = await open(path, 'wx');10+ const handle = await openExclusive(path);16+}17+18+async function openExclusive(path: string, attempts = 5): Promise<FileHandle> {19+ for (let attempt = 0; ; attempt++) {20+ try {21+ return await open(path, 'wx');22+ } catch (err) {23+ const code = (err as NodeJS.ErrnoException).code ?? '';24+ if (!RELEASING.has(code) || attempt === attempts - 1) throw err;25+ await new Promise((r) => setTimeout(r, 20 * 2 ** attempt));26+ }27+ }
Raw input and output
Input
{
"path": "src/store/lock.ts",
"search": "/** An exclusive lock file: only one tern process may write a journal. */\nexport async function acquireLock(dir: string): Promise<() => Promise<void>> {\n const path = join(dir, '.tern', 'lock');\n const handle: FileHandle = await open(path, 'wx');\n await handle.write(String(process.pid));\n return async () => {\n await handle.close();\n await rm(path, { force: true });\n };\n}",
"replace": "/** Codes Windows returns while a just-closed lock file is still being released. */\nconst RELEASING = new Set(['EBUSY', 'EPERM']);\n\n/** An exclusive lock file: only one tern process may write a journal. */\nexport async function acquireLock(dir: string): Promise<() => Promise<void>> {\n const path = join(dir, '.tern', 'lock');\n const handle = await openExclusive(path);\n await handle.write(String(process.pid));\n return async () => {\n await handle.close();\n await rm(path, { force: true });\n };\n}\n\nasync function openExclusive(path: string, attempts = 5): Promise<FileHandle> {\n for (let attempt = 0; ; attempt++) {\n try {\n return await open(path, 'wx');\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code ?? '';\n if (!RELEASING.has(code) || attempt === attempts - 1) throw err;\n await new Promise((r) => setTimeout(r, 20 * 2 ** attempt));\n }\n }\n}\n"
}Output
Replaced 1 occurrence(s) in src/store/lock.ts
Runnpm testexit 0
File issueLock file is left behind when tern is killed mid-sync
Assistant
Fixed, and the suite passes (6/6).
- Cause: on Windows a just-removed
.tern/lockis still pending delete for a few milliseconds, so an immediate reopen fails withEPERMorEBUSY. - Fix:
acquireLock()now goes throughopenExclusive(), which retries only those two codes — 5 attempts, 20 ms doubling — and rethrows anything else, including a realEEXIST. - Worth knowing: macOS never shows this, so the proof is
windows-latest. Re-run CI a few times before trusting it.
While reading the lock code I filed TER-13: a lock left behind by a killed tern blocks every later sync.
92.3 tok/s
TTFT 0.322s
gen 1.062s
15,954 tokens
Edited 1 file
+16−1