Skip to main content

ringkernel_core/
hot_reload.rs

1//! Configuration Hot Reload — FR-008
2//!
3//! Runtime configuration updates without restart:
4//! - Validate before apply
5//! - Atomic swap (rollback on failure)
6//! - Config versioning (monotonic counter)
7//! - Audit trail (who changed what, when)
8
9use std::collections::HashMap;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::time::Instant;
12
13/// A versioned configuration value.
14#[derive(Debug, Clone)]
15pub struct ConfigEntry {
16    /// The configuration value.
17    pub value: ConfigValue,
18    /// Whether this config can be changed at runtime.
19    pub reloadable: bool,
20    /// Description for documentation.
21    pub description: String,
22}
23
24/// Configuration value types.
25#[derive(Debug, Clone, PartialEq)]
26pub enum ConfigValue {
27    /// String value.
28    String(String),
29    /// Integer value.
30    Int(i64),
31    /// Float value.
32    Float(f64),
33    /// Boolean value.
34    Bool(bool),
35    /// Duration in milliseconds.
36    DurationMs(u64),
37}
38
39impl std::fmt::Display for ConfigValue {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            Self::String(s) => write!(f, "{}", s),
43            Self::Int(i) => write!(f, "{}", i),
44            Self::Float(v) => write!(f, "{}", v),
45            Self::Bool(b) => write!(f, "{}", b),
46            Self::DurationMs(ms) => write!(f, "{}ms", ms),
47        }
48    }
49}
50
51/// Record of a configuration change (audit trail).
52#[derive(Debug, Clone)]
53pub struct ConfigChange {
54    /// Which key was changed.
55    pub key: String,
56    /// Old value (None if new key).
57    pub old_value: Option<ConfigValue>,
58    /// New value.
59    pub new_value: ConfigValue,
60    /// Version after this change.
61    pub version: u64,
62    /// When the change was applied.
63    pub changed_at: Instant,
64    /// Who made the change (e.g., API caller ID).
65    pub changed_by: String,
66}
67
68/// Hot-reloadable configuration store.
69pub struct HotReloadConfig {
70    /// Current configuration entries.
71    entries: HashMap<String, ConfigEntry>,
72    /// Monotonic version counter.
73    version: AtomicU64,
74    /// Audit trail of recent changes.
75    changes: Vec<ConfigChange>,
76    /// Maximum audit trail entries.
77    max_audit_entries: usize,
78}
79
80impl HotReloadConfig {
81    /// Create a new configuration store.
82    pub fn new() -> Self {
83        Self {
84            entries: HashMap::new(),
85            version: AtomicU64::new(0),
86            changes: Vec::new(),
87            max_audit_entries: 1000,
88        }
89    }
90
91    /// Register a configuration key with initial value and reload policy.
92    pub fn register(
93        &mut self,
94        key: impl Into<String>,
95        value: ConfigValue,
96        reloadable: bool,
97        description: impl Into<String>,
98    ) {
99        self.entries.insert(
100            key.into(),
101            ConfigEntry {
102                value,
103                reloadable,
104                description: description.into(),
105            },
106        );
107    }
108
109    /// Get a configuration value.
110    pub fn get(&self, key: &str) -> Option<&ConfigValue> {
111        self.entries.get(key).map(|e| &e.value)
112    }
113
114    /// Get a string value.
115    pub fn get_string(&self, key: &str) -> Option<&str> {
116        match self.get(key)? {
117            ConfigValue::String(s) => Some(s),
118            _ => None,
119        }
120    }
121
122    /// Get an integer value.
123    pub fn get_int(&self, key: &str) -> Option<i64> {
124        match self.get(key)? {
125            ConfigValue::Int(i) => Some(*i),
126            _ => None,
127        }
128    }
129
130    /// Get a boolean value.
131    pub fn get_bool(&self, key: &str) -> Option<bool> {
132        match self.get(key)? {
133            ConfigValue::Bool(b) => Some(*b),
134            _ => None,
135        }
136    }
137
138    /// Update a configuration value at runtime.
139    ///
140    /// Returns Ok(version) on success, Err on validation failure.
141    pub fn update(
142        &mut self,
143        key: &str,
144        new_value: ConfigValue,
145        changed_by: impl Into<String>,
146    ) -> Result<u64, ConfigUpdateError> {
147        let entry = self
148            .entries
149            .get(key)
150            .ok_or_else(|| ConfigUpdateError::KeyNotFound(key.to_string()))?;
151
152        if !entry.reloadable {
153            return Err(ConfigUpdateError::NotReloadable(key.to_string()));
154        }
155
156        // Type check
157        if std::mem::discriminant(&entry.value) != std::mem::discriminant(&new_value) {
158            return Err(ConfigUpdateError::TypeMismatch {
159                key: key.to_string(),
160                expected: format!("{:?}", std::mem::discriminant(&entry.value)),
161                got: format!("{:?}", std::mem::discriminant(&new_value)),
162            });
163        }
164
165        let old_value = entry.value.clone();
166        let version = self.version.fetch_add(1, Ordering::Relaxed) + 1;
167
168        // Record change
169        let change = ConfigChange {
170            key: key.to_string(),
171            old_value: Some(old_value),
172            new_value: new_value.clone(),
173            version,
174            changed_at: Instant::now(),
175            changed_by: changed_by.into(),
176        };
177
178        // Apply the change
179        self.entries
180            .get_mut(key)
181            .expect("config entry must exist after contains_key check")
182            .value = new_value;
183
184        // Audit trail
185        self.changes.push(change);
186        while self.changes.len() > self.max_audit_entries {
187            self.changes.remove(0);
188        }
189
190        Ok(version)
191    }
192
193    /// Current config version.
194    pub fn version(&self) -> u64 {
195        self.version.load(Ordering::Relaxed)
196    }
197
198    /// Get recent changes (audit trail).
199    pub fn recent_changes(&self, limit: usize) -> &[ConfigChange] {
200        let start = self.changes.len().saturating_sub(limit);
201        &self.changes[start..]
202    }
203
204    /// List all configuration keys.
205    pub fn list_keys(&self) -> Vec<(&str, bool)> {
206        self.entries
207            .iter()
208            .map(|(k, v)| (k.as_str(), v.reloadable))
209            .collect()
210    }
211
212    /// Number of configuration entries.
213    pub fn len(&self) -> usize {
214        self.entries.len()
215    }
216
217    /// Check if empty.
218    pub fn is_empty(&self) -> bool {
219        self.entries.is_empty()
220    }
221}
222
223impl Default for HotReloadConfig {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229/// Errors from configuration updates.
230#[derive(Debug, Clone)]
231pub enum ConfigUpdateError {
232    /// Key not found.
233    KeyNotFound(String),
234    /// Key is not reloadable (requires restart).
235    NotReloadable(String),
236    /// Value type doesn't match registered type.
237    TypeMismatch {
238        /// Configuration key with the type mismatch.
239        key: String,
240        /// Expected type name.
241        expected: String,
242        /// Actual type name provided.
243        got: String,
244    },
245    /// Custom validation failed.
246    ValidationFailed(String),
247}
248
249impl std::fmt::Display for ConfigUpdateError {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        match self {
252            Self::KeyNotFound(k) => write!(f, "Config key not found: {}", k),
253            Self::NotReloadable(k) => write!(f, "Config key '{}' requires restart to change", k),
254            Self::TypeMismatch { key, expected, got } => {
255                write!(
256                    f,
257                    "Type mismatch for '{}': expected {}, got {}",
258                    key, expected, got
259                )
260            }
261            Self::ValidationFailed(msg) => write!(f, "Validation failed: {}", msg),
262        }
263    }
264}
265
266impl std::error::Error for ConfigUpdateError {}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_register_and_get() {
274        let mut config = HotReloadConfig::new();
275        config.register(
276            "rate_limit",
277            ConfigValue::Int(1000),
278            true,
279            "Max requests/sec",
280        );
281        config.register("gpu_device", ConfigValue::Int(0), false, "GPU device index");
282
283        assert_eq!(config.get_int("rate_limit"), Some(1000));
284        assert_eq!(config.get_int("gpu_device"), Some(0));
285    }
286
287    #[test]
288    fn test_update_reloadable() {
289        let mut config = HotReloadConfig::new();
290        config.register("rate_limit", ConfigValue::Int(1000), true, "");
291
292        let version = config
293            .update("rate_limit", ConfigValue::Int(2000), "admin")
294            .unwrap();
295        assert_eq!(version, 1);
296        assert_eq!(config.get_int("rate_limit"), Some(2000));
297    }
298
299    #[test]
300    fn test_update_non_reloadable() {
301        let mut config = HotReloadConfig::new();
302        config.register("gpu_device", ConfigValue::Int(0), false, "");
303
304        let result = config.update("gpu_device", ConfigValue::Int(1), "admin");
305        assert!(matches!(result, Err(ConfigUpdateError::NotReloadable(_))));
306    }
307
308    #[test]
309    fn test_type_mismatch() {
310        let mut config = HotReloadConfig::new();
311        config.register("rate_limit", ConfigValue::Int(1000), true, "");
312
313        let result = config.update("rate_limit", ConfigValue::String("fast".into()), "admin");
314        assert!(matches!(
315            result,
316            Err(ConfigUpdateError::TypeMismatch { .. })
317        ));
318    }
319
320    #[test]
321    fn test_audit_trail() {
322        let mut config = HotReloadConfig::new();
323        config.register("rate_limit", ConfigValue::Int(1000), true, "");
324
325        config
326            .update("rate_limit", ConfigValue::Int(2000), "admin")
327            .unwrap();
328        config
329            .update("rate_limit", ConfigValue::Int(3000), "operator")
330            .unwrap();
331
332        let changes = config.recent_changes(10);
333        assert_eq!(changes.len(), 2);
334        assert_eq!(changes[0].changed_by, "admin");
335        assert_eq!(changes[1].changed_by, "operator");
336    }
337
338    #[test]
339    fn test_versioning() {
340        let mut config = HotReloadConfig::new();
341        config.register("a", ConfigValue::Bool(true), true, "");
342
343        assert_eq!(config.version(), 0);
344        config
345            .update("a", ConfigValue::Bool(false), "test")
346            .unwrap();
347        assert_eq!(config.version(), 1);
348        config.update("a", ConfigValue::Bool(true), "test").unwrap();
349        assert_eq!(config.version(), 2);
350    }
351}