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