Skip to content

Commit dd42609

Browse files
chenxiaoyu209claude
andcommitted
fix(backup): retry fs.rm on EBUSY to prevent tar retry-loop collapse on Windows
On Windows, fs.rm can throw EBUSY when the runTar process still holds a file descriptor. The old code caught and logged the error but left the locked file behind, causing the next runTar attempt to fail immediately and defeating the BACKUP_TAR_BACKOFF_MS retry mechanism. Fix: wrap fs.rm in a removeTempArchive helper that retries up to 3 times with a 100ms sleep on EBUSY, giving the OS time to release the handle. Fixes #101382. Co-Authored-By: Claude <[email protected]>
1 parent 36a91ac commit dd42609

1 file changed

Lines changed: 25 additions & 1 deletion

File tree

src/infra/backup-create.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,30 @@ function isTarEofRaceError(err: unknown): boolean {
170170

171171
export type BackupTarRetryLogger = (message: string) => void;
172172

173+
const RM_EBUSY_RETRY_MAX = 3;
174+
const RM_EBUSY_RETRY_MS = 100;
175+
176+
/**
177+
* Removes a temp archive file, retrying on EBUSY (Windows file-lock
178+
* contention) so a leaked tar fd doesn't defeat the outer retry loop.
179+
*/
180+
async function removeTempArchive(tempArchivePath: string): Promise<void> {
181+
for (let attempt = 1; attempt <= RM_EBUSY_RETRY_MAX; attempt += 1) {
182+
try {
183+
await fs.rm(tempArchivePath, { force: true });
184+
return;
185+
} catch (err) {
186+
const code = (err as NodeJS.ErrnoException).code;
187+
if (code === "ENOENT") return;
188+
if (code === "EBUSY" && attempt < RM_EBUSY_RETRY_MAX) {
189+
await sleep(RM_EBUSY_RETRY_MS);
190+
continue;
191+
}
192+
throw err;
193+
}
194+
}
195+
}
196+
173197
async function writeTarArchiveWithRetry(params: {
174198
tempArchivePath: string;
175199
runTar: () => Promise<void>;
@@ -188,7 +212,7 @@ async function writeTarArchiveWithRetry(params: {
188212
break;
189213
}
190214
try {
191-
await fs.rm(params.tempArchivePath, { force: true });
215+
await removeTempArchive(params.tempArchivePath);
192216
} catch (cleanupErr) {
193217
const code = (cleanupErr as NodeJS.ErrnoException).code;
194218
if (code && code !== "ENOENT") {

0 commit comments

Comments
 (0)