rust_serv/config_reloader/
reloader.rs1use 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#[derive(Debug, Clone, PartialEq)]
13pub enum ReloadResult {
14 Success(ConfigDiff),
16 NoChanges,
18 FileNotFound,
20 ParseError(String),
22 RequiresRestart(ConfigDiff),
24}
25
26pub struct ConfigReloader {
28 config: Arc<RwLock<Config>>,
30 config_path: PathBuf,
32 watcher: Option<ConfigWatcher>,
34 auto_reload: bool,
36 debounce_ms: u64,
38}
39
40impl ConfigReloader {
41 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 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 pub fn disable_auto_reload(&mut self) {
63 self.watcher = None;
64 self.auto_reload = false;
65 }
66
67 pub fn is_auto_reload_enabled(&self) -> bool {
69 self.auto_reload
70 }
71
72 pub fn set_debounce_ms(&mut self, ms: u64) {
74 self.debounce_ms = ms;
75 }
76
77 pub fn get_config(&self) -> Config {
79 self.config.read().unwrap().clone()
80 }
81
82 pub fn reload(&self) -> ReloadResult {
84 if !self.config_path.exists() {
86 return ReloadResult::FileNotFound;
87 }
88
89 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 let current_config = self.get_config();
102
103 let diff = ConfigDiff::compare(¤t_config, &new_config);
105
106 if !diff.has_changes() {
107 return ReloadResult::NoChanges;
108 }
109
110 if diff.requires_restart {
112 return ReloadResult::RequiresRestart(diff);
113 }
114
115 if let Ok(mut config) = self.config.write() {
117 *config = new_config;
118 }
119
120 ReloadResult::Success(diff)
121 }
122
123 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 std::thread::sleep(Duration::from_millis(self.debounce_ms));
133 return Some(self.reload());
134 }
135 }
136
137 None
138 }
139
140 pub fn config_path(&self) -> &Path {
142 &self.config_path
143 }
144
145 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 assert!(!reloader.is_auto_reload_enabled());
289
290 assert!(reloader.enable_auto_reload().is_ok());
292 assert!(reloader.is_auto_reload_enabled());
293
294 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 let result = reloader.check_and_reload();
310 assert!(result.is_none());
311 }
312}