Skip to content

Make fs::remove_dir_all race-resistant #170

Closed
@pitaj

Description

@pitaj

Proposal

Problem statement

Currently, fs::remove_dir_all will fail if the directory in question does not exist. It will also fail if any descendant fails deletion for any reason, even if that's because it doesn't exist.

This means the API is not idempotent, and can fail when multiple threads or processes call it concurrently.

Motivation, use-cases

The current documentation for this function does not directly state the above behavior, instead simply punting to the docs for fs::remove_file and fs::remove_dir.

The current behavior is also in stark contrast to fs::create_dir_all, which has been race-resistant since 2017:

This function will return an error in the following situations, but is not limited to just these cases:

  • If any directory in the path specified by path does not already exist and it could not be created otherwise. The specific error conditions for when a directory is being created (after it is determined to not exist) are outlined by fs::create_dir.

Notable exception is made for situations where any of the directories specified in the path could not be created as it was being created concurrently. Such cases are considered to be successful. That is, calling create_dir_all concurrently from multiple threads or processes is guaranteed not to fail due to a race condition with itself.

Solution sketches

Change the remove_dir_all algorithm to work similarly to create_dir_all:

  • If the directory at path does not exist, immediately exit Ok(()).
  • Accept ErrorKind::NotFound as if the deletion succeeded.
  • Any other errors from read_dir and remove_dir are immediately returned.
pub fn remove_dir_all(path: &Path) -> io::Result<()> {
    let filetype = match fs::symlink_metadata(path) {
        Ok(x) => x.file_type(),
        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
        Err(e) => return Err(e),
    };
    if filetype.is_symlink() {
        match fs::remove_file(path) {
            Ok(_) => Ok(()),
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
            Err(e) => Err(e),
        }
    } else { remove_dir_all_recursive(path) }
}

fn remove_dir_all_recursive(path: &Path) -> io::Result<()> {
    let dir = match fs::read_dir(path) {
        Ok(x) => x,
        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
        Err(e) => return Err(e),
    };
    for child in dir {
        let child = match child {
            Ok(x) => x,
            Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
            Err(e) => return Err(e),
        };
        let file_type = match child.file_type() {
            Ok(x) => x,
            Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
            Err(e) => return Err(e),
        };
        if file_type.is_dir() {
            remove_dir_all_recursive(&child.path())?;
        } else {
            match fs::remove_file(&child.path())  {
                Ok(_) => (),
                Err(e) if e.kind() == io::ErrorKind::NotFound => (),
                Err(e) => return Err(e),
            }
        }
    }
    match fs::remove_dir(path) {
        Ok(_) => Ok(()),
        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
        Err(e) => Err(e),
    }
}

Links and related work

What happens now?

This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals in its weekly meeting. You should receive feedback within a week or two.

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions