Skip to main content

ferrum_types/
runtime_config.rs

1//! Runtime configuration snapshot and small env parsing helpers.
2//!
3//! This is intentionally a narrow data surface first: it makes effective
4//! `FERRUM_*` overrides visible in health and bench artifacts while the
5//! hot-path env reads are migrated to typed config structs.
6
7use serde::{Deserialize, Serialize};
8use std::sync::RwLock;
9use std::{collections::BTreeMap, path::PathBuf};
10
11/// Process-wide runtime snapshot, installed once at the composition root.
12///
13/// This is the single env-bridge seam the test-architecture goal asks for:
14/// the CLI (`serve`/`run`/`bench`) captures `FERRUM_*` via
15/// [`RuntimeConfigSnapshot::capture_current`] and installs it here when it
16/// applies the snapshot to the engine config; model code downstream reads
17/// [`active_runtime_snapshot`] instead of `std::env`, so no model/engine
18/// module freezes its own env config. Re-installable (RwLock, not OnceLock)
19/// so per-construction test paths can vary it after `std::env::set_var`.
20static ACTIVE_SNAPSHOT: RwLock<Option<RuntimeConfigSnapshot>> = RwLock::new(None);
21
22/// Install the process-wide runtime snapshot resolved at the composition root.
23pub fn install_runtime_snapshot(snapshot: RuntimeConfigSnapshot) {
24    *ACTIVE_SNAPSHOT
25        .write()
26        .expect("runtime snapshot lock poisoned") = Some(snapshot);
27}
28
29/// The installed runtime snapshot, or an empty snapshot when none was
30/// installed (unit tests that do not exercise runtime knobs see defaults).
31pub fn active_runtime_snapshot() -> RuntimeConfigSnapshot {
32    ACTIVE_SNAPSHOT
33        .read()
34        .expect("runtime snapshot lock poisoned")
35        .clone()
36        .unwrap_or_default()
37}
38
39/// Stable snapshot of non-default runtime configuration visible to the process.
40#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
41pub struct RuntimeConfigSnapshot {
42    /// Sorted by key for stable JSON and machine-readable diffs.
43    pub entries: Vec<RuntimeConfigEntry>,
44}
45
46impl RuntimeConfigSnapshot {
47    /// Capture all currently set `FERRUM_*` env overrides.
48    pub fn capture_current() -> Self {
49        Self::from_env_vars(std::env::vars())
50    }
51
52    /// Build a snapshot from a supplied environment map or iterator.
53    pub fn from_env_vars<I, K, V>(vars: I) -> Self
54    where
55        I: IntoIterator<Item = (K, V)>,
56        K: Into<String>,
57        V: Into<String>,
58    {
59        let mut sorted = BTreeMap::new();
60        for (key, value) in vars {
61            let key = key.into();
62            if key.starts_with("FERRUM_") {
63                sorted.insert(key, value.into());
64            }
65        }
66
67        Self {
68            entries: sorted
69                .into_iter()
70                .map(|(key, effective_value)| RuntimeConfigEntry {
71                    affects: infer_effects(&key),
72                    key,
73                    effective_value,
74                    source: RuntimeConfigSource::Env,
75                })
76                .collect(),
77        }
78    }
79
80    /// Build a stable snapshot from explicit entries. Later entries for the
81    /// same key replace earlier entries.
82    pub fn from_entries<I>(entries: I) -> Self
83    where
84        I: IntoIterator<Item = RuntimeConfigEntry>,
85    {
86        let mut sorted = BTreeMap::new();
87        for entry in entries {
88            sorted.insert(entry.key.clone(), entry);
89        }
90        Self {
91            entries: sorted.into_values().collect(),
92        }
93    }
94
95    /// Insert or replace one effective value, preserving stable key order.
96    pub fn upsert(
97        &mut self,
98        key: impl Into<String>,
99        effective_value: impl Into<String>,
100        source: RuntimeConfigSource,
101    ) {
102        self.upsert_entry(RuntimeConfigEntry::new(key, effective_value, source));
103    }
104
105    /// Insert or replace one explicit entry, preserving stable key order.
106    pub fn upsert_entry(&mut self, entry: RuntimeConfigEntry) {
107        let mut entries = std::mem::take(&mut self.entries);
108        entries.retain(|existing| existing.key != entry.key);
109        entries.push(entry);
110        *self = Self::from_entries(entries);
111    }
112
113    /// Return a snapshot with one additional effective value.
114    pub fn with_entry(
115        mut self,
116        key: impl Into<String>,
117        effective_value: impl Into<String>,
118        source: RuntimeConfigSource,
119    ) -> Self {
120        self.upsert(key, effective_value, source);
121        self
122    }
123}
124
125/// One effective config value in a runtime snapshot.
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct RuntimeConfigEntry {
128    pub key: String,
129    pub effective_value: String,
130    pub source: RuntimeConfigSource,
131    pub affects: Vec<RuntimeConfigEffect>,
132}
133
134impl RuntimeConfigEntry {
135    pub fn new(
136        key: impl Into<String>,
137        effective_value: impl Into<String>,
138        source: RuntimeConfigSource,
139    ) -> Self {
140        let key = key.into();
141        Self {
142            affects: infer_effects(&key),
143            key,
144            effective_value: effective_value.into(),
145            source,
146        }
147    }
148}
149
150/// Source of an effective config value.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum RuntimeConfigSource {
154    Default,
155    ConfigFile,
156    Cli,
157    Env,
158    ScriptCase,
159    MemoryProfile,
160}
161
162/// Impact classes used by config snapshots and artifact diffs.
163#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
164#[serde(rename_all = "snake_case")]
165pub enum RuntimeConfigEffect {
166    Correctness,
167    Performance,
168    Memory,
169    Diagnostics,
170}
171
172/// Tri-state env override used by paths that distinguish unset from forced off.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(rename_all = "snake_case")]
175pub enum EnvTriState {
176    Default,
177    ForcedOff,
178    ForcedOn,
179}
180
181pub fn parse_bool_env_value(raw: &str) -> Result<bool, String> {
182    match raw.trim().to_ascii_lowercase().as_str() {
183        "1" | "true" | "yes" | "on" => Ok(true),
184        "0" | "false" | "no" | "off" => Ok(false),
185        other => Err(format!("invalid boolean env value: {other:?}")),
186    }
187}
188
189pub fn parse_usize_env_value(raw: &str) -> Result<usize, String> {
190    raw.trim()
191        .parse::<usize>()
192        .map_err(|_| format!("invalid integer env value: {raw:?}"))
193}
194
195pub fn parse_path_env_value(raw: &str) -> Result<PathBuf, String> {
196    let trimmed = raw.trim();
197    if trimmed.is_empty() {
198        return Err("path env value must not be empty".to_string());
199    }
200    Ok(PathBuf::from(trimmed))
201}
202
203pub fn parse_tri_state_env_value(raw: Option<&str>) -> Result<EnvTriState, String> {
204    let Some(raw) = raw else {
205        return Ok(EnvTriState::Default);
206    };
207    if raw.trim().is_empty() {
208        return Ok(EnvTriState::Default);
209    }
210    Ok(if parse_bool_env_value(raw)? {
211        EnvTriState::ForcedOn
212    } else {
213        EnvTriState::ForcedOff
214    })
215}
216
217fn infer_effects(key: &str) -> Vec<RuntimeConfigEffect> {
218    let mut effects = Vec::new();
219
220    if key.contains("DIAG")
221        || key.contains("PROF")
222        || key.contains("TRACE")
223        || key.contains("DUMP")
224        || key.contains("LOG_CONFIG")
225        || key.contains("CAPTURE")
226        || key.contains("DEBUG")
227    {
228        effects.push(RuntimeConfigEffect::Diagnostics);
229    }
230
231    if key.contains("KV")
232        || key.contains("BATCHED_TOKENS")
233        || key.contains("PAGED_MAX_SEQS")
234        || key.contains("MODEL_LEN")
235        || key.contains("MEMORY")
236    {
237        effects.push(RuntimeConfigEffect::Memory);
238    }
239
240    if key.contains("PREFIX_CACHE")
241        || key.contains("MODEL_PATH")
242        || key.contains("MODEL_LEN")
243        || key.contains("SPEC_")
244        || key.contains("REF_")
245        || key.contains("DTYPE")
246    {
247        effects.push(RuntimeConfigEffect::Correctness);
248    }
249
250    if effects.is_empty()
251        || key.contains("MOE")
252        || key.contains("VLLM")
253        || key.contains("MARLIN")
254        || key.contains("PAGED")
255        || key.contains("GRAPH")
256        || key.contains("SCHED")
257        || key.contains("BATCH")
258        || key.contains("ATTN")
259        || key.contains("FLASH")
260        || key.contains("CUDA")
261        || key.contains("TRITON")
262        || key.contains("GREEDY")
263        || key.contains("FA")
264    {
265        effects.push(RuntimeConfigEffect::Performance);
266    }
267
268    effects.sort();
269    effects.dedup();
270    effects
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn parses_boolean_values() {
279        assert_eq!(parse_bool_env_value("1").unwrap(), true);
280        assert_eq!(parse_bool_env_value("off").unwrap(), false);
281        assert!(parse_bool_env_value("maybe").is_err());
282    }
283
284    #[test]
285    fn parses_integer_values() {
286        assert_eq!(parse_usize_env_value("4096").unwrap(), 4096);
287        assert!(parse_usize_env_value("-1").is_err());
288        assert!(parse_usize_env_value("many").is_err());
289    }
290
291    #[test]
292    fn parses_path_values() {
293        assert_eq!(
294            parse_path_env_value("/tmp/model").unwrap(),
295            PathBuf::from("/tmp/model")
296        );
297        assert!(parse_path_env_value("   ").is_err());
298    }
299
300    #[test]
301    fn parses_tri_state_values() {
302        assert_eq!(
303            parse_tri_state_env_value(None).unwrap(),
304            EnvTriState::Default
305        );
306        assert_eq!(
307            parse_tri_state_env_value(Some("0")).unwrap(),
308            EnvTriState::ForcedOff
309        );
310        assert_eq!(
311            parse_tri_state_env_value(Some("on")).unwrap(),
312            EnvTriState::ForcedOn
313        );
314        assert!(parse_tri_state_env_value(Some("auto")).is_err());
315    }
316
317    #[test]
318    fn snapshot_is_sorted_and_classified() {
319        let snapshot = RuntimeConfigSnapshot::from_env_vars([
320            ("OTHER_ENV", "ignored"),
321            ("FERRUM_PREFIX_CACHE", "1"),
322            ("FERRUM_MOE_GRAPH", "1"),
323        ]);
324        let keys: Vec<_> = snapshot
325            .entries
326            .iter()
327            .map(|entry| entry.key.as_str())
328            .collect();
329        assert_eq!(keys, vec!["FERRUM_MOE_GRAPH", "FERRUM_PREFIX_CACHE"]);
330        assert_eq!(snapshot.entries[0].source, RuntimeConfigSource::Env);
331        assert!(snapshot.entries[0]
332            .affects
333            .contains(&RuntimeConfigEffect::Performance));
334        assert!(snapshot.entries[1]
335            .affects
336            .contains(&RuntimeConfigEffect::Correctness));
337    }
338
339    #[test]
340    fn upsert_preserves_non_env_source_and_stable_order() {
341        let mut snapshot = RuntimeConfigSnapshot::from_env_vars([
342            ("FERRUM_KV_DTYPE", "fp16"),
343            ("FERRUM_MOE_GRAPH", "1"),
344        ]);
345        snapshot.upsert("FERRUM_KV_DTYPE", "int8", RuntimeConfigSource::Cli);
346        snapshot.upsert(
347            "FERRUM_PROFILE_JSONL",
348            "/tmp/profile.jsonl",
349            RuntimeConfigSource::Cli,
350        );
351
352        let keys: Vec<_> = snapshot
353            .entries
354            .iter()
355            .map(|entry| entry.key.as_str())
356            .collect();
357        assert_eq!(
358            keys,
359            [
360                "FERRUM_KV_DTYPE",
361                "FERRUM_MOE_GRAPH",
362                "FERRUM_PROFILE_JSONL"
363            ]
364        );
365        let kv = snapshot
366            .entries
367            .iter()
368            .find(|entry| entry.key == "FERRUM_KV_DTYPE")
369            .unwrap();
370        assert_eq!(kv.effective_value, "int8");
371        assert_eq!(kv.source, RuntimeConfigSource::Cli);
372        assert!(kv.affects.contains(&RuntimeConfigEffect::Correctness));
373
374        let profile = snapshot
375            .entries
376            .iter()
377            .find(|entry| entry.key == "FERRUM_PROFILE_JSONL")
378            .unwrap();
379        assert_eq!(profile.source, RuntimeConfigSource::Cli);
380        assert!(profile.affects.contains(&RuntimeConfigEffect::Diagnostics));
381    }
382}