//! Durable file updates (spec ยง13.6): every write that must survive a //! crash goes through here. //! //! A file is replaced by writing a uniquely named temporary file in the //! same directory, syncing it, renaming it over the target, and syncing //! the directory so the rename itself is on disk. A crash at any point //! leaves either the old file or the new one, never a torn one; at worst //! a stray temporary file, which nothing reads. //! //! In tests, `crash_after(n)` makes the n-th durable step on the current //! thread fail as if the process had died there, so a test can stop an //! operation at every step and check what is left behind. use rand::RngCore; use std::fs; use std::io::{self, Write}; use std::path::Path; /// Replace `path` with `data`, atomically and durably. pub fn write_atomic(path: &Path, data: &[u8]) -> io::Result<()> { let dir = path.parent().unwrap_or(Path::new(".")); let name = path .file_name() .map(|n| n.to_string_lossy().into_owned()) .unwrap_or_default(); let mut nonce = [0u8; 6]; rand::rngs::OsRng.fill_bytes(&mut nonce); let tmp = dir.join(format!(".{name}.tmp-{}", hex::encode(nonce))); step("create temporary file")?; let written = (|| { let mut f = fs::File::create(&tmp)?; f.write_all(data)?; f.sync_all()?; step("rename into place")?; fs::rename(&tmp, path) })(); if let Err(e) = written { if !is_injected(&e) { let _ = fs::remove_file(&tmp); } return Err(e); } sync_dir(dir) } /// Rename `from` to `to` and make the rename durable. pub fn rename(from: &Path, to: &Path) -> io::Result<()> { step("rename")?; fs::rename(from, to)?; sync_dir(to.parent().unwrap_or(Path::new("."))) } /// Flush a directory's entries (new, renamed or removed files) to disk. /// A no-op where directories cannot be opened for syncing. pub fn sync_dir(dir: &Path) -> io::Result<()> { #[cfg(unix)] { fs::File::open(dir)?.sync_all() } #[cfg(not(unix))] { let _ = dir; Ok(()) } } /// A point where a crash may happen. Outside tests it does nothing. pub fn step(what: &str) -> io::Result<()> { #[cfg(test)] { failpoint::hit(what) } #[cfg(not(test))] { let _ = what; Ok(()) } } const INJECTED: &str = "injected crash"; fn is_injected(e: &io::Error) -> bool { e.to_string().starts_with(INJECTED) } #[cfg(test)] pub(crate) mod failpoint { use std::cell::Cell; use std::io; thread_local! { static COUNTDOWN: Cell> = const { Cell::new(None) }; static FIRED: Cell = const { Cell::new(false) }; } /// Fail the `n`-th durable step from now (1-based) on this thread, /// and every step after it, as a dead process would. pub fn crash_after(n: usize) { COUNTDOWN.with(|c| c.set(Some(n))); FIRED.with(|f| f.set(false)); } /// Stop injecting; returns whether a crash was injected. pub fn disarm() -> bool { COUNTDOWN.with(|c| c.set(None)); FIRED.with(|f| f.replace(false)) } pub(super) fn hit(what: &str) -> io::Result<()> { if FIRED.with(|f| f.get()) { return Err(io::Error::other(format!( "{} (after) at {what}", super::INJECTED ))); } COUNTDOWN.with(|c| match c.get() { Some(1) => { c.set(None); FIRED.with(|f| f.set(true)); Err(io::Error::other(format!("{} at {what}", super::INJECTED))) } Some(n) => { c.set(Some(n - 1)); Ok(()) } None => Ok(()), }) } }