Skip to content

Inconsistent retryDelay in fs.rmSync and asynchronous fs.rm (on Windows) #64016

@louiellan

Description

@louiellan

Version

v24.17.0

Platform

Microsoft Windows NT 10.0.26200.0 x64

Subsystem

fs

What steps will reproduce the bug?

Given that the current workspace looks like this: (just for reference on how the minimal reproducible were structured)

eperm-folder
|---> locked-file.txt
locking.py
index.js

locking.py

import os, signal
os.makedirs("eperm-folder", exist_ok=True)

filepath = "eperm-folder/locked-file.txt"
print(f"locking {filepath} indefinitely, until SIGINT")
fd = os.open(filepath, os.O_RDWR | os.O_CREAT )
os.write(fd, b'Hello, World!')
def sigterm_handler(s, f):
    os.close(fd)
    exit(0)
signal.signal(signal.SIGINT, sigterm_handler)
while True:
    pass

index.js

const fs = require('node:fs');
const fsPromises = require('node:fs/promises');
const path = require('node:path')
const { spawn } = require('node:child_process')
const filepath = "eperm-folder/locked-file.txt"
if (fs.existsSync(filepath)) {
    console.log('deleting eperm-folder/')
    const start = performance.now()
    try { 
        fs.rmSync(path.dirname(filepath), { recursive: true, retryDelay: 5 * 1000, maxRetries: 1 })
    }
    catch (e){
        console.log(e)
        const duration = performance.now() - start;
        console.log(`rmSync - ${duration} ms`)
    }
}

Steps to run:

  1. Run python locking.py first to create eperm-folder/locked-file.txt and to indefinitely lock it (this is to simulate an EPERM/EBUSY error)
  2. Then run node index.js

How often does it reproduce? Is there a required condition?

Always if the directory / file being deleted is locked by another process.

What is the expected behavior? Why is that the expected behavior?

The time elapsed should be around 15,000ms, since the given retryDelay is 5000 (ms), the time elapsed shouldn't be less than 5000ms

For comparison, in the asynchronous fs.rm functions, the time elapsed is around 15,000ms which is what expected

Here's a reproducible script for the asynchronous fs.rm functions

const fs = require('node:fs');
const fsPromises = require('node:fs/promises');
const path = require('node:path')
const filepath = "eperm-folder/locked-file.txt"

if (fs.existsSync(filepath)) {
    console.log('deleting eperm-folder/')
    const start = performance.now()
        fs.rm(
            path.dirname(filepath), 
            { recursive: true, retryDelay: 5 * 1000, maxRetries: 1 },
            (e) => {
                if (e)
                    console.error(e)
                const duration = performance.now() - start;
                console.log(`async fs.rm() - ${duration} ms`)
            }
        )
}

This means that in the fs.rmSync, retries are not delayed

What do you see instead?

The time elapsed in the fs.rmSync operation is between 1-30 ms (on my end)

Additional information

This affects other retryable errors in Windows (e.g., EMFILE, ENFILE, ENOTEMPTY)
Might be related to #55555, but this is Windows-specific

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions