Frontend Labs
⋯

41. Files that survive crashes: fsync and atomic replacement

std::fs::write(path, data) truncates the file and then writes it. If the machine crashes between the two steps, the file is empty or partly written. Data you have written sits in the page cache until the kernel flushes it (OS Ch 14). The crash-safe sequence is the one from OS Ch 77: write a temporary file in the same directory, fsync it, rename it over the target, then fsync the directory.

use std::fs::{self, File};
use std::io::{self, Write};
use std::path::Path;

/// Replace `path` with `data` atomically: readers see the old file or the new one, never a mix.
pub fn atomic_write(path: &Path, data: &[u8]) -> io::Result<()> {
    let dir = path.parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or(Path::new("."));
    let file_name = path.file_name().ok_or_else(|| io::Error::other("path has no file name"))?;
    // Same directory = same filesystem, so rename(2) is atomic.
    let tmp = dir.join(format!(".{}.tmp.{}", file_name.to_string_lossy(), std::process::id()));

    let result = (|| {
        let mut f = File::create(&tmp)?;
        f.write_all(data)?;
        f.sync_all()?;                 // fsync: the DATA is on stable storage
        drop(f);
        fs::rename(&tmp, path)?;       // atomic swap of the directory entry
        File::open(dir)?.sync_all()    // fsync the DIRECTORY: the rename itself is durable
    })();

    if result.is_err() {
        let _ = fs::remove_file(&tmp); // do not leave temporary files behind on failure
    }
    result
}

fn main() -> io::Result<()> {
    let path = std::env::temp_dir().join("rust-guide-state.json");
    atomic_write(&path, br#"{"version": 1}"#)?;
    atomic_write(&path, br#"{"version": 2}"#)?;
    println!("{}", fs::read_to_string(&path)?);
    fs::remove_file(path)
}

On macOS, fsync does not force the drive's own write cache to flush. Full durability there needs fcntl(F_FULLFSYNC). Databases such as SQLite issue it, and Rust's File::sync_all uses it on Apple platforms.

The Go version of this lab is in OS Ch 77. The Rust version adds one thing: the closure plus is_err cleanup makes sure the temporary file is removed on every failure path. Ch 63's key-value store uses this function for snapshots.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36