worldinterface_daemon/
config.rs1use std::num::NonZeroUsize;
4use std::path::{Path, PathBuf};
5use std::time::Duration;
6
7use serde::{Deserialize, Serialize};
8use worldinterface_host::HostConfig;
9
10use crate::error::DaemonError;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct DaemonConfig {
15 #[serde(default = "default_bind_address")]
17 pub bind_address: String,
18
19 #[serde(default = "default_aq_data_dir")]
21 pub aq_data_dir: PathBuf,
22
23 #[serde(default = "default_context_store_path")]
25 pub context_store_path: PathBuf,
26
27 #[serde(default = "default_tick_interval_ms")]
29 pub tick_interval_ms: u64,
30
31 #[serde(default = "default_dispatch_concurrency")]
33 pub dispatch_concurrency: usize,
34
35 #[serde(default = "default_lease_timeout_secs")]
37 pub lease_timeout_secs: u64,
38
39 #[serde(default = "default_shutdown_timeout_secs")]
41 pub shutdown_timeout_secs: u64,
42}
43
44fn default_bind_address() -> String {
45 "127.0.0.1:7800".to_string()
46}
47fn default_aq_data_dir() -> PathBuf {
48 PathBuf::from("data/aq")
49}
50fn default_context_store_path() -> PathBuf {
51 PathBuf::from("data/context.db")
52}
53fn default_tick_interval_ms() -> u64 {
54 50
55}
56fn default_dispatch_concurrency() -> usize {
57 4
58}
59fn default_lease_timeout_secs() -> u64 {
60 300
61}
62fn default_shutdown_timeout_secs() -> u64 {
63 30
64}
65
66impl Default for DaemonConfig {
67 fn default() -> Self {
68 Self {
69 bind_address: default_bind_address(),
70 aq_data_dir: default_aq_data_dir(),
71 context_store_path: default_context_store_path(),
72 tick_interval_ms: default_tick_interval_ms(),
73 dispatch_concurrency: default_dispatch_concurrency(),
74 lease_timeout_secs: default_lease_timeout_secs(),
75 shutdown_timeout_secs: default_shutdown_timeout_secs(),
76 }
77 }
78}
79
80impl DaemonConfig {
81 pub fn load(config_path: Option<&Path>) -> Result<Self, DaemonError> {
83 let mut config = match config_path {
84 Some(path) => {
85 let contents = std::fs::read_to_string(path)
86 .map_err(|e| DaemonError::Config(format!("reading {}: {e}", path.display())))?;
87 toml::from_str(&contents)?
88 }
89 None => Self::default(),
90 };
91
92 if let Ok(val) = std::env::var("WI_BIND_ADDRESS") {
94 config.bind_address = val;
95 }
96 if let Ok(val) = std::env::var("WI_AQ_DATA_DIR") {
97 config.aq_data_dir = PathBuf::from(val);
98 }
99 if let Ok(val) = std::env::var("WI_CONTEXT_STORE_PATH") {
100 config.context_store_path = PathBuf::from(val);
101 }
102 if let Ok(val) = std::env::var("WI_TICK_INTERVAL_MS") {
103 config.tick_interval_ms = val
104 .parse()
105 .map_err(|e| DaemonError::Config(format!("WI_TICK_INTERVAL_MS: {e}")))?;
106 }
107 if let Ok(val) = std::env::var("WI_DISPATCH_CONCURRENCY") {
108 config.dispatch_concurrency = val
109 .parse()
110 .map_err(|e| DaemonError::Config(format!("WI_DISPATCH_CONCURRENCY: {e}")))?;
111 }
112 if let Ok(val) = std::env::var("WI_LEASE_TIMEOUT_SECS") {
113 config.lease_timeout_secs = val
114 .parse()
115 .map_err(|e| DaemonError::Config(format!("WI_LEASE_TIMEOUT_SECS: {e}")))?;
116 }
117 if let Ok(val) = std::env::var("WI_SHUTDOWN_TIMEOUT_SECS") {
118 config.shutdown_timeout_secs = val
119 .parse()
120 .map_err(|e| DaemonError::Config(format!("WI_SHUTDOWN_TIMEOUT_SECS: {e}")))?;
121 }
122
123 Ok(config)
124 }
125
126 pub fn to_host_config(&self) -> HostConfig {
128 HostConfig {
129 aq_data_dir: self.aq_data_dir.clone(),
130 context_store_path: self.context_store_path.clone(),
131 tick_interval: Duration::from_millis(self.tick_interval_ms),
132 dispatch_concurrency: NonZeroUsize::new(self.dispatch_concurrency)
133 .unwrap_or(NonZeroUsize::new(4).unwrap()),
134 lease_timeout_secs: self.lease_timeout_secs,
135 shutdown_timeout: Duration::from_secs(self.shutdown_timeout_secs),
136 ..HostConfig::default()
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use std::sync::Mutex;
144
145 use super::*;
146
147 static ENV_LOCK: Mutex<()> = Mutex::new(());
149
150 #[test]
151 fn default_config_is_valid() {
152 let config = DaemonConfig::default();
153 assert_eq!(config.bind_address, "127.0.0.1:7800");
154 assert_eq!(config.tick_interval_ms, 50);
155 assert_eq!(config.dispatch_concurrency, 4);
156 }
157
158 #[test]
159 fn to_host_config_preserves_values() {
160 let config = DaemonConfig {
161 tick_interval_ms: 25,
162 dispatch_concurrency: 8,
163 lease_timeout_secs: 600,
164 ..Default::default()
165 };
166 let host_config = config.to_host_config();
167 assert_eq!(host_config.tick_interval, Duration::from_millis(25));
168 assert_eq!(host_config.dispatch_concurrency.get(), 8);
169 assert_eq!(host_config.lease_timeout_secs, 600);
170 }
171
172 #[test]
173 fn load_without_config_file() {
174 let _lock = ENV_LOCK.lock().unwrap();
175 let config = DaemonConfig::load(None).unwrap();
176 assert_eq!(config.bind_address, "127.0.0.1:7800");
177 }
178
179 #[test]
180 fn load_with_env_overrides() {
181 let _lock = ENV_LOCK.lock().unwrap();
182
183 let key = "WI_BIND_ADDRESS";
184 let original = std::env::var(key).ok();
185
186 std::env::set_var(key, "0.0.0.0:9999");
187 let config = DaemonConfig::load(None).unwrap();
188 assert_eq!(config.bind_address, "0.0.0.0:9999");
189
190 match original {
191 Some(val) => std::env::set_var(key, val),
192 None => std::env::remove_var(key),
193 }
194 }
195
196 #[test]
197 fn load_env_overrides_toml() {
198 let _lock = ENV_LOCK.lock().unwrap();
199
200 let dir = tempfile::tempdir().unwrap();
201 let path = dir.path().join("test.toml");
202 std::fs::write(
203 &path,
204 r#"
205bind_address = "127.0.0.1:1111"
206tick_interval_ms = 10
207"#,
208 )
209 .unwrap();
210
211 let key = "WI_TICK_INTERVAL_MS";
212 let original = std::env::var(key).ok();
213
214 std::env::set_var(key, "99");
215 let config = DaemonConfig::load(Some(&path)).unwrap();
216 assert_eq!(config.tick_interval_ms, 99);
218 assert_eq!(config.bind_address, "127.0.0.1:1111");
220
221 match original {
222 Some(val) => std::env::set_var(key, val),
223 None => std::env::remove_var(key),
224 }
225 }
226
227 #[test]
228 fn load_from_toml_file() {
229 let _lock = ENV_LOCK.lock().unwrap();
230
231 let dir = tempfile::tempdir().unwrap();
232 let path = dir.path().join("test.toml");
233 std::fs::write(
234 &path,
235 r#"
236bind_address = "0.0.0.0:9000"
237tick_interval_ms = 10
238"#,
239 )
240 .unwrap();
241 let config = DaemonConfig::load(Some(&path)).unwrap();
242 assert_eq!(config.bind_address, "0.0.0.0:9000");
243 assert_eq!(config.tick_interval_ms, 10);
244 assert_eq!(config.dispatch_concurrency, 4);
246 }
247}