OXYGENHUB
◆ v0.7.0 oxygen / crates / oxy-core / src / durable.rs
5.7 KiB · Rust · last changed Sign in once per hub: oxy auth login, status and logout (closes #47) 1 month ago source blame history raw
4dcb8c09 Andy McCall ↶ 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.
20
pub fn write_atomic(path: &Path, data: &[u8]) -> io::Result<()> {
ff1f62fe Andy McCall ↶ 21
    write_atomic_mode(path, data, false)
22
}
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.
26
pub fn write_atomic_private(path: &Path, data: &[u8]) -> io::Result<()> {
27
    write_atomic_mode(path, data, true)
28
}
29
30
fn write_atomic_mode(path: &Path, data: &[u8], private: bool) -> io::Result<()> {
4dcb8c09 Andy McCall ↶ 31
    let dir = path.parent().unwrap_or(Path::new("."));
32
    let name = path
33
        .file_name()
34
        .map(|n| n.to_string_lossy().into_owned())
35
        .unwrap_or_default();
36
    let mut nonce = [0u8; 6];
37
    rand::rngs::OsRng.fill_bytes(&mut nonce);
38
    let tmp = dir.join(format!(".{name}.tmp-{}", hex::encode(nonce)));
39
    step("create temporary file")?;
40
    let written = (|| {
ff1f62fe Andy McCall ↶ 41
        let mut open = fs::OpenOptions::new();
42
        open.write(true).create_new(true);
43
        #[cfg(unix)]
44
        if private {
45
            use std::os::unix::fs::OpenOptionsExt;
46
            open.mode(0o600);
47
        }
48
        #[cfg(not(unix))]
49
        let _ = private;
50
        let mut f = open.open(&tmp)?;
4dcb8c09 Andy McCall ↶ 51
        f.write_all(data)?;
52
        f.sync_all()?;
53
        step("rename into place")?;
54
        fs::rename(&tmp, path)
55
    })();
56
    if let Err(e) = written {
57
        if !is_injected(&e) {
58
            let _ = fs::remove_file(&tmp);
59
        }
314db9b4 Andy McCall ↶ 60
        return Err(at("write", path, e));
4dcb8c09 Andy McCall ↶ 61
    }
314db9b4 Andy McCall ↶ 62
    sync_dir(dir).map_err(|e| at("sync the directory of", path, e))
4dcb8c09 Andy McCall ↶ 63
}
64
314db9b4 Andy McCall ↶ 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.
68
fn at(what: &str, path: &Path, e: io::Error) -> io::Error {
69
    if is_injected(&e) {
70
        return e;
71
    }
72
    io::Error::new(e.kind(), format!("{what} {}: {e}", path.display()))
73
}
74
4dcb8c09 Andy McCall ↶ 75
/// Rename `from` to `to` and make the rename durable.
76
pub fn rename(from: &Path, to: &Path) -> io::Result<()> {
77
    step("rename")?;
314db9b4 Andy McCall ↶ 78
    fs::rename(from, to).map_err(|e| at(&format!("rename {} to", from.display()), to, e))?;
79
    sync_dir(to.parent().unwrap_or(Path::new("."))).map_err(|e| at("sync the directory of", to, e))
4dcb8c09 Andy McCall ↶ 80
}
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.
84
pub fn sync_dir(dir: &Path) -> io::Result<()> {
85
    #[cfg(unix)]
86
    {
87
        fs::File::open(dir)?.sync_all()
88
    }
89
    #[cfg(not(unix))]
90
    {
91
        let _ = dir;
92
        Ok(())
93
    }
94
}
95
96
/// A point where a crash may happen. Outside tests it does nothing.
97
pub fn step(what: &str) -> io::Result<()> {
98
    #[cfg(test)]
99
    {
100
        failpoint::hit(what)
101
    }
102
    #[cfg(not(test))]
103
    {
104
        let _ = what;
105
        Ok(())
106
    }
107
}
108
109
const INJECTED: &str = "injected crash";
110
111
fn is_injected(e: &io::Error) -> bool {
112
    e.to_string().starts_with(INJECTED)
113
}
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.
127
    pub fn crash_after(n: usize) {
128
        COUNTDOWN.with(|c| c.set(Some(n)));
129
        FIRED.with(|f| f.set(false));
130
    }
131
132
    /// Stop injecting; returns whether a crash was injected.
133
    pub fn disarm() -> bool {
134
        COUNTDOWN.with(|c| c.set(None));
135
        FIRED.with(|f| f.replace(false))
136
    }
137
138
    pub(super) fn hit(what: &str) -> io::Result<()> {
139
        if FIRED.with(|f| f.get()) {
140
            return Err(io::Error::other(format!(
141
                "{} (after) at {what}",
142
                super::INJECTED
143
            )));
144
        }
145
        COUNTDOWN.with(|c| match c.get() {
146
            Some(1) => {
147
                c.set(None);
148
                FIRED.with(|f| f.set(true));
149
                Err(io::Error::other(format!("{} at {what}", super::INJECTED)))
150
            }
151
            Some(n) => {
152
                c.set(Some(n - 1));
153
                Ok(())
154
            }
155
            None => Ok(()),
156
        })
314db9b4 Andy McCall ↶ 157
    }
158
}
159
160
#[cfg(test)]
161
mod tests {
162
    use super::*;
163
164
    #[test]
165
    fn a_failed_write_names_the_file_and_keeps_its_kind() {
166
        let dir = tempfile::tempdir().unwrap();
167
        let path = dir.path().join("attest").join("feature").join("x");
168
        let e = write_atomic(&path, b"state").unwrap_err();
169
        assert_eq!(e.kind(), io::ErrorKind::NotFound);
170
        let msg = e.to_string();
171
        assert!(
172
            msg.starts_with("write ") && msg.contains("attest/feature/x"),
173
            "{msg}"
174
        );
175
        write_atomic(&dir.path().join("ok"), b"state").unwrap();
176
        let e = rename(&dir.path().join("missing"), &dir.path().join("to")).unwrap_err();
177
        assert!(
178
            e.to_string().contains("missing") && e.to_string().contains("to"),
179
            "{e}"
180
        );
4dcb8c09 Andy McCall ↶ 181
    }
182
}