OXYGENHUB

Coverage / crates/oxy-core/src/durable.rs 93.4%

99 of 106 lines run, 31 of 37 functions · run #387 on main · run missed

1
//! Durable file updates (spec §13.6): every write that must survive a
2
//! crash goes through here.
3
//!
4
//! A file is replaced by writing a uniquely named temporary file in the
5
//! same directory, syncing it, renaming it over the target, and syncing
6
//! the directory so the rename itself is on disk. A crash at any point
7
//! leaves either the old file or the new one, never a torn one; at worst
8
//! a stray temporary file, which nothing reads.
9
//!
10
//! In tests, `crash_after(n)` makes the n-th durable step on the current
11
//! thread fail as if the process had died there, so a test can stop an
12
//! operation at every step and check what is left behind.
13
14
use rand::RngCore;
15
use std::fs;
16
use std::io::{self, Write};
17
use std::path::Path;
18
19
/// Replace `path` with `data`, atomically and durably.
208682×
pub fn write_atomic(path: &Path, data: &[u8]) -> io::Result<()> {
218682×
    write_atomic_mode(path, data, false)
228682×
}
23
24
/// [`write_atomic`] for a secret: on Unix the file is created readable by
25
/// its owner only, so the secret is never on disk with wider permissions.
26104×
pub fn write_atomic_private(path: &Path, data: &[u8]) -> io::Result<()> {
27104×
    write_atomic_mode(path, data, true)
28104×
}
29
308786×
fn write_atomic_mode(path: &Path, data: &[u8], private: bool) -> io::Result<()> {
318786×
    let dir = path.parent().unwrap_or(Path::new("."));
328786×
    let name = path
338786×
        .file_name()
348786×
        .map(|n| n.to_string_lossy().into_owned())
358786×
        .unwrap_or_default();
368786×
    let mut nonce = [0u8; 6];
378786×
    rand::rngs::OsRng.fill_bytes(&mut nonce);
388786×
    let tmp = dir.join(format!(".{name}.tmp-{}", hex::encode(nonce)));
398786×
    step("create temporary file")?;
408781×
    let written = (|| {
418781×
        let mut open = fs::OpenOptions::new();
428781×
        open.write(true).create_new(true);
43
        #[cfg(unix)]
448781×
        if private {
45
            use std::os::unix::fs::OpenOptionsExt;
46104×
            open.mode(0o600);
478677×
        }
48
        #[cfg(not(unix))]
49
        let _ = private;
508781×
        let mut f = open.open(&tmp)?;
518779×
        f.write_all(data)?;
528779×
        f.sync_all()?;
538779×
        step("rename into place")?;
548774×
        fs::rename(&tmp, path)
55
    })();
568781×
    if let Err(e) = written {
577×
        if !is_injected(&e) {
582×
            let _ = fs::remove_file(&tmp);
595×
        }
607×
        return Err(at("write", path, e));
618774×
    }
628774×
    sync_dir(dir).map_err(|e| at("sync the directory of", path, e))
638786×
}
64
65
/// `e`, naming what failed and on which file, so a failure deep in a
66
/// command says where it happened (an injected test crash stays as is).
67
/// The kind is kept for callers that test it.
688×
fn at(what: &str, path: &Path, e: io::Error) -> io::Error {
698×
    if is_injected(&e) {
705×
        return e;
713×
    }
723×
    io::Error::new(e.kind(), format!("{what} {}: {e}", path.display()))
738×
}
74
75
/// Rename `from` to `to` and make the rename durable.
761×
pub fn rename(from: &Path, to: &Path) -> io::Result<()> {
771×
    step("rename")?;
781×
    fs::rename(from, to).map_err(|e| at(&format!("rename {} to", from.display()), to, e))?;
790×
    sync_dir(to.parent().unwrap_or(Path::new("."))).map_err(|e| at("sync the directory of", to, e))
801×
}
81
82
/// Flush a directory's entries (new, renamed or removed files) to disk.
83
/// A no-op where directories cannot be opened for syncing.
8436663×
pub fn sync_dir(dir: &Path) -> io::Result<()> {
85
    #[cfg(unix)]
86
    {
8736663×
        fs::File::open(dir)?.sync_all()
88
    }
89
    #[cfg(not(unix))]
90
    {
91
        let _ = dir;
92
        Ok(())
93
    }
9436663×
}
95
96
/// A point where a crash may happen. Outside tests it does nothing.
9729060×
pub fn step(what: &str) -> io::Result<()> {
98
    #[cfg(test)]
99
    {
10018428×
        failpoint::hit(what)
101
    }
102
    #[cfg(not(test))]
103
    {
10410632×
        let _ = what;
10510632×
        Ok(())
106
    }
10729060×
}
108
109
const INJECTED: &str = "injected crash";
110
11115×
fn is_injected(e: &io::Error) -> bool {
11215×
    e.to_string().starts_with(INJECTED)
11315×
}
114
115
#[cfg(test)]
116
pub(crate) mod failpoint {
117
    use std::cell::Cell;
118
    use std::io;
119
120
    thread_local! {
121
        static COUNTDOWN: Cell<Option<usize>> = const { Cell::new(None) };
122
        static FIRED: Cell<bool> = const { Cell::new(false) };
123
    }
124
125
    /// Fail the `n`-th durable step from now (1-based) on this thread,
126
    /// and every step after it, as a dead process would.
12721×
    pub fn crash_after(n: usize) {
12821×
        COUNTDOWN.with(|c| c.set(Some(n)));
12921×
        FIRED.with(|f| f.set(false));
13021×
    }
131
132
    /// Stop injecting; returns whether a crash was injected.
13321×
    pub fn disarm() -> bool {
13421×
        COUNTDOWN.with(|c| c.set(None));
13521×
        FIRED.with(|f| f.replace(false))
13621×
    }
137
13818428×
    pub(super) fn hit(what: &str) -> io::Result<()> {
13918428×
        if FIRED.with(|f| f.get()) {
1400×
            return Err(io::Error::other(format!(
1410×
                "{} (after) at {what}",
1420×
                super::INJECTED
1430×
            )));
14418428×
        }
14518428×
        COUNTDOWN.with(|c| match c.get() {
146
            Some(1) => {
14719×
                c.set(None);
14819×
                FIRED.with(|f| f.set(true));
14919×
                Err(io::Error::other(format!("{} at {what}", super::INJECTED)))
150
            }
151130×
            Some(n) => {
152130×
                c.set(Some(n - 1));
153130×
                Ok(())
154
            }
15518279×
            None => Ok(()),
15618428×
        })
15718428×
    }
158
}
159
160
#[cfg(test)]
161
mod tests {
162
    use super::*;
163
164
    #[test]
1651×
    fn a_failed_write_names_the_file_and_keeps_its_kind() {
1661×
        let dir = tempfile::tempdir().unwrap();
1671×
        let path = dir.path().join("attest").join("feature").join("x");
1681×
        let e = write_atomic(&path, b"state").unwrap_err();
1691×
        assert_eq!(e.kind(), io::ErrorKind::NotFound);
1701×
        let msg = e.to_string();
1711×
        assert!(
1721×
            msg.starts_with("write ") && msg.contains("attest/feature/x"),
173
            "{msg}"
174
        );
1751×
        write_atomic(&dir.path().join("ok"), b"state").unwrap();
1761×
        let e = rename(&dir.path().join("missing"), &dir.path().join("to")).unwrap_err();
1771×
        assert!(
1781×
            e.to_string().contains("missing") && e.to_string().contains("to"),
179
            "{e}"
180
        );
1811×
    }
182
}