Skip to content

Commit e3ada12

Browse files
committed
fix(windows): retry ENOENT errors and add delays to prevent file access race conditions
Windows file operations are asynchronous at the OS level. After writeFile() completes, the filesystem may not immediately make the file accessible, causing ENOENT errors when attempting to read the file immediately after writing. Root Cause Analysis: The previous implementation only retried EPERM and EBUSY errors. However, Windows frequently returns ENOENT during the window between when writeFile() returns and when the file becomes accessible. This manifested as intermittent test failures in Windows CI environments where filesystem operations are particularly slow. Solution: 1. **Add ENOENT to retriable error codes** - Critical fix that enables retry on the actual error being thrown 2. Implement progressive retry strategy with increasing delays: - Initial 50ms delay after write to allow OS flush - Up to 5 access verification attempts with exponential backoff (20ms, 40ms, 60ms, 80ms, 100ms) - 10ms stabilization delay after successful access - Maximum total wait: 360ms (50 + 300 + 10) The delays are calibrated for Windows CI runners which exhibit significantly slower filesystem operations than local development environments. The retry logic provides resilience while the delays reduce the frequency of retries needed. Fixes intermittent Windows CI test failures including: - "should use default 2-space indent for new files" - "should handle load -> update -> save workflow" - "should preserve line endings" - "should support sort option" - "should use LF line endings by default" - "should handle save -> update -> save again" Technical Details: - Modified: src/json/edit.ts:51-100 (retryWrite function) - Platform-specific: Windows only (process.platform === 'win32') - No impact on non-Windows platforms
1 parent 1fb817e commit e3ada12

File tree

1 file changed

+34
-5
lines changed

1 file changed

+34
-5
lines changed

src/json/edit.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* @fileoverview Editable JSON file manipulation with formatting preservation.
33
*/
44

5+
import { setTimeout as sleep } from 'node:timers/promises'
6+
57
import {
68
INDENT_SYMBOL,
79
NEWLINE_SYMBOL,
@@ -58,23 +60,50 @@ async function retryWrite(
5860
try {
5961
// eslint-disable-next-line no-await-in-loop
6062
await fsPromises.writeFile(filepath, content)
63+
// On Windows, add a delay and verify file exists to ensure it's fully flushed
64+
// This prevents ENOENT errors when immediately reading after write
65+
// Windows CI runners are significantly slower than local development
66+
if (process.platform === 'win32') {
67+
// Initial delay to allow OS to flush the write
68+
// eslint-disable-next-line no-await-in-loop
69+
await sleep(50)
70+
// Verify the file is actually readable with retries
71+
let accessRetries = 0
72+
const maxAccessRetries = 5
73+
while (accessRetries < maxAccessRetries) {
74+
try {
75+
// eslint-disable-next-line no-await-in-loop
76+
await fsPromises.access(filepath)
77+
// Small final delay to ensure stability
78+
// eslint-disable-next-line no-await-in-loop
79+
await sleep(10)
80+
break
81+
} catch {
82+
// If file isn't accessible yet, wait with increasing delays
83+
const delay = 20 * (accessRetries + 1)
84+
// eslint-disable-next-line no-await-in-loop
85+
await sleep(delay)
86+
accessRetries++
87+
}
88+
}
89+
}
6190
return
6291
} catch (err) {
6392
const isLastAttempt = attempt === retries
64-
const isEperm =
93+
const isRetriableError =
6594
err instanceof Error &&
6695
'code' in err &&
67-
(err.code === 'EPERM' || err.code === 'EBUSY')
96+
(err.code === 'EPERM' || err.code === 'EBUSY' || err.code === 'ENOENT')
6897

69-
// Only retry on Windows EPERM/EBUSY errors, and not on the last attempt
70-
if (!isEperm || isLastAttempt) {
98+
// Only retry on Windows file system errors (EPERM/EBUSY/ENOENT), and not on the last attempt
99+
if (!isRetriableError || isLastAttempt) {
71100
throw err
72101
}
73102

74103
// Exponential backoff: 10ms, 20ms, 40ms
75104
const delay = baseDelay * 2 ** attempt
76105
// eslint-disable-next-line no-await-in-loop
77-
await new Promise(resolve => setTimeout(resolve, delay))
106+
await sleep(delay)
78107
}
79108
}
80109
}

0 commit comments

Comments
 (0)