Skip to content

Commit 53b72c8

Browse files
committed
add retry support to recursive_remove
1 parent 22898ba commit 53b72c8

File tree

2 files changed

+39
-10
lines changed

2 files changed

+39
-10
lines changed

src/build_helper/src/fs/mod.rs

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,20 @@ where
2929
/// unspecified if this helper is called concurrently.
3030
/// - This helper is not robust against TOCTOU problems.
3131
///
32-
/// FIXME: this implementation is insufficiently robust to replace bootstrap's clean `rm_rf`
33-
/// implementation:
34-
///
35-
/// - This implementation currently does not perform retries.
32+
/// FIXME: Audit whether this implementation is robust enough to replace bootstrap's clean `rm_rf`.
3633
#[track_caller]
3734
pub fn recursive_remove<P: AsRef<Path>>(path: P) -> io::Result<()> {
3835
let path = path.as_ref();
39-
let metadata = fs::symlink_metadata(path)?;
36+
37+
// If the path doesn't exist, we treat it as a successful no-op.
38+
// From the caller's perspective, the goal is simply "ensure this file/dir is gone" —
39+
// if it's already not there, that's a success, not an error.
40+
let metadata = match fs::symlink_metadata(path) {
41+
Ok(m) => m,
42+
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
43+
Err(e) => return Err(e),
44+
};
45+
4046
#[cfg(windows)]
4147
let is_dir_like = |meta: &fs::Metadata| {
4248
use std::os::windows::fs::FileTypeExt;
@@ -45,11 +51,34 @@ pub fn recursive_remove<P: AsRef<Path>>(path: P) -> io::Result<()> {
4551
#[cfg(not(windows))]
4652
let is_dir_like = fs::Metadata::is_dir;
4753

48-
if is_dir_like(&metadata) {
49-
fs::remove_dir_all(path)
50-
} else {
51-
try_remove_op_set_perms(fs::remove_file, path, metadata)
54+
const MAX_RETRIES: usize = 5;
55+
const RETRY_DELAY_MS: u64 = 10;
56+
57+
let try_remove = || {
58+
if is_dir_like(&metadata) {
59+
fs::remove_dir_all(path)
60+
} else {
61+
try_remove_op_set_perms(fs::remove_file, path, metadata.clone())
62+
}
63+
};
64+
65+
// Retry deletion a few times to handle transient filesystem errors.
66+
// This is unusual for local file operations, but it's a safeguard against
67+
// edge cases like temporary locks (e.g., on Windows), slow antivirus scans,
68+
// or race conditions where another process/thread is interacting with the file.
69+
for attempt in 0..MAX_RETRIES {
70+
match try_remove() {
71+
Ok(()) => return Ok(()),
72+
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
73+
Err(_) if attempt < MAX_RETRIES - 1 => {
74+
std::thread::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS));
75+
continue;
76+
}
77+
Err(e) => return Err(e),
78+
}
5279
}
80+
81+
Ok(())
5382
}
5483

5584
fn try_remove_op_set_perms<'p, Op>(mut op: Op, path: &'p Path, metadata: Metadata) -> io::Result<()>

src/build_helper/src/fs/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ mod recursive_remove_tests {
1414
let tmpdir = env::temp_dir();
1515
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_nonexistent_path");
1616
assert!(fs::symlink_metadata(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound));
17-
assert!(recursive_remove(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound));
17+
assert!(recursive_remove(&path).is_ok());
1818
}
1919

2020
#[test]

0 commit comments

Comments
 (0)