Update safeRemove() to retry on EINTR / EBUSY filesystem responses

This change improves the robustness of local file cleanup by enhancing util.safeRemove() to retry deletion when the underlying filesystem operation is interrupted or temporarily busy.

What changed

safeRemove() now retries remove() when:

EINTR — Interrupted system call (signal received before syscall completion)

EBUSY — transient “resource busy” conditions

Retries are capped and include a small backoff to avoid tight retry loops.

Existing behaviour is preserved:

ENOENT is treated as success (file already removed).

All other error conditions are logged once and returned.

Why this is needed

Under normal operation (signal handling, network interruptions, shutdown sequences), POSIX systems such as Linux and FreeBSD can legitimately return EINTR for file deletion calls. Treating this as a hard failure creates noisy logs and can leave temporary files behind even though a retry would succeed.

In rarer cases, EBUSY may also be returned for transient filesystem conditions. A limited retry avoids false error reporting while still surfacing persistent failures.

Scope

Applies to Linux and FreeBSD.

No change to functional semantics or error visibility for genuine failures.

Reduces spurious “Interrupted system call” errors observed in debug logs, particularly for temporary resume download files.

This aligns safeRemove() with POSIX-recommended retry behaviour for interruptible system calls while keeping retries bounded and safe.
This commit is contained in:
abraunegg 2025-12-30 07:21:43 +11:00
commit 30523670d5

View file

@ -3,7 +3,7 @@ module util;
// What does this module require to function?
import core.memory;
import core.stdc.errno : ENOENT;
import core.stdc.errno : ENOENT, EINTR, EBUSY;
import core.stdc.stdlib;
import core.stdc.string;
import core.sys.posix.pwd;
@ -208,23 +208,37 @@ void safeRename(const(char)[] oldPath, const(char)[] newPath, bool dryRun) {
}
}
// Deletes the specified file without throwing an exception if it does not exists
// Deletes the specified file without throwing an exception if there is an issue
void safeRemove(const(char)[] path) {
// Set this function name
string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({})));
// Attempt the local deletion
try {
// Attempt once; no pre-check to avoid TOCTTOU
remove(path); // attempt once, no pre-check
return; // removed
} catch (FileException e) {
if (e.errno == ENOENT) { // already gone → fine
return; // nothing to do
string thisFunctionName = format("%s.%s", strip(__MODULE__), strip(getFunctionName!({})));
int maxAttempts = 5;
foreach (attempt; 0 .. maxAttempts) {
try {
// Attempt to remove; no pre-check to avoid TOCTTOU
remove(path);
return;
} catch (FileException e) {
if (e.errno == ENOENT) return; // already gone → fine
if (e.errno == EINTR) { // Interrupted by signal → retry
// 10ms backoff to avoid spinning if signals are frequent
Thread.sleep(dur!"msecs"(10 * (attempt + 1)));
continue;
}
if (e.errno == EBUSY) { // Filesystem was busy → retry
// 25ms backoff to avoid spinning if signals are frequent
Thread.sleep(dur!"msecs"(25 * (attempt + 1)));
continue;
}
// Anything else is noteworthy (EISDIR, EACCES, etc.)
displayFileSystemErrorMessage(e.msg, thisFunctionName, to!string(path));
return;
}
// Anything else is noteworthy (EISDIR, EACCES, etc.)
displayFileSystemErrorMessage(e.msg, thisFunctionName, to!string(path));
}
// If we get here, we exhausted retries on EINTR
// Log the last failure
displayFileSystemErrorMessage("Failed to remove file after retries: " ~ to!string(path), thisFunctionName, to!string(path));
}
// Returns the quickXorHash base64 string of a file, or an empty string on failure