Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Tasks.UnitTests/WriteLinesToFile_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ public void TransactionalModeHandlesConcurrentWritesSuccessfully()
}

[Fact]
public void TransactionalModePreservesAllData()
public void TransactionalModeSucceedsWithConcurrentOverwrites()
{
using (var testEnv = TestEnvironment.Create(_output))
{
Expand Down
52 changes: 49 additions & 3 deletions src/Tasks/FileIO/WriteLinesToFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,55 @@ private bool SaveAtomically(AbsolutePath filePath, string contentsAsString, Enco
}
catch (IOException moveEx)
{
// Move failed, log and return
string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath);
Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, moveEx.Message, lockedFileMessage);
// Only retry with Replace if the destination now exists (concurrent write race).
if (System.IO.File.Exists(filePath))
{
// Retry Replace a few times with a small delay, mirroring the later Replace retry logic.
bool TryReplaceWithRetry(string sourcePath, string destinationPath, out IOException lastReplaceException)
{
lastReplaceException = null;

for (int retry = 0; retry < 3; retry++)
{
try
{
if (retry > 0)
{
System.Threading.Thread.Sleep(10);
}

System.IO.File.Replace(sourcePath, destinationPath, null, true);
return true;
}
catch (IOException replaceEx)
{
lastReplaceException = replaceEx;
// Continue to next retry.
}
}

return false;
}

if (TryReplaceWithRetry(temporaryFilePath, filePath, out IOException lastReplaceException))
{
temporaryFilePath = null; // Mark as successfully replaced
return !Log.HasLoggedErrors;
}
// All Replace retries failed; log the original move error as the root cause.
string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath);
string replaceMessage = lastReplaceException != null ? lastReplaceException.Message : string.Empty;
Log.LogErrorWithCodeFromResources(
"WriteLinesToFile.ErrorOrWarning",
filePath.OriginalValue,
moveEx.Message + (string.IsNullOrEmpty(replaceMessage) ? string.Empty : " " + replaceMessage),
lockedFileMessage);
return !Log.HasLoggedErrors;
}

// Destination doesn't exist; move failed for a different reason.
string lockedFileDiagnostics = LockCheck.GetLockedFileMessage(filePath);
Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, moveEx.Message, lockedFileDiagnostics);
return !Log.HasLoggedErrors;
}
}
Expand Down
Loading