Recover an interrupted write instead of the stale backup - #313
Merged
Merged
Conversation
WriteText deletes the main file before moving the temp file into its place. A process killed in that window leaves the main file gone, the temp file holding the save that was in flight and the backup holding the content before it. ReadText only ever looked for the backup, so the next load silently restored the previous content and discarded the newest save, leaving the temp file orphaned on disk where nothing would ever read or clean it up. Try the temp file first and fall back to the backup, via a helper that carries the existing timestamped-archive behaviour for both. Archiving rather than deleting is what keeps LoadOrCreate's corrupt-file recovery terminating: it deletes a file it cannot deserialize and reads again, and a candidate left in place would be promoted again every time. Fixes #310 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Pup1E96GmvMiRTNMdABat
|
This was referenced Sep 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #310
The defect
WriteTextdeletes the main file before moving the temp file into its place:A process killed in that window leaves: main missing,
.tmpholding the save that was in flight,.bkholding the content before it.ReadText'sFileNotFoundExceptionhandler only ever looked for.bk. So the next load restored the previous content, discarded the newest save with no error surfaced to the caller, and left the.tmporphaned on disk — nothing reads it again, and nothing cleans it up.The change
The recovery handler tries the temp file first, then the backup:
TryRestoreFromis the existing backup-recovery body — copy the candidate over the main file, then move it aside under a unique timestamped name — lifted into a helper so both candidates get the same treatment rather than the temp path growing a second copy of it.Why the ordering is safe.
WriteTextwrites the temp file in full (WriteAllText) before entering the block that deletes anything, so whenever the main file is missing because of this window, the temp file holds a complete write. Preferring it is not a guess about which file is newer — it is the only ordering in which the window can occur.Why archive rather than delete. Moving the consumed candidate aside is what keeps
LoadOrCreate's corrupt-file recovery terminating. That path deletes a file it cannot deserialize and reads again; a candidate left in place would be promoted again on every pass, and a deleted one would lose content that is worth keeping for inspection. Archiving also means a temp file that was itself truncated — a crash duringWriteAllText, where the main file still exists and is authoritative — can never be promoted twice.ReadText's fall-through is unchanged when no temp file exists, which is the normal case:WriteText'sMoveconsumes the temp file on every successful save, so it is only ever present after an interruption.Tests
TestReadTextRecoversTheInterruptedWriteRatherThanTheStaleBackupTestReadTextArchivesTheRecoveredTempFileRatherThanLeavingItOrphaned.tmpand present under an archive nameThe second is not redundant. A fix that read the temp file and left it in place would pass the first while leaving the orphan the issue also reports; one that deleted it would pass both halves of "not orphaned" while losing the content and breaking the corrupt-file loop. Asserting on the archive pins the behaviour the fix actually relies on.
SetUpInterruptedWritebuilds the state the issue describes rather than trying to kill a process mid-write, which is what makes this testable at all againstMockFileSystem.TestReadTextRestoresFromBackupIfMainFileMissingis untouched and still passes — with no temp file present, the backup path runs exactly as before.Proved failing without the fix. Reverting only
AppDataStorage/AppData.csand keeping both tests:Both fail on the reported symptoms — the stale backup winning, and the orphan left behind — not on a message that merely changed shape. The 103 pre-existing tests are unaffected in both directions.
Verification
dotnet build AppDataStorage.sln -c Release— succeeded, 0 errorsdotnet test AppDataStorage.sln -c Release— 105 total, 105 passed, 0 failed, 0 skippedAppData.cs— 2 of 105 failed, as aboveRun on .NET SDK 10.0.401, Linux. The 3 build warnings (
System.Text.Encodings.Web/System.IO.Pipelines/System.Text.Json10.0.12 reporting nonet7.0support) are pre-existing and unrelated — they reproduce identically with this change reverted.What this does not do
The issue offers a second, complementary fix: restructure
WriteTextso the original is never deleted before the replacement is in place. That is deliberately left alone here.It is the better long-term shape, but it is a change to the write protocol rather than to recovery — it needs an overwriting
Move, whichnetstandard2.0(one of this library's six target frameworks) does not offer, so it would mean a per-TFM implementation of the durability path. That belongs in its own change, weighed on its own. This fix is also still needed afterwards: any repository written by a version that has the currentWriteTextcan already have an orphaned.tmpon disk, and onlyReadTextcan recover it.Worth stating plainly: this narrows the consequence of the window rather than closing the window. A crash still loses nothing, but the save is recovered on the next load rather than never — it does not make
WriteTextatomic.🤖 Generated with Claude Code
https://claude.ai/code/session_012Pup1E96GmvMiRTNMdABat
Generated by Claude Code