Retry ReplaceFile in case of failure
The Win32 ReplaceFile function performs several IO operations under the
covers. Among these is creating a temporary file to which the original
file will be moved. Aggressive filesystem scanners, such as A/V software
or git fsmontor--daemon, may inspect this temporary file. When such
software wins the race with the actual movement of the file, ReplaceFile
will fail with ERROR_UNABLE_TO_REMOVE_REPLACED. This is unfortunate.
This CL makes base::ReplaceFile a bit more robust against these scanners
by retrying a call to ReplaceFile that fails in this way for up to half
a second. In local experimentation, the second or third attempt tends to
succeed when the first fails.
This is a band-aid over failures of the "ERROR Failed to write
build.ninja." variety.
Bug: 384523565
Change-Id: Ia77e596687fd91daa888bcc6c07604b845093295
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/18020
Reviewed-by: Dirk Pranke <dpranke@google.com>
Reviewed-by: David Turner <digit@google.com>
Commit-Queue: Dirk Pranke <dpranke@google.com>
diff --git a/src/base/files/file_util_win.cc b/src/base/files/file_util_win.cc
index 774d1e3..0bc07bf 100644
--- a/src/base/files/file_util_win.cc
+++ b/src/base/files/file_util_win.cc
@@ -228,13 +228,26 @@
return true;
File::Error move_error = File::OSErrorToFileError(GetLastError());
- // Try the full-blown replace if the move fails, as ReplaceFile will only
- // succeed when |to_path| does exist. When writing to a network share, we may
- // not be able to change the ACLs. Ignore ACL errors then
- // (REPLACEFILE_IGNORE_MERGE_ERRORS).
- if (::ReplaceFile(ToWCharT(&to_path.value()), ToWCharT(&from_path.value()),
- NULL, REPLACEFILE_IGNORE_MERGE_ERRORS, NULL, NULL)) {
- return true;
+ // ReplaceFile will fail with ERROR_UNABLE_TO_REMOVE_REPLACED when it is
+ // racing with another process (e.g., git fsmonitor--deamon). Repeatedly retry
+ // after a short delay for up to half a second in an attempt to win the race.
+ for (int i = 0; i < 11; ++i) {
+ if (i != 0) {
+ ::Sleep(/*dwMilliseconds=*/50);
+ }
+ // Try the full-blown replace if the move fails, as ReplaceFile will only
+ // succeed when |to_path| does exist. When writing to a network share, we
+ // may not be able to change the ACLs. Ignore ACL errors then
+ // (REPLACEFILE_IGNORE_MERGE_ERRORS).
+ if (::ReplaceFile(ToWCharT(&to_path.value()), ToWCharT(&from_path.value()),
+ NULL, REPLACEFILE_IGNORE_MERGE_ERRORS, NULL, NULL)) {
+ return true;
+ }
+ if (::GetLastError() != ERROR_UNABLE_TO_REMOVE_REPLACED) {
+ // Do not retry if replacement failed for any reason other than this one,
+ // as it's possible that one or the other file has been modified.
+ break;
+ }
}
// In the case of FILE_ERROR_NOT_FOUND from ReplaceFile, it is likely that
// |to_path| does not exist. In this case, the more relevant error comes