Skip to main content

rust_serv/config_reloader/
reloader.rs

1//! Configuration hot reloader
2
3use std::path::{Path, PathBuf};
4use std::sync::{Arc, RwLock};
5use std::time::Duration;
6
7use crate::config::Config;
8use crate::config_reloader::diff::ConfigDiff;
9use crate::config_reloader::watcher::ConfigWatcher;
10
11/// Configuration reload result
12#[derive(Debug, Clone, PartialEq)]
13pub enum ReloadResult {
14    /// Configuration reloaded successfully
15    Success(ConfigDiff),
16    /// No changes detected
17    NoChanges,
18    /// Configuration file not found
19    FileNotFound,
20    /// Parse error
21    ParseError(String),
22    /// Reload requires restart
23    RequiresRestart(ConfigDiff),
24}
25
26/// Configuration hot reloader
27pub struct ConfigReloader {
28    /// Current configuration
29    config: Arc<RwLock<Config>>,
30    /// Configuration file path
31    config_path: PathBuf,
32    /// File watcher
33    watcher: Option<ConfigWatcher>,
34    /// Auto-reload enabled
35    auto_reload: bool,
36    /// Debounce duration
37    debounce_ms: u64,
38}
39
40impl ConfigReloader {
41    /// Create a new config reloader
42    pub fn new(config: Config, config_path: impl AsRef<Path>) -> Self {
43        Self {
44            config: Arc::new(RwLock::new(config)),
45            config_path: config_path.as_ref().to_path_buf(),
46            watcher: None,
47            auto_reload: false,
48            debounce_ms: 500,
49        }
50    }
51    
52    /// Enable auto-reload with file watching
53    pub fn enable_auto_reload(&mut self) -> Result<(), Box<dyn std::error::Error>> {
54        let mut watcher = ConfigWatcher::new(&self.config_path)?;
55        watcher.watch(&self.config_path)?;
56        self.watcher = Some(watcher);
57        self.auto_reload = true;
58        Ok(())
59    }
60    
61    /// Disable auto-reload
62    pub fn disable_auto_reload(&mut self) {
63        self.watcher = None;
64        self.auto_reload = false;
65    }
66    
67    /// Check if auto-reload is enabled
68    pub fn is_auto_reload_enabled(&self) -> bool {
69        self.auto_reload
70    }
71    
72    /// Set debounce duration
73    pub fn set_debounce_ms(&mut self, ms: u64) {
74        self.debounce_ms = ms;
75    }
76    
77    /// Get current configuration
78    pub fn get_config(&self) -> Config {
79        self.config.read().unwrap().clone()
80    }
81    
82    /// Manually reload configuration
83    pub fn reload(&self) -> ReloadResult {
84        // Check if file exists
85        if !self.config_path.exists() {
86            return ReloadResult::FileNotFound;
87        }
88        
89        // Load new configuration
90        let content = match std::fs::read_to_string(&self.config_path) {
91            Ok(c) => c,
92            Err(e) => return ReloadResult::ParseError(e.to_string()),
93        };
94        
95        let new_config: Config = match toml::from_str(&content) {
96            Ok(c) => c,
97            Err(e) => return ReloadResult::ParseError(e.to_string()),
98        };
99        
100        // Get current config
101        let current_config = self.get_config();
102        
103        // Compare configurations
104        let diff = ConfigDiff::compare(&current_config, &new_config);
105        
106        if !diff.has_changes() {
107            return ReloadResult::NoChanges;
108        }
109        
110        // Check if restart is required
111        if diff.requires_restart {
112            return ReloadResult::RequiresRestart(diff);
113        }
114        
115        // Update configuration
116        if let Ok(mut config) = self.config.write() {
117            *config = new_config;
118        }
119        
120        ReloadResult::Success(diff)
121    }
122    
123    /// Check for file changes and reload if necessary
124    pub fn check_and_reload(&self) -> Option<ReloadResult> {
125        if !self.auto_reload {
126            return None;
127        }
128        
129        if let Some(ref watcher) = self.watcher {
130            if watcher.try_recv().is_some() {
131                // Debounce
132                std::thread::sleep(Duration::from_millis(self.debounce_ms));
133                return Some(self.reload());
134            }
135        }
136        
137        None
138    }
139    
140    /// Get configuration path
141    pub fn config_path(&self) -> &Path {
142        &self.config_path
143    }
144    
145    /// Get debounce duration
146    pub fn debounce_ms(&self) -> u64 {
147        self.debounce_ms
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use std::fs;
155    use std::thread;
156    use tempfile::TempDir;
157    
158    fn create_test_config() -> Config {
159        Config::default()
160    }
161    
162    #[test]
163    fn test_reloader_creation() {
164        let dir = TempDir::new().unwrap();
165        let config_path = dir.path().join("config.toml");
166        fs::write(&config_path, "port = 8080").unwrap();
167        
168        let config = create_test_config();
169        let reloader = ConfigReloader::new(config, &config_path);
170        
171        assert_eq!(reloader.config_path(), config_path);
172        assert!(!reloader.is_auto_reload_enabled());
173    }
174    
175    #[test]
176    fn test_reload_no_changes() {
177        let dir = TempDir::new().unwrap();
178        let config_path = dir.path().join("config.toml");
179        fs::write(&config_path, "port = 8080").unwrap();
180        
181        let config = create_test_config();
182        let reloader = ConfigReloader::new(config, &config_path);
183        
184        let result = reloader.reload();
185        assert!(matches!(result, ReloadResult::NoChanges));
186    }
187    
188    #[test]
189    fn test_reload_with_changes() {
190        let dir = TempDir::new().unwrap();
191        let config_path = dir.path().join("config.toml");
192        fs::write(&config_path, "log_level = \"debug\"").unwrap();
193        
194        let config = create_test_config();
195        let reloader = ConfigReloader::new(config, &config_path);
196        
197        let result = reloader.reload();
198        
199        if let ReloadResult::Success(diff) = result {
200            assert!(diff.has_changes());
201            assert!(diff.field_changed("log_level"));
202        } else {
203            panic!("Expected Success result");
204        }
205    }
206    
207    #[test]
208    fn test_reload_requires_restart() {
209        let dir = TempDir::new().unwrap();
210        let config_path = dir.path().join("config.toml");
211        fs::write(&config_path, "port = 9090").unwrap();
212        
213        let config = create_test_config();
214        let reloader = ConfigReloader::new(config, &config_path);
215        
216        let result = reloader.reload();
217        
218        if let ReloadResult::RequiresRestart(diff) = result {
219            assert!(diff.has_changes());
220            assert!(diff.field_changed("port"));
221            assert!(diff.requires_restart);
222        } else {
223            panic!("Expected RequiresRestart result");
224        }
225    }
226    
227    #[test]
228    fn test_reload_file_not_found() {
229        let dir = TempDir::new().unwrap();
230        let config_path = dir.path().join("nonexistent.toml");
231        
232        let config = create_test_config();
233        let reloader = ConfigReloader::new(config, &config_path);
234        
235        let result = reloader.reload();
236        assert!(matches!(result, ReloadResult::FileNotFound));
237    }
238    
239    #[test]
240    fn test_reload_parse_error() {
241        let dir = TempDir::new().unwrap();
242        let config_path = dir.path().join("config.toml");
243        fs::write(&config_path, "invalid toml content {{{").unwrap();
244        
245        let config = create_test_config();
246        let reloader = ConfigReloader::new(config, &config_path);
247        
248        let result = reloader.reload();
249        assert!(matches!(result, ReloadResult::ParseError(_)));
250    }
251    
252    #[test]
253    fn test_get_config() {
254        let dir = TempDir::new().unwrap();
255        let config_path = dir.path().join("config.toml");
256        fs::write(&config_path, "port = 8080").unwrap();
257        
258        let config = create_test_config();
259        let reloader = ConfigReloader::new(config.clone(), &config_path);
260        
261        let retrieved = reloader.get_config();
262        assert_eq!(retrieved.port, config.port);
263    }
264    
265    #[test]
266    fn test_set_debounce() {
267        let dir = TempDir::new().unwrap();
268        let config_path = dir.path().join("config.toml");
269        fs::write(&config_path, "").unwrap();
270        
271        let config = create_test_config();
272        let mut reloader = ConfigReloader::new(config, &config_path);
273        
274        reloader.set_debounce_ms(1000);
275        assert_eq!(reloader.debounce_ms(), 1000);
276    }
277
278    #[test]
279    fn test_enable_disable_auto_reload() {
280        let dir = TempDir::new().unwrap();
281        let config_path = dir.path().join("config.toml");
282        fs::write(&config_path, "port = 8080").unwrap();
283        
284        let config = create_test_config();
285        let mut reloader = ConfigReloader::new(config, &config_path);
286        
287        // Auto-reload disabled by default
288        assert!(!reloader.is_auto_reload_enabled());
289        
290        // Enable auto-reload
291        assert!(reloader.enable_auto_reload().is_ok());
292        assert!(reloader.is_auto_reload_enabled());
293        
294        // Disable auto-reload
295        reloader.disable_auto_reload();
296        assert!(!reloader.is_auto_reload_enabled());
297    }
298
299    #[test]
300    fn test_check_and_reload_without_auto_reload() {
301        let dir = TempDir::new().unwrap();
302        let config_path = dir.path().join("config.toml");
303        fs::write(&config_path, "port = 8080").unwrap();
304        
305        let config = create_test_config();
306        let reloader = ConfigReloader::new(config, &config_path);
307        
308        // Should return None when auto-reload is disabled
309        let result = reloader.check_and_reload();
310        assert!(result.is_none());
311    }
312}