crates/gunnar-server/src/storage/writer.rs
//! **Which writer arm this process appends through, decided once, said once.**
//!
//! `znippy-plugin-git` has two durable arms with ONE durability contract (the
//! blob's bytes are durable before the journal row that references them):
//! `safe` = pwrite → fsync → write → fsync as four blocking syscalls, `uring` =
//! the same four as ONE `io_uring_enter` with `IOSQE_IO_LINK`. znippy ships
//! `safe` and never falls back on its own — `UringWriter::create` refuses on a
//! kernel without a ring, by name, because a pwrite number reported as an
//! io_uring number would be a lie. **A server is the one process that has to
//! choose before it opens its first store**, and on the appliance the kernel is
//! whatever the medium's donor built (tunnr shipped `# CONFIG_IO_URING is not
//! set` until `[build.installer.data] io = "uring"` asked for it). So:
//!
//! 1. the operator's `ZNIPPY_GIT_WRITER=` wins, spelled exactly (a dev box, a
//! bench column);
//! 2. else the medium's `korp.data.io=uring|safe` token off `/proc/cmdline`
//! (baked by nornir-build-thing from the recipe's `data.io`, carried onto
//! the installed disk as a site token — the same path `korp.data.fs` takes);
//! 3. else znippy's shipping default (`safe`).
//!
//! Then, and only when the answer is `uring`, ONE `io_uring_setup` probe
//! ([`znippy_plugin_git::uring_write::probe`]): a refusal (`ENOSYS` = no
//! ring in this kernel, `EPERM` = `kernel.io_uring_disabled`/seccomp, …) picks
//! `safe` **by name** — the row says `writer safe (io_uring unavailable:
//! ENOSYS)`, never a silent downgrade. The dashboard's disk cell carries the
//! arm that was built (`… xfs uring`), so a screen photo says which column a
//! number belongs to.
//!
//! [`resolve`] is pure (env value, cmdline text, a probe closure) so the four
//! doors are asserted without a kernel; [`select`] is the one call
//! `znippy_engine::arms` makes, and [`selected`] is what the dashboard reads.
use std::sync::OnceLock;
use znippy_plugin_git::arms::{StoreConfig, WriterArm};
/// The cmdline key — `korp-installer::cmdline::DATA_IO_KEY` and
/// `nornir-build-thing::installer::DATA_IO_BOOT_PARAM` are the same spelling.
pub const DATA_IO_KEY: &str = "korp.data.io";
/// Where the arm came from, for the row.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Source {
/// `ZNIPPY_GIT_WRITER=` in the environment.
Env,
/// `korp.data.io=` on the kernel command line.
Cmdline,
/// Nobody asked: znippy's shipping default.
Default,
}
impl Source {
pub fn as_str(self) -> &'static str {
match self {
Source::Env => "env",
Source::Cmdline => "cmdline",
Source::Default => "default",
}
}
}
/// The decision: the arm that was built, where the wish came from, and — when
/// the wish was `uring` and the kernel said no — the refusal, named.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Selection {
pub arm: WriterArm,
pub source: Source,
/// `Some("io_uring unavailable: ENOSYS")` when `uring` was asked for and
/// `safe` was built instead.
pub fallback: Option<String>,
}
impl Selection {
/// The row text: `uring`, `safe`, or `safe (io_uring unavailable: ENOSYS)`.
pub fn describe(&self) -> String {
match &self.fallback {
Some(why) => format!("{} ({why})", self.arm.as_str()),
None => self.arm.as_str().to_owned(),
}
}
}
/// The LAST `korp.data.io=` token on `cmdline` (an operator-appended token
/// overrides a baked one, the `korp.env.*` rule), `None` when absent.
pub fn data_io_token(cmdline: &str) -> Option<&str> {
cmdline
.split_whitespace()
.filter_map(|tok| {
tok.strip_prefix(DATA_IO_KEY)
.and_then(|rest| rest.strip_prefix('='))
})
.map(|v| v.trim_matches('"'))
.rfind(|v| !v.is_empty())
}
/// The errno's NAME for the row — `ENOSYS` says "this kernel has no ring" in
/// one word where `Function not implemented (os error 38)` does not.
fn errno_name(e: &std::io::Error) -> String {
match e.raw_os_error() {
Some(1) => "EPERM".into(),
Some(12) => "ENOMEM".into(),
Some(13) => "EACCES".into(),
Some(22) => "EINVAL".into(),
Some(23) => "ENFILE".into(),
Some(24) => "EMFILE".into(),
Some(38) => "ENOSYS".into(),
Some(n) => format!("errno {n}"),
None => e.to_string(),
}
}
/// **The pure decision.** `env` is `ZNIPPY_GIT_WRITER`'s value as znippy
/// already parsed it into `cfg.writer` (`env_set` says whether it was set at
/// all — an unset variable is not a wish for `safe`); `cmdline` is the text of
/// `/proc/cmdline`; `probe` is asked at most once, and only for `uring`.
pub fn resolve(
cfg: StoreConfig,
env_set: bool,
cmdline: &str,
probe: impl FnOnce() -> std::io::Result<()>,
) -> (StoreConfig, Selection) {
let (wanted, source) = if env_set {
(cfg.writer, Source::Env)
} else {
match data_io_token(cmdline).and_then(|v| WriterArm::parse(v).ok()) {
Some(arm) => (arm, Source::Cmdline),
None => (cfg.writer, Source::Default),
}
};
let (arm, fallback) = match wanted {
WriterArm::Uring => match probe() {
Ok(()) => (WriterArm::Uring, None),
Err(e) => (
WriterArm::Safe,
Some(format!("io_uring unavailable: {}", errno_name(&e))),
),
},
other => (other, None),
};
(
cfg.with_writer(arm),
Selection {
arm,
source,
fallback,
},
)
}
static SELECTED: OnceLock<Selection> = OnceLock::new();
/// **The one call the engine makes**, with the real environment, the real
/// `/proc/cmdline` and the real probe. Logs the row once, remembers the
/// answer for [`selected`]. Idempotent: a second call returns the first
/// decision without probing again.
pub fn select(cfg: StoreConfig) -> StoreConfig {
let env_set =
std::env::var_os(znippy_plugin_git::arms::ENV_WRITER).is_some_and(|v| !v.is_empty());
let cmdline = std::fs::read_to_string("/proc/cmdline").unwrap_or_default();
let (cfg, sel) = resolve(
cfg,
env_set,
&cmdline,
znippy_plugin_git::uring_write::probe,
);
let sel = SELECTED.get_or_init(|| sel).clone();
tracing::info!(
target: "gunnar::store",
event = "store.writer_selected",
writer = sel.arm.as_str(),
source = sel.source.as_str(),
fallback = sel.fallback.as_deref().unwrap_or("-"),
"the writer arm every store in this process appends through"
);
cfg.with_writer(sel.arm)
}
/// The arm that was built, once [`select`] ran; `None` before the first store.
pub fn selected() -> Option<&'static Selection> {
SELECTED.get()
}
#[cfg(test)]
mod tests {
use super::*;
fn nosys() -> std::io::Result<()> {
Err(std::io::Error::from_raw_os_error(38))
}
fn yes() -> std::io::Result<()> {
Ok(())
}
fn never() -> std::io::Result<()> {
panic!("the probe is asked only for uring")
}
#[test]
fn the_token_is_the_last_one_and_absent_is_none() {
assert_eq!(
data_io_token("console=tty0 korp.data.fs=xfs korp.data.io=uring korp.entry=gunnar"),
Some("uring")
);
assert_eq!(
data_io_token("korp.data.io=uring korp.data.io=safe"),
Some("safe"),
"an appended token overrides a baked one"
);
assert_eq!(data_io_token("korp.data.io=\"uring\""), Some("uring"));
assert_eq!(data_io_token("korp.data.iox=uring korp.data.io="), None);
assert_eq!(data_io_token(""), None);
}
/// The four doors: env wins over the token; the token picks; nothing asked
/// = the shipping default and the probe is never bothered; `uring` refused
/// = `safe` BY NAME with the errno on the row.
#[test]
fn env_then_cmdline_then_default_and_a_refused_ring_falls_back_by_name() {
let d = StoreConfig::DEFAULT;
let (cfg, s) = resolve(d, false, "korp.entry=gunnar", never);
assert_eq!(
(cfg.writer, s.source, s.fallback.clone()),
(WriterArm::Safe, Source::Default, None)
);
assert_eq!(s.describe(), "safe");
let (cfg, s) = resolve(d, false, "korp.data.io=uring", yes);
assert_eq!(
(cfg.writer, s.source, s.fallback.clone()),
(WriterArm::Uring, Source::Cmdline, None)
);
assert_eq!(s.describe(), "uring");
assert_eq!(cfg.index, d.index, "only the writer moves");
let (cfg, s) = resolve(d, false, "korp.data.io=uring", nosys);
assert_eq!((cfg.writer, s.source), (WriterArm::Safe, Source::Cmdline));
assert_eq!(s.describe(), "safe (io_uring unavailable: ENOSYS)");
let (cfg, s) = resolve(d, false, "korp.data.io=uring", || {
Err(std::io::Error::from_raw_os_error(1))
});
assert_eq!(
(cfg.writer, s.describe()),
(
WriterArm::Safe,
"safe (io_uring unavailable: EPERM)".to_owned()
)
);
// env says safe on a medium that says uring: the operator wins, no probe.
let (cfg, s) = resolve(
d.with_writer(WriterArm::Safe),
true,
"korp.data.io=uring",
never,
);
assert_eq!((cfg.writer, s.source), (WriterArm::Safe, Source::Env));
// env says uring: probed like any other wish.
let (cfg, s) = resolve(d.with_writer(WriterArm::Uring), true, "", nosys);
assert_eq!((cfg.writer, s.source), (WriterArm::Safe, Source::Env));
assert!(s.fallback.is_some());
// a token that is not an arm is ignored, not a crash.
let (cfg, s) = resolve(d, false, "korp.data.io=aio", never);
assert_eq!((cfg.writer, s.source), (WriterArm::Safe, Source::Default));
}
/// The real `select` is decided ONCE per process — whatever test or store
/// asked first — and a later call with another wish neither re-probes nor
/// changes its mind; and a built `uring` implies this kernel's probe says yes.
#[test]
fn select_is_decided_once_and_never_builds_uring_the_kernel_refuses() {
let cfg = select(StoreConfig::DEFAULT.with_writer(WriterArm::Uring));
let first = selected().expect("selected after select").clone();
assert_eq!(cfg.writer, first.arm);
let again = select(StoreConfig::DEFAULT);
assert_eq!(again.writer, first.arm, "remembered, not re-decided");
assert_eq!(selected().unwrap(), &first);
if first.arm == WriterArm::Uring {
znippy_plugin_git::uring_write::probe().expect("uring was built, so the ring exists");
assert!(first.fallback.is_none());
}
if first.fallback.is_some() {
assert_eq!(first.arm, WriterArm::Safe, "a fallback is always to safe");
}
}
}