Skip to main content

gunnar_server/storage/
writer.rs

1//! **Which writer arm this process appends through, decided once, said once.**
2//!
3//! `znippy-plugin-git` has two durable arms with ONE durability contract (the
4//! blob's bytes are durable before the journal row that references them):
5//! `safe` = pwrite → fsync → write → fsync as four blocking syscalls, `uring` =
6//! the same four as ONE `io_uring_enter` with `IOSQE_IO_LINK`. znippy ships
7//! `safe` and never falls back on its own — `UringWriter::create` refuses on a
8//! kernel without a ring, by name, because a pwrite number reported as an
9//! io_uring number would be a lie. **A server is the one process that has to
10//! choose before it opens its first store**, and on the appliance the kernel is
11//! whatever the medium's donor built (tunnr shipped `# CONFIG_IO_URING is not
12//! set` until `[build.installer.data] io = "uring"` asked for it). So:
13//!
14//! 1. the operator's `ZNIPPY_GIT_WRITER=` wins, spelled exactly (a dev box, a
15//!    bench column);
16//! 2. else the medium's `korp.data.io=uring|safe` token off `/proc/cmdline`
17//!    (baked by nornir-build-thing from the recipe's `data.io`, carried onto
18//!    the installed disk as a site token — the same path `korp.data.fs` takes);
19//! 3. else znippy's shipping default (`safe`).
20//!
21//! Then, and only when the answer is `uring`, ONE `io_uring_setup` probe
22//! ([`znippy_plugin_git::uring_write::probe`]): a refusal (`ENOSYS` = no
23//! ring in this kernel, `EPERM` = `kernel.io_uring_disabled`/seccomp, …) picks
24//! `safe` **by name** — the row says `writer  safe (io_uring unavailable:
25//! ENOSYS)`, never a silent downgrade. The dashboard's disk cell carries the
26//! arm that was built (`… xfs uring`), so a screen photo says which column a
27//! number belongs to.
28//!
29//! [`resolve`] is pure (env value, cmdline text, a probe closure) so the four
30//! doors are asserted without a kernel; [`select`] is the one call
31//! `znippy_engine::arms` makes, and [`selected`] is what the dashboard reads.
32
33use std::sync::OnceLock;
34
35use znippy_plugin_git::arms::{StoreConfig, WriterArm};
36
37/// The cmdline key — `korp-installer::cmdline::DATA_IO_KEY` and
38/// `nornir-build-thing::installer::DATA_IO_BOOT_PARAM` are the same spelling.
39pub const DATA_IO_KEY: &str = "korp.data.io";
40
41/// Where the arm came from, for the row.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Source {
44    /// `ZNIPPY_GIT_WRITER=` in the environment.
45    Env,
46    /// `korp.data.io=` on the kernel command line.
47    Cmdline,
48    /// Nobody asked: znippy's shipping default.
49    Default,
50}
51
52impl Source {
53    pub fn as_str(self) -> &'static str {
54        match self {
55            Source::Env => "env",
56            Source::Cmdline => "cmdline",
57            Source::Default => "default",
58        }
59    }
60}
61
62/// The decision: the arm that was built, where the wish came from, and — when
63/// the wish was `uring` and the kernel said no — the refusal, named.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Selection {
66    pub arm: WriterArm,
67    pub source: Source,
68    /// `Some("io_uring unavailable: ENOSYS")` when `uring` was asked for and
69    /// `safe` was built instead.
70    pub fallback: Option<String>,
71}
72
73impl Selection {
74    /// The row text: `uring`, `safe`, or `safe (io_uring unavailable: ENOSYS)`.
75    pub fn describe(&self) -> String {
76        match &self.fallback {
77            Some(why) => format!("{} ({why})", self.arm.as_str()),
78            None => self.arm.as_str().to_owned(),
79        }
80    }
81}
82
83/// The LAST `korp.data.io=` token on `cmdline` (an operator-appended token
84/// overrides a baked one, the `korp.env.*` rule), `None` when absent.
85pub fn data_io_token(cmdline: &str) -> Option<&str> {
86    cmdline
87        .split_whitespace()
88        .filter_map(|tok| {
89            tok.strip_prefix(DATA_IO_KEY)
90                .and_then(|rest| rest.strip_prefix('='))
91        })
92        .map(|v| v.trim_matches('"'))
93        .rfind(|v| !v.is_empty())
94}
95
96/// The errno's NAME for the row — `ENOSYS` says "this kernel has no ring" in
97/// one word where `Function not implemented (os error 38)` does not.
98fn errno_name(e: &std::io::Error) -> String {
99    match e.raw_os_error() {
100        Some(1) => "EPERM".into(),
101        Some(12) => "ENOMEM".into(),
102        Some(13) => "EACCES".into(),
103        Some(22) => "EINVAL".into(),
104        Some(23) => "ENFILE".into(),
105        Some(24) => "EMFILE".into(),
106        Some(38) => "ENOSYS".into(),
107        Some(n) => format!("errno {n}"),
108        None => e.to_string(),
109    }
110}
111
112/// **The pure decision.** `env` is `ZNIPPY_GIT_WRITER`'s value as znippy
113/// already parsed it into `cfg.writer` (`env_set` says whether it was set at
114/// all — an unset variable is not a wish for `safe`); `cmdline` is the text of
115/// `/proc/cmdline`; `probe` is asked at most once, and only for `uring`.
116pub fn resolve(
117    cfg: StoreConfig,
118    env_set: bool,
119    cmdline: &str,
120    probe: impl FnOnce() -> std::io::Result<()>,
121) -> (StoreConfig, Selection) {
122    let (wanted, source) = if env_set {
123        (cfg.writer, Source::Env)
124    } else {
125        match data_io_token(cmdline).and_then(|v| WriterArm::parse(v).ok()) {
126            Some(arm) => (arm, Source::Cmdline),
127            None => (cfg.writer, Source::Default),
128        }
129    };
130    let (arm, fallback) = match wanted {
131        WriterArm::Uring => match probe() {
132            Ok(()) => (WriterArm::Uring, None),
133            Err(e) => (
134                WriterArm::Safe,
135                Some(format!("io_uring unavailable: {}", errno_name(&e))),
136            ),
137        },
138        other => (other, None),
139    };
140    (
141        cfg.with_writer(arm),
142        Selection {
143            arm,
144            source,
145            fallback,
146        },
147    )
148}
149
150static SELECTED: OnceLock<Selection> = OnceLock::new();
151
152/// **The one call the engine makes**, with the real environment, the real
153/// `/proc/cmdline` and the real probe. Logs the row once, remembers the
154/// answer for [`selected`]. Idempotent: a second call returns the first
155/// decision without probing again.
156pub fn select(cfg: StoreConfig) -> StoreConfig {
157    let env_set =
158        std::env::var_os(znippy_plugin_git::arms::ENV_WRITER).is_some_and(|v| !v.is_empty());
159    let cmdline = std::fs::read_to_string("/proc/cmdline").unwrap_or_default();
160    let (cfg, sel) = resolve(
161        cfg,
162        env_set,
163        &cmdline,
164        znippy_plugin_git::uring_write::probe,
165    );
166    let sel = SELECTED.get_or_init(|| sel).clone();
167    tracing::info!(
168        target: "gunnar::store",
169        event = "store.writer_selected",
170        writer = sel.arm.as_str(),
171        source = sel.source.as_str(),
172        fallback = sel.fallback.as_deref().unwrap_or("-"),
173        "the writer arm every store in this process appends through"
174    );
175    cfg.with_writer(sel.arm)
176}
177
178/// The arm that was built, once [`select`] ran; `None` before the first store.
179pub fn selected() -> Option<&'static Selection> {
180    SELECTED.get()
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    fn nosys() -> std::io::Result<()> {
188        Err(std::io::Error::from_raw_os_error(38))
189    }
190    fn yes() -> std::io::Result<()> {
191        Ok(())
192    }
193    fn never() -> std::io::Result<()> {
194        panic!("the probe is asked only for uring")
195    }
196
197    #[test]
198    fn the_token_is_the_last_one_and_absent_is_none() {
199        assert_eq!(
200            data_io_token("console=tty0 korp.data.fs=xfs korp.data.io=uring korp.entry=gunnar"),
201            Some("uring")
202        );
203        assert_eq!(
204            data_io_token("korp.data.io=uring korp.data.io=safe"),
205            Some("safe"),
206            "an appended token overrides a baked one"
207        );
208        assert_eq!(data_io_token("korp.data.io=\"uring\""), Some("uring"));
209        assert_eq!(data_io_token("korp.data.iox=uring korp.data.io="), None);
210        assert_eq!(data_io_token(""), None);
211    }
212
213    /// The four doors: env wins over the token; the token picks; nothing asked
214    /// = the shipping default and the probe is never bothered; `uring` refused
215    /// = `safe` BY NAME with the errno on the row.
216    #[test]
217    fn env_then_cmdline_then_default_and_a_refused_ring_falls_back_by_name() {
218        let d = StoreConfig::DEFAULT;
219        let (cfg, s) = resolve(d, false, "korp.entry=gunnar", never);
220        assert_eq!(
221            (cfg.writer, s.source, s.fallback.clone()),
222            (WriterArm::Safe, Source::Default, None)
223        );
224        assert_eq!(s.describe(), "safe");
225
226        let (cfg, s) = resolve(d, false, "korp.data.io=uring", yes);
227        assert_eq!(
228            (cfg.writer, s.source, s.fallback.clone()),
229            (WriterArm::Uring, Source::Cmdline, None)
230        );
231        assert_eq!(s.describe(), "uring");
232        assert_eq!(cfg.index, d.index, "only the writer moves");
233
234        let (cfg, s) = resolve(d, false, "korp.data.io=uring", nosys);
235        assert_eq!((cfg.writer, s.source), (WriterArm::Safe, Source::Cmdline));
236        assert_eq!(s.describe(), "safe (io_uring unavailable: ENOSYS)");
237
238        let (cfg, s) = resolve(d, false, "korp.data.io=uring", || {
239            Err(std::io::Error::from_raw_os_error(1))
240        });
241        assert_eq!(
242            (cfg.writer, s.describe()),
243            (
244                WriterArm::Safe,
245                "safe (io_uring unavailable: EPERM)".to_owned()
246            )
247        );
248
249        // env says safe on a medium that says uring: the operator wins, no probe.
250        let (cfg, s) = resolve(
251            d.with_writer(WriterArm::Safe),
252            true,
253            "korp.data.io=uring",
254            never,
255        );
256        assert_eq!((cfg.writer, s.source), (WriterArm::Safe, Source::Env));
257        // env says uring: probed like any other wish.
258        let (cfg, s) = resolve(d.with_writer(WriterArm::Uring), true, "", nosys);
259        assert_eq!((cfg.writer, s.source), (WriterArm::Safe, Source::Env));
260        assert!(s.fallback.is_some());
261        // a token that is not an arm is ignored, not a crash.
262        let (cfg, s) = resolve(d, false, "korp.data.io=aio", never);
263        assert_eq!((cfg.writer, s.source), (WriterArm::Safe, Source::Default));
264    }
265
266    /// The real `select` is decided ONCE per process — whatever test or store
267    /// asked first — and a later call with another wish neither re-probes nor
268    /// changes its mind; and a built `uring` implies this kernel's probe says yes.
269    #[test]
270    fn select_is_decided_once_and_never_builds_uring_the_kernel_refuses() {
271        let cfg = select(StoreConfig::DEFAULT.with_writer(WriterArm::Uring));
272        let first = selected().expect("selected after select").clone();
273        assert_eq!(cfg.writer, first.arm);
274        let again = select(StoreConfig::DEFAULT);
275        assert_eq!(again.writer, first.arm, "remembered, not re-decided");
276        assert_eq!(selected().unwrap(), &first);
277        if first.arm == WriterArm::Uring {
278            znippy_plugin_git::uring_write::probe().expect("uring was built, so the ring exists");
279            assert!(first.fallback.is_none());
280        }
281        if first.fallback.is_some() {
282            assert_eq!(first.arm, WriterArm::Safe, "a fallback is always to safe");
283        }
284    }
285}