config_lib/
hot_reload.rs

1//! Configuration Hot Reloading System
2//!
3//! Enterprise-grade hot reloading with:
4//! - File watching for automatic updates
5//! - Arc swapping for zero-downtime updates
6//! - Change notifications and callbacks
7//! - Thread-safe concurrent access
8//! - Graceful error handling and fallback
9
10use crate::config::Config;
11use crate::error::{Error, Result};
12use std::path::{Path, PathBuf};
13use std::sync::mpsc::{self, Receiver, Sender};
14use std::sync::{Arc, RwLock};
15use std::thread;
16use std::time::{Duration, SystemTime};
17
18/// Configuration change event types
19#[derive(Debug, Clone)]
20pub enum ConfigChangeEvent {
21    /// Configuration successfully reloaded
22    Reloaded {
23        /// Path to the configuration file that was reloaded
24        path: PathBuf,
25        /// Timestamp when the reload completed
26        timestamp: SystemTime,
27    },
28    /// Configuration reload failed
29    ReloadFailed {
30        /// Path to the configuration file that failed to reload
31        path: PathBuf,
32        /// Error message describing what went wrong
33        error: String,
34        /// Timestamp when the error occurred
35        timestamp: SystemTime,
36    },
37    /// Configuration file was modified
38    FileModified {
39        /// Path to the configuration file that was modified
40        path: PathBuf,
41        /// Timestamp when the modification was detected
42        timestamp: SystemTime,
43    },
44    /// Configuration file was deleted
45    FileDeleted {
46        /// Path to the configuration file that was deleted
47        path: PathBuf,
48        /// Timestamp when the deletion was detected
49        timestamp: SystemTime,
50    },
51}
52
53/// Hot-reloadable configuration container
54pub struct HotReloadConfig {
55    /// Current configuration (thread-safe)
56    current: Arc<RwLock<Config>>,
57    /// File path being watched
58    file_path: PathBuf,
59    /// Last known modification time
60    last_modified: SystemTime,
61    /// Event sender for notifications
62    event_sender: Option<Sender<ConfigChangeEvent>>,
63    /// Polling interval for file changes
64    poll_interval: Duration,
65}
66
67impl HotReloadConfig {
68    /// Create a new hot-reloadable configuration from a file
69    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
70        let path = path.as_ref().to_path_buf();
71        let config = Config::from_file(&path)?;
72
73        let last_modified = std::fs::metadata(&path)
74            .map_err(|e| Error::io(path.display().to_string(), e))?
75            .modified()
76            .map_err(|e| Error::io(path.display().to_string(), e))?;
77
78        Ok(Self {
79            current: Arc::new(RwLock::new(config)),
80            file_path: path,
81            last_modified,
82            event_sender: None,
83            poll_interval: Duration::from_millis(1000), // Default 1 second polling
84        })
85    }
86
87    /// Set the polling interval for file change detection
88    pub fn with_poll_interval(mut self, interval: Duration) -> Self {
89        self.poll_interval = interval;
90        self
91    }
92
93    /// Enable change notifications
94    pub fn with_change_notifications(mut self) -> (Self, Receiver<ConfigChangeEvent>) {
95        let (sender, receiver) = mpsc::channel();
96        self.event_sender = Some(sender);
97        (self, receiver)
98    }
99
100    /// Get a thread-safe reference to the current configuration
101    pub fn config(&self) -> Arc<RwLock<Config>> {
102        Arc::clone(&self.current)
103    }
104
105    /// Get a read-only snapshot of the current configuration
106    pub fn snapshot(&self) -> Result<Config> {
107        let _config = self
108            .current
109            .read()
110            .map_err(|_| Error::concurrency("Failed to acquire read lock".to_string()))?;
111
112        // Create a deep copy of the config
113        // Since Config doesn't implement Clone, we'll serialize and deserialize
114        let _content = std::fs::read_to_string(&self.file_path)
115            .map_err(|e| Error::io(self.file_path.display().to_string(), e))?;
116
117        Config::from_file(&self.file_path)
118    }
119
120    /// Manually trigger a reload
121    pub fn reload(&mut self) -> Result<bool> {
122        let metadata = std::fs::metadata(&self.file_path)
123            .map_err(|e| Error::io(self.file_path.display().to_string(), e))?;
124
125        let modified = metadata
126            .modified()
127            .map_err(|e| Error::io(self.file_path.display().to_string(), e))?;
128
129        if modified <= self.last_modified {
130            return Ok(false); // No changes
131        }
132
133        match Config::from_file(&self.file_path) {
134            Ok(new_config) => {
135                // Atomic swap of configuration
136                {
137                    let mut config = self.current.write().map_err(|_| {
138                        Error::concurrency("Failed to acquire write lock".to_string())
139                    })?;
140                    *config = new_config;
141                }
142
143                self.last_modified = modified;
144
145                // Send notification if enabled
146                if let Some(ref sender) = self.event_sender {
147                    let _ = sender.send(ConfigChangeEvent::Reloaded {
148                        path: self.file_path.clone(),
149                        timestamp: SystemTime::now(),
150                    });
151                }
152
153                Ok(true)
154            }
155            Err(e) => {
156                // Send error notification if enabled
157                if let Some(ref sender) = self.event_sender {
158                    let _ = sender.send(ConfigChangeEvent::ReloadFailed {
159                        path: self.file_path.clone(),
160                        error: e.to_string(),
161                        timestamp: SystemTime::now(),
162                    });
163                }
164                Err(e)
165            }
166        }
167    }
168
169    /// Start automatic hot reloading in a background thread
170    pub fn start_watching(self) -> HotReloadHandle {
171        let (stop_sender, stop_receiver) = mpsc::channel();
172        let config_clone = Arc::clone(&self.current);
173        let file_path = self.file_path.clone();
174        let event_sender = self.event_sender.clone();
175        let poll_interval = self.poll_interval;
176        let mut last_modified = self.last_modified;
177
178        let handle = thread::spawn(move || {
179            loop {
180                // Check for stop signal
181                if stop_receiver.try_recv().is_ok() {
182                    break;
183                }
184
185                // Check for file changes
186                if let Ok(metadata) = std::fs::metadata(&file_path) {
187                    if let Ok(modified) = metadata.modified() {
188                        if modified > last_modified {
189                            // File was modified, send notification
190                            if let Some(ref sender) = event_sender {
191                                let _ = sender.send(ConfigChangeEvent::FileModified {
192                                    path: file_path.clone(),
193                                    timestamp: SystemTime::now(),
194                                });
195                            }
196
197                            // Attempt to reload
198                            match Config::from_file(&file_path) {
199                                Ok(new_config) => {
200                                    // Atomic swap
201                                    if let Ok(mut config) = config_clone.write() {
202                                        *config = new_config;
203                                        last_modified = modified;
204
205                                        // Send success notification
206                                        if let Some(ref sender) = event_sender {
207                                            let _ = sender.send(ConfigChangeEvent::Reloaded {
208                                                path: file_path.clone(),
209                                                timestamp: SystemTime::now(),
210                                            });
211                                        }
212                                    }
213                                }
214                                Err(e) => {
215                                    // Send error notification
216                                    if let Some(ref sender) = event_sender {
217                                        let _ = sender.send(ConfigChangeEvent::ReloadFailed {
218                                            path: file_path.clone(),
219                                            error: e.to_string(),
220                                            timestamp: SystemTime::now(),
221                                        });
222                                    }
223                                }
224                            }
225                        }
226                    }
227                }
228
229                thread::sleep(poll_interval);
230            }
231        });
232
233        HotReloadHandle {
234            handle: Some(handle),
235            stop_sender,
236        }
237    }
238
239    /// Get the file path being watched
240    pub fn file_path(&self) -> &Path {
241        &self.file_path
242    }
243
244    /// Get the last modification time
245    pub fn last_modified(&self) -> SystemTime {
246        self.last_modified
247    }
248}
249
250/// Handle for controlling hot reload background thread
251pub struct HotReloadHandle {
252    handle: Option<thread::JoinHandle<()>>,
253    stop_sender: Sender<()>,
254}
255
256impl HotReloadHandle {
257    /// Stop the background watching thread
258    pub fn stop(mut self) -> Result<()> {
259        if self.stop_sender.send(()).is_err() {
260            return Err(Error::concurrency("Failed to send stop signal".to_string()));
261        }
262
263        if let Some(handle) = self.handle.take() {
264            handle
265                .join()
266                .map_err(|_| Error::concurrency("Failed to join background thread".to_string()))?;
267        }
268
269        Ok(())
270    }
271}
272
273impl Drop for HotReloadHandle {
274    fn drop(&mut self) {
275        let _ = self.stop_sender.send(());
276        if let Some(handle) = self.handle.take() {
277            let _ = handle.join();
278        }
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use std::fs::File;
286    use std::io::Write;
287    use tempfile::TempDir;
288
289    #[test]
290    fn test_hot_reload_basic() {
291        let temp_dir = TempDir::new().unwrap();
292        let config_path = temp_dir.path().join("test.conf");
293
294        // Create initial config file
295        let mut file = File::create(&config_path).unwrap();
296        writeln!(file, "key=value1").unwrap();
297        file.flush().unwrap();
298        drop(file);
299
300        // Create hot reload config
301        let mut hot_config = HotReloadConfig::from_file(&config_path).unwrap();
302
303        // Read initial value
304        {
305            let config = hot_config.config();
306            let config_read = config.read().unwrap();
307            assert_eq!(
308                config_read.get("key").unwrap().as_string().unwrap(),
309                "value1"
310            );
311        }
312
313        // Wait a bit to ensure different modification time
314        thread::sleep(Duration::from_millis(10));
315
316        // Update config file
317        let mut file = File::create(&config_path).unwrap();
318        writeln!(file, "key=value2").unwrap();
319        file.flush().unwrap();
320        drop(file);
321
322        // Manual reload
323        let reloaded = hot_config.reload().unwrap();
324        assert!(reloaded);
325
326        // Verify new value
327        {
328            let config = hot_config.config();
329            let config_read = config.read().unwrap();
330            assert_eq!(
331                config_read.get("key").unwrap().as_string().unwrap(),
332                "value2"
333            );
334        }
335    }
336
337    #[test]
338    fn test_hot_reload_notifications() {
339        let temp_dir = TempDir::new().unwrap();
340        let config_path = temp_dir.path().join("test.conf");
341
342        // Create initial config file
343        let mut file = File::create(&config_path).unwrap();
344        writeln!(file, "key=value1").unwrap();
345        file.flush().unwrap();
346        drop(file);
347
348        // Create hot reload config with notifications
349        let (mut hot_config, receiver) = HotReloadConfig::from_file(&config_path)
350            .unwrap()
351            .with_change_notifications();
352
353        // Wait a bit
354        thread::sleep(Duration::from_millis(10));
355
356        // Update config file
357        let mut file = File::create(&config_path).unwrap();
358        writeln!(file, "key=value2").unwrap();
359        file.flush().unwrap();
360        drop(file);
361
362        // Manual reload should trigger notification
363        hot_config.reload().unwrap();
364
365        // Check for notification
366        let event = receiver.try_recv().unwrap();
367        match event {
368            ConfigChangeEvent::Reloaded { path, .. } => {
369                assert_eq!(path, config_path);
370            }
371            _ => panic!("Expected Reloaded event"),
372        }
373    }
374
375    #[test]
376    fn test_automatic_watching() {
377        let temp_dir = TempDir::new().unwrap();
378        let config_path = temp_dir.path().join("test.conf");
379
380        // Create initial config file
381        let mut file = File::create(&config_path).unwrap();
382        writeln!(file, "key=value1").unwrap();
383        file.flush().unwrap();
384        drop(file);
385
386        // Create hot reload config with fast polling
387        let (hot_config, receiver) = HotReloadConfig::from_file(&config_path)
388            .unwrap()
389            .with_poll_interval(Duration::from_millis(50))
390            .with_change_notifications();
391
392        let config_ref = hot_config.config();
393        let handle = hot_config.start_watching();
394
395        // Wait a bit
396        thread::sleep(Duration::from_millis(100));
397
398        // Update config file
399        let mut file = File::create(&config_path).unwrap();
400        writeln!(file, "key=value2").unwrap();
401        file.flush().unwrap();
402        drop(file);
403
404        // Wait for automatic reload
405        thread::sleep(Duration::from_millis(200));
406
407        // Check that config was updated
408        {
409            let config_read = config_ref.read().unwrap();
410            assert_eq!(
411                config_read.get("key").unwrap().as_string().unwrap(),
412                "value2"
413            );
414        }
415
416        // Check for notifications
417        let mut received_events = Vec::new();
418        while let Ok(event) = receiver.try_recv() {
419            received_events.push(event);
420        }
421
422        assert!(!received_events.is_empty());
423
424        // Should have received at least a Reloaded event
425        let has_reloaded = received_events
426            .iter()
427            .any(|event| matches!(event, ConfigChangeEvent::Reloaded { .. }));
428        assert!(has_reloaded);
429
430        // Stop watching
431        handle.stop().unwrap();
432    }
433}