fix: write persistence files atomically#34
Merged
Conversation
FileDataStoreConfig persisted both data stores and the resolver index with file.writeAsString, which truncates the target before writing the new contents. A crash, OOM kill, or power loss mid-write left the file torn — on the next launch jsonDecode throws (only PathNotFoundException is caught), so that partition's data is lost or hydration breaks. This is the default persistor on mobile/desktop. Write to a sibling temp file, flush it to disk, then rename over the target. A same-filesystem rename is atomic, so the target always holds either the complete previous contents or the complete new contents. Modern Dart's File.rename replaces an existing regular file on both POSIX and Windows, so no platform special-casing is needed. The .tmp suffix is not matched by the worker's data store file listing, so a temp file orphaned by an interrupted write is ignored and overwritten on the next write rather than loaded as a store.
5 tasks
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.
Summary
FileDataStoreConfigpersisted both data stores and the resolver index withfile.writeAsString, which opens withFileMode.write— truncating the target to zero before writing the new contents. That leaves a window where a crash, OOM kill, or power loss corrupts the file:On the next launch
jsonDecodethrows (and onlyPathNotFoundExceptionis caught), so the error propagates throughLoon.hydrateand that partition's data is lost or startup breaks. This affects the default persistor on mobile/desktop, and the resolver file is equally exposed (it's the path → store-name index).Fix
Write to a sibling temp file, flush it to disk, then
renameover the target:A same-filesystem
renameis atomic, so the target always holds either the complete previous contents or the complete new contents — never a torn file. Theflushbefore the rename ensures the data is durable so the rename can't expose an empty file after power loss.Why this is safe cross-platform
Modern Dart (3.x, which the package requires) documents that
File.renameremoves an existing destination file first, and on Windows it usesMoveFileEx-style replace semantics for regular files (our case — these are plain.jsonfiles, not symlinks). So no platform special-casing is needed.Orphaned temp files are harmless
The worker's data store file listing (
fileRegex) matches only…\.json$, so ausers.json.tmporphaned by an interrupted write is not loaded as a store — it's simply overwritten on the next write. A test pins this.Scope / what this does not cover
This makes each file individually torn-proof. It does not make a multi-file sync (multiple partitions + resolver) transactionally consistent — that would need a manifest/generation pointer and is out of scope. For static persistence keys the resolver rarely changes, so this fix covers the real, common failure (a torn individual file on a process kill). SQLite/IndexedDB backends are already transactional and are unaffected.
Test plan
flutter test test/native --concurrency=1— 86/86 green (the existing 42 round-trip persistor tests confirm the atomic write produces identical content).tmpfiles remain after a sync.tmpfilesflutter analyzeclean on changed filesGenerated by Claude Code