1use std::env;
2
3#[cfg(feature = "config-env")]
4use serde::{Deserialize, Serialize};
5
6use daedalus_runtime::{BackpressureStrategy, EdgePolicyKind, MetricsLevel};
7
8#[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#[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#[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#[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 #[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#[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 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 #[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}