use std::sync::OnceLock;
use znippy_plugin_git::arms::{StoreConfig, WriterArm};
pub const DATA_IO_KEY: &str = "korp.data.io";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Source {
Env,
Cmdline,
Default,
}
impl Source {
pub fn as_str(self) -> &'static str {
match self {
Source::Env => "env",
Source::Cmdline => "cmdline",
Source::Default => "default",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Selection {
pub arm: WriterArm,
pub source: Source,
pub fallback: Option<String>,
}
impl Selection {
pub fn describe(&self) -> String {
match &self.fallback {
Some(why) => format!("{} ({why})", self.arm.as_str()),
None => self.arm.as_str().to_owned(),
}
}
}
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())
}
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(),
}
}
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();
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)
}
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);
}
#[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()
)
);
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));
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());
let (cfg, s) = resolve(d, false, "korp.data.io=aio", never);
assert_eq!((cfg.writer, s.source), (WriterArm::Safe, Source::Default));
}
#[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");
}
}
}