daedalus_engine/
config.rs

1use std::env;
2
3#[cfg(feature = "config-env")]
4use serde::{Deserialize, Serialize};
5
6use daedalus_runtime::{BackpressureStrategy, EdgePolicyKind, MetricsLevel};
7
8/// GPU backend selection; device requires the `gpu` feature.
9///
10/// ```
11/// use daedalus_engine::GpuBackend;
12/// let backend = GpuBackend::Cpu;
13/// assert_eq!(backend, GpuBackend::Cpu);
14/// ```
15#[derive(Clone, Debug, PartialEq, Eq, Default)]
16#[cfg_attr(feature = "config-env", derive(Serialize, Deserialize))]
17#[cfg_attr(feature = "config-env", serde(rename_all = "snake_case"))]
18pub enum GpuBackend {
19    #[default]
20    Cpu,
21    Mock,
22    Device,
23}
24
25/// Runtime execution mode.
26///
27/// ```
28/// use daedalus_engine::RuntimeMode;
29/// let mode = RuntimeMode::Parallel;
30/// assert_eq!(mode, RuntimeMode::Parallel);
31/// ```
32#[derive(Clone, Debug, PartialEq, Eq, Default)]
33#[cfg_attr(feature = "config-env", derive(Serialize, Deserialize))]
34#[cfg_attr(feature = "config-env", serde(rename_all = "snake_case"))]
35pub enum RuntimeMode {
36    #[default]
37    Serial,
38    Parallel,
39}
40
41/// Planner knobs.
42///
43/// ```ignore
44/// use daedalus_engine::config::PlannerSection;
45/// let planner = PlannerSection::default();
46/// assert!(!planner.enable_gpu);
47/// ```
48#[derive(Clone, Debug, PartialEq, Eq)]
49#[cfg_attr(feature = "config-env", derive(Serialize, Deserialize))]
50pub struct PlannerSection {
51    #[cfg_attr(feature = "config-env", serde(default))]
52    pub enable_gpu: bool,
53    #[cfg_attr(feature = "config-env", serde(default))]
54    pub enable_lints: bool,
55    #[cfg_attr(feature = "config-env", serde(default))]
56    pub active_features: Vec<String>,
57}
58
59impl Default for PlannerSection {
60    fn default() -> Self {
61        Self {
62            enable_gpu: false,
63            enable_lints: true,
64            active_features: Vec::new(),
65        }
66    }
67}
68
69/// Runtime scheduler/backpressure options.
70///
71/// ```ignore
72/// use daedalus_engine::config::RuntimeSection;
73/// let runtime = RuntimeSection::default();
74/// assert_eq!(runtime.pool_size, None);
75/// ```
76#[derive(Clone, Debug, PartialEq, Eq)]
77#[cfg_attr(feature = "config-env", derive(Serialize, Deserialize))]
78pub struct RuntimeSection {
79    #[cfg_attr(feature = "config-env", serde(default))]
80    pub default_policy: EdgePolicyKind,
81    #[cfg_attr(feature = "config-env", serde(default))]
82    pub backpressure: BackpressureStrategy,
83    #[cfg_attr(feature = "config-env", serde(default))]
84    pub mode: RuntimeMode,
85    #[cfg_attr(feature = "config-env", serde(default))]
86    pub metrics_level: MetricsLevel,
87    /// Prefer lock-free bounded edge queues when available.
88    ///
89    /// Requires the `lockfree-queues` Cargo feature.
90    #[cfg_attr(feature = "config-env", serde(default))]
91    pub lockfree_queues: bool,
92    #[cfg_attr(feature = "config-env", serde(default))]
93    pub pool_size: Option<usize>,
94}
95
96impl Default for RuntimeSection {
97    fn default() -> Self {
98        Self {
99            default_policy: EdgePolicyKind::Fifo,
100            backpressure: BackpressureStrategy::None,
101            mode: RuntimeMode::Serial,
102            metrics_level: MetricsLevel::default(),
103            lockfree_queues: false,
104            pool_size: None,
105        }
106    }
107}
108
109/// Top-level engine configuration.
110///
111/// ```
112/// use daedalus_engine::EngineConfig;
113/// let cfg = EngineConfig::default();
114/// assert!(cfg.validate().is_ok());
115/// ```
116#[derive(Clone, Debug, PartialEq, Eq)]
117#[cfg_attr(feature = "config-env", derive(Serialize, Deserialize))]
118pub struct EngineConfig {
119    #[cfg_attr(feature = "config-env", serde(default))]
120    pub gpu: GpuBackend,
121    #[cfg_attr(feature = "config-env", serde(default))]
122    pub planner: PlannerSection,
123    #[cfg_attr(feature = "config-env", serde(default))]
124    pub runtime: RuntimeSection,
125}
126
127impl Default for EngineConfig {
128    fn default() -> Self {
129        Self {
130            gpu: GpuBackend::Cpu,
131            planner: PlannerSection::default(),
132            runtime: RuntimeSection::default(),
133        }
134    }
135}
136
137impl EngineConfig {
138    /// Lightweight validation: ensures non-zero pool size when provided.
139    ///
140    /// ```
141    /// use daedalus_engine::EngineConfig;
142    /// let cfg = EngineConfig::default();
143    /// assert!(cfg.validate().is_ok());
144    /// ```
145    pub fn validate(&self) -> Result<(), String> {
146        if let Some(sz) = self.runtime.pool_size
147            && sz == 0
148        {
149            return Err("pool_size must be > 0 when provided".into());
150        }
151        if self.planner.enable_gpu && matches!(self.gpu, GpuBackend::Cpu) {
152            return Err("planner GPU is enabled but gpu backend is set to cpu".into());
153        }
154        Ok(())
155    }
156
157    /// Construct config from environment variables. Only compiled when `config-env` is enabled.
158    ///
159    /// Example (doc-test guarded by the feature flag):
160    /// ```
161    /// # #[cfg(feature = "config-env")] {
162    /// use daedalus_engine::{EngineConfig, GpuBackend};
163    /// unsafe { std::env::set_var("DAEDALUS_GPU", "mock"); }
164    /// let cfg = EngineConfig::from_env().unwrap();
165    /// assert_eq!(cfg.gpu, GpuBackend::Mock);
166    /// unsafe { std::env::remove_var("DAEDALUS_GPU"); }
167    /// # }
168    /// ```
169    ///
170    /// Environment variables:
171    /// - `DAEDALUS_METRICS_LEVEL=off|basic|detailed|profile`
172    #[cfg(feature = "config-env")]
173    pub fn from_env() -> Result<Self, String> {
174        let mut cfg = EngineConfig::default();
175
176        if let Ok(raw) = env::var("DAEDALUS_GPU") {
177            cfg.gpu = match raw.to_ascii_lowercase().as_str() {
178                "cpu" => GpuBackend::Cpu,
179                "mock" | "gpu-mock" => GpuBackend::Mock,
180                "gpu" | "device" => GpuBackend::Device,
181                other => return Err(format!("unknown DAEDALUS_GPU '{}'", other)),
182            };
183        }
184
185        cfg.planner.enable_gpu = read_bool("DAEDALUS_PLANNER_GPU", cfg.planner.enable_gpu)?;
186        cfg.planner.enable_lints = read_bool("DAEDALUS_PLANNER_LINTS", cfg.planner.enable_lints)?;
187        if let Ok(raw) = env::var("DAEDALUS_PLANNER_FEATURES") {
188            cfg.planner.active_features = raw
189                .split(',')
190                .filter(|s| !s.trim().is_empty())
191                .map(|s| s.trim().to_string())
192                .collect();
193        }
194
195        if let Ok(raw) = env::var("DAEDALUS_RUNTIME_POLICY") {
196            cfg.runtime.default_policy = match raw.to_ascii_lowercase().as_str() {
197                "fifo" => EdgePolicyKind::Fifo,
198                "newest" | "newest_wins" => EdgePolicyKind::NewestWins,
199                "broadcast" => EdgePolicyKind::Broadcast,
200                other => {
201                    if let Some(rest) = other.strip_prefix("bounded:") {
202                        let cap: usize = rest.parse().map_err(|_| {
203                            format!("invalid bounded cap in DAEDALUS_RUNTIME_POLICY '{}'", raw)
204                        })?;
205                        EdgePolicyKind::Bounded { cap }
206                    } else {
207                        return Err(format!("unknown DAEDALUS_RUNTIME_POLICY '{}'", other));
208                    }
209                }
210            };
211        }
212
213        if let Ok(raw) = env::var("DAEDALUS_RUNTIME_BACKPRESSURE") {
214            cfg.runtime.backpressure = match raw.to_ascii_lowercase().as_str() {
215                "none" => BackpressureStrategy::None,
216                "bounded" => BackpressureStrategy::BoundedQueues,
217                "error" | "error_on_overflow" => BackpressureStrategy::ErrorOnOverflow,
218                other => return Err(format!("unknown DAEDALUS_RUNTIME_BACKPRESSURE '{}'", other)),
219            };
220        }
221
222        if let Ok(raw) = env::var("DAEDALUS_RUNTIME_MODE") {
223            cfg.runtime.mode = match raw.to_ascii_lowercase().as_str() {
224                "serial" => RuntimeMode::Serial,
225                "parallel" => RuntimeMode::Parallel,
226                other => return Err(format!("unknown DAEDALUS_RUNTIME_MODE '{}'", other)),
227            };
228        }
229        if let Ok(raw) = env::var("DAEDALUS_METRICS_LEVEL") {
230            cfg.runtime.metrics_level = match raw.to_ascii_lowercase().as_str() {
231                "off" => MetricsLevel::Off,
232                "basic" => MetricsLevel::Basic,
233                "detailed" => MetricsLevel::Detailed,
234                "profile" => MetricsLevel::Profile,
235                other => return Err(format!("unknown DAEDALUS_METRICS_LEVEL '{}'", other)),
236            };
237        }
238        if let Ok(raw) = env::var("DAEDALUS_RUNTIME_POOL_SIZE") {
239            cfg.runtime.pool_size = Some(
240                raw.parse()
241                    .map_err(|_| format!("invalid DAEDALUS_RUNTIME_POOL_SIZE '{}'", raw))?,
242            );
243        }
244        cfg.runtime.lockfree_queues =
245            read_bool("DAEDALUS_LOCKFREE_QUEUES", cfg.runtime.lockfree_queues)?;
246
247        cfg.validate()?;
248        Ok(cfg)
249    }
250}
251
252#[cfg(feature = "config-env")]
253fn read_bool(var: &str, default: bool) -> Result<bool, String> {
254    match env::var(var) {
255        Ok(val) => {
256            let v = val.to_ascii_lowercase();
257            match v.as_str() {
258                "1" | "true" | "yes" | "on" => Ok(true),
259                "0" | "false" | "no" | "off" => Ok(false),
260                _ => Err(format!("invalid boolean '{}': {}", var, val)),
261            }
262        }
263        Err(env::VarError::NotPresent) => Ok(default),
264        Err(e) => Err(format!("error reading {}: {}", var, e)),
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn defaults_are_stable() {
274        let cfg = EngineConfig::default();
275        assert_eq!(cfg.gpu, GpuBackend::Cpu);
276        assert_eq!(cfg.runtime.mode, RuntimeMode::Serial);
277    }
278}