271. fs::rename — Replace a File Atomically, Never Expose a Half-Write
fs::write truncates the file before writing. Crash halfway — or get read halfway — and the world sees a torn file. fs::rename is the atomic swap that fixes it.
The obvious way to save state overwrites in place:
| |
Between the truncate and the final byte, state.json is incomplete. A concurrent reader gets garbage; a crash or power loss leaves it that way permanently. For a config file, a cache, or anything another process watches, that window is a real bug.
The fix is the classic write-then-rename dance:
| |
On every major platform, rename over an existing file is atomic: any observer sees either the old complete file or the new complete file, never a mix and never a missing file. If the process dies before the rename, the old file is untouched and only a .tmp straggler is left behind.
Three details worth knowing:
- Keep the temp file in the same directory.
renamecan’t cross filesystems — a temp file in/tmpand a target on another mount fails with an error (ErrorKind::CrossesDevices). Same directory guarantees same filesystem, and keeps the swap atomic. - Durability needs one more step. Atomic ≠ flushed. If you need the data to survive power loss, open the temp file yourself and call
sync_all()before renaming, instead of usingfs::write. - Unique temp names matter under concurrency. Two processes writing
state.json.tmpwill trample each other. Give each writer its own name (PID, random suffix) — and create it withFile::create_new(bite 169) so collisions fail loudly.
Same moral as try_exists in bite 270: don’t check-then-act on the filesystem — make the filesystem do the transition in one atomic step.