1use std::sync::OnceLock;
34
35use znippy_plugin_git::arms::{StoreConfig, WriterArm};
36
37pub const DATA_IO_KEY: &str = "korp.data.io";
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Source {
44 Env,
46 Cmdline,
48 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#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Selection {
66 pub arm: WriterArm,
67 pub source: Source,
68 pub fallback: Option<String>,
71}
72
73impl Selection {
74 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
83pub 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
96fn 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
112pub 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
152pub 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
178pub 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 #[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 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 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 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 #[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}