1use crate::error::{Result, ThingsError};
7use crate::mcp_config::McpServerConfig;
8use std::path::PathBuf;
9use std::sync::Arc;
10use std::time::Duration;
11use tokio::sync::{broadcast, RwLock};
12use tokio::time::interval;
13use tracing::{debug, error, info};
14
15#[derive(Debug)]
17pub struct ConfigHotReloader {
18 config: Arc<RwLock<McpServerConfig>>,
20 config_path: PathBuf,
22 reload_interval: Duration,
24 enabled: bool,
26 change_tx: broadcast::Sender<McpServerConfig>,
28 last_modified: Option<std::time::SystemTime>,
30}
31
32impl ConfigHotReloader {
33 pub fn new(
43 config: McpServerConfig,
44 config_path: PathBuf,
45 reload_interval: Duration,
46 ) -> Result<Self> {
47 if !config_path.exists() {
49 return Err(ThingsError::configuration(format!(
50 "Configuration file does not exist: {}",
51 config_path.display()
52 )));
53 }
54
55 let (change_tx, _) = broadcast::channel(16);
56 let last_modified = Self::get_file_modified_time(&config_path)?;
57
58 Ok(Self {
59 config: Arc::new(RwLock::new(config)),
60 config_path,
61 reload_interval,
62 enabled: true,
63 change_tx,
64 last_modified: Some(last_modified),
65 })
66 }
67
68 pub fn with_default_settings(config_path: PathBuf) -> Result<Self> {
76 let config = McpServerConfig::default();
77 Self::new(config, config_path, Duration::from_secs(5))
78 }
79
80 #[must_use]
82 pub async fn get_config(&self) -> McpServerConfig {
83 self.config.read().await.clone()
84 }
85
86 pub async fn update_config(&self, new_config: McpServerConfig) -> Result<()> {
94 new_config.validate()?;
95
96 let mut config = self.config.write().await;
97 *config = new_config.clone();
98
99 let _ = self.change_tx.send(new_config);
101
102 info!("Configuration updated successfully");
103 Ok(())
104 }
105
106 #[must_use]
108 pub fn subscribe_to_changes(&self) -> broadcast::Receiver<McpServerConfig> {
109 self.change_tx.subscribe()
110 }
111
112 pub fn set_enabled(&mut self, enabled: bool) {
114 self.enabled = enabled;
115 if enabled {
116 info!("Configuration hot reloading enabled");
117 } else {
118 info!("Configuration hot reloading disabled");
119 }
120 }
121
122 #[must_use]
124 pub fn is_enabled(&self) -> bool {
125 self.enabled
126 }
127
128 pub fn start(&self) -> Result<()> {
137 if !self.enabled {
138 debug!("Hot reloading is disabled, not starting reloader task");
139 return Ok(());
140 }
141
142 let config = Arc::clone(&self.config);
143 let config_path = self.config_path.clone();
144 let change_tx = self.change_tx.clone();
145 let mut interval = interval(self.reload_interval);
146 let mut last_modified = self.last_modified;
147
148 info!(
149 "Starting configuration hot reloader for: {}",
150 config_path.display()
151 );
152
153 tokio::spawn(async move {
154 loop {
155 interval.tick().await;
156
157 match Self::check_and_reload_config(
158 &config_path,
159 &config,
160 &change_tx,
161 &mut last_modified,
162 )
163 .await
164 {
165 Ok(reloaded) => {
166 if reloaded {
167 debug!(
168 "Configuration reloaded from file: {}",
169 config_path.display()
170 );
171 }
172 }
173 Err(e) => {
174 error!("Failed to check/reload configuration: {}", e);
175 }
176 }
177 }
178 });
179
180 Ok(())
181 }
182
183 async fn check_and_reload_config(
185 config_path: &PathBuf,
186 config: &Arc<RwLock<McpServerConfig>>,
187 change_tx: &broadcast::Sender<McpServerConfig>,
188 last_modified: &mut Option<std::time::SystemTime>,
189 ) -> Result<bool> {
190 let current_modified = Self::get_file_modified_time(config_path)?;
192
193 if let Some(last) = *last_modified {
194 if current_modified <= last {
195 return Ok(false); }
197 }
198
199 debug!("Configuration file modified, attempting to reload");
201
202 match McpServerConfig::from_file(config_path) {
203 Ok(new_config) => {
204 new_config.validate()?;
206
207 {
209 let mut current_config = config.write().await;
210 *current_config = new_config.clone();
211 }
212
213 let _ = change_tx.send(new_config);
215
216 *last_modified = Some(current_modified);
218
219 info!(
220 "Configuration successfully reloaded from: {}",
221 config_path.display()
222 );
223 Ok(true)
224 }
225 Err(e) => {
226 error!(
227 "Failed to reload configuration from {}: {}",
228 config_path.display(),
229 e
230 );
231 Err(e)
232 }
233 }
234 }
235
236 fn get_file_modified_time(path: &PathBuf) -> Result<std::time::SystemTime> {
238 let metadata = std::fs::metadata(path).map_err(|e| {
239 ThingsError::Io(std::io::Error::other(format!(
240 "Failed to get file metadata for {}: {}",
241 path.display(),
242 e
243 )))
244 })?;
245
246 metadata.modified().map_err(|e| {
247 ThingsError::Io(std::io::Error::other(format!(
248 "Failed to get modification time for {}: {}",
249 path.display(),
250 e
251 )))
252 })
253 }
254
255 pub async fn reload_now(&self) -> Result<bool> {
260 let mut last_modified = self.last_modified;
261 Self::check_and_reload_config(
262 &self.config_path,
263 &self.config,
264 &self.change_tx,
265 &mut last_modified,
266 )
267 .await
268 }
269
270 #[must_use]
272 pub fn config_path(&self) -> &PathBuf {
273 &self.config_path
274 }
275
276 #[must_use]
278 pub fn reload_interval(&self) -> Duration {
279 self.reload_interval
280 }
281
282 pub fn set_reload_interval(&mut self, interval: Duration) {
284 self.reload_interval = interval;
285 debug!("Configuration reload interval set to: {:?}", interval);
286 }
287}
288
289#[async_trait::async_trait]
291pub trait ConfigChangeHandler: Send + Sync {
292 async fn handle_config_change(
298 &self,
299 old_config: &McpServerConfig,
300 new_config: &McpServerConfig,
301 );
302}
303
304pub struct DefaultConfigChangeHandler;
306
307#[async_trait::async_trait]
308impl ConfigChangeHandler for DefaultConfigChangeHandler {
309 async fn handle_config_change(
310 &self,
311 old_config: &McpServerConfig,
312 new_config: &McpServerConfig,
313 ) {
314 info!("Configuration changed:");
315
316 if old_config.server.name != new_config.server.name {
317 info!(
318 " Server name: {} -> {}",
319 old_config.server.name, new_config.server.name
320 );
321 }
322 if old_config.logging.level != new_config.logging.level {
323 info!(
324 " Log level: {} -> {}",
325 old_config.logging.level, new_config.logging.level
326 );
327 }
328 if old_config.cache.enabled != new_config.cache.enabled {
329 info!(
330 " Cache enabled: {} -> {}",
331 old_config.cache.enabled, new_config.cache.enabled
332 );
333 }
334 if old_config.performance.enabled != new_config.performance.enabled {
335 info!(
336 " Performance monitoring: {} -> {}",
337 old_config.performance.enabled, new_config.performance.enabled
338 );
339 }
340 if old_config.security.authentication.enabled != new_config.security.authentication.enabled
341 {
342 info!(
343 " Authentication: {} -> {}",
344 old_config.security.authentication.enabled,
345 new_config.security.authentication.enabled
346 );
347 }
348 }
349}
350
351pub struct ConfigHotReloaderWithHandler {
353 reloader: ConfigHotReloader,
355 handler: Arc<dyn ConfigChangeHandler>,
357}
358
359impl ConfigHotReloaderWithHandler {
360 pub fn new(
371 config: McpServerConfig,
372 config_path: PathBuf,
373 reload_interval: Duration,
374 handler: Arc<dyn ConfigChangeHandler>,
375 ) -> Result<Self> {
376 let reloader = ConfigHotReloader::new(config, config_path, reload_interval)?;
377
378 Ok(Self { reloader, handler })
379 }
380
381 pub fn start_with_handler(&self) -> Result<()> {
386 self.reloader.start()?;
388
389 let mut change_rx = self.reloader.subscribe_to_changes();
391 let handler = Arc::clone(&self.handler);
392 let config = Arc::clone(&self.reloader.config);
393
394 tokio::spawn(async move {
395 let mut old_config = config.read().await.clone();
396
397 while let Ok(new_config) = change_rx.recv().await {
398 handler.handle_config_change(&old_config, &new_config).await;
399 old_config = new_config;
400 }
401 });
402
403 Ok(())
404 }
405
406 #[must_use]
408 pub fn reloader(&self) -> &ConfigHotReloader {
409 &self.reloader
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use std::time::Duration;
417 use tempfile::NamedTempFile;
418
419 #[tokio::test]
420 async fn test_config_hot_reloader_creation() {
421 let temp_file = NamedTempFile::new().unwrap();
422 let config_path = temp_file.path().with_extension("json");
423
424 let config = McpServerConfig::default();
425 config.to_file(&config_path, "json").unwrap();
426
427 let reloader = ConfigHotReloader::new(config, config_path, Duration::from_secs(1)).unwrap();
428 assert!(reloader.is_enabled());
429 }
430
431 #[tokio::test]
432 async fn test_config_hot_reloader_with_default_settings() {
433 let temp_file = NamedTempFile::new().unwrap();
434 let config_path = temp_file.path().with_extension("json");
435
436 let config = McpServerConfig::default();
437 config.to_file(&config_path, "json").unwrap();
438
439 let reloader = ConfigHotReloader::with_default_settings(config_path).unwrap();
440 assert!(reloader.is_enabled());
441 }
442
443 #[tokio::test]
444 async fn test_config_hot_reloader_enable_disable() {
445 let temp_file = NamedTempFile::new().unwrap();
446 let config_path = temp_file.path().with_extension("json");
447
448 let config = McpServerConfig::default();
449 config.to_file(&config_path, "json").unwrap();
450
451 let mut reloader =
452 ConfigHotReloader::new(config, config_path, Duration::from_secs(1)).unwrap();
453 assert!(reloader.is_enabled());
454
455 reloader.set_enabled(false);
456 assert!(!reloader.is_enabled());
457
458 reloader.set_enabled(true);
459 assert!(reloader.is_enabled());
460 }
461
462 #[tokio::test]
463 async fn test_config_hot_reloader_get_config() {
464 let temp_file = NamedTempFile::new().unwrap();
465 let config_path = temp_file.path().with_extension("json");
466
467 let mut config = McpServerConfig::default();
468 config.server.name = "test-server".to_string();
469 config.to_file(&config_path, "json").unwrap();
470
471 let reloader = ConfigHotReloader::new(config, config_path, Duration::from_secs(1)).unwrap();
472 let loaded_config = reloader.get_config().await;
473 assert_eq!(loaded_config.server.name, "test-server");
474 }
475
476 #[tokio::test]
477 async fn test_config_hot_reloader_update_config() {
478 let temp_file = NamedTempFile::new().unwrap();
479 let config_path = temp_file.path().with_extension("json");
480
481 let config = McpServerConfig::default();
482 config.to_file(&config_path, "json").unwrap();
483
484 let reloader = ConfigHotReloader::new(config, config_path, Duration::from_secs(1)).unwrap();
485
486 let mut new_config = McpServerConfig::default();
487 new_config.server.name = "updated-server".to_string();
488
489 reloader.update_config(new_config).await.unwrap();
490
491 let loaded_config = reloader.get_config().await;
492 assert_eq!(loaded_config.server.name, "updated-server");
493 }
494
495 #[tokio::test]
496 async fn test_config_hot_reloader_subscribe_to_changes() {
497 let temp_file = NamedTempFile::new().unwrap();
498 let config_path = temp_file.path().with_extension("json");
499
500 let config = McpServerConfig::default();
501 config.to_file(&config_path, "json").unwrap();
502
503 let reloader = ConfigHotReloader::new(config, config_path, Duration::from_secs(1)).unwrap();
504 let mut change_rx = reloader.subscribe_to_changes();
505
506 let mut new_config = McpServerConfig::default();
507 new_config.server.name = "changed-server".to_string();
508
509 reloader.update_config(new_config).await.unwrap();
510
511 let received_config = change_rx.recv().await.unwrap();
512 assert_eq!(received_config.server.name, "changed-server");
513 }
514
515 #[tokio::test]
516 async fn test_config_hot_reloader_with_handler() {
517 let temp_file = NamedTempFile::new().unwrap();
518 let config_path = temp_file.path().with_extension("json");
519
520 let config = McpServerConfig::default();
521 config.to_file(&config_path, "json").unwrap();
522
523 let handler = Arc::new(DefaultConfigChangeHandler);
524 let reloader =
525 ConfigHotReloaderWithHandler::new(config, config_path, Duration::from_secs(1), handler)
526 .unwrap();
527
528 assert!(reloader.reloader().is_enabled());
529 }
530
531 #[tokio::test]
532 async fn test_config_hot_reloader_nonexistent_file() {
533 let config_path = PathBuf::from("/nonexistent/config.json");
534 let config = McpServerConfig::default();
535
536 let result = ConfigHotReloader::new(config, config_path, Duration::from_secs(1));
537 assert!(result.is_err());
538 let error = result.unwrap_err();
539 assert!(matches!(error, ThingsError::Configuration { .. }));
540 }
541
542 #[tokio::test]
543 async fn test_config_hot_reloader_invalid_config_file() {
544 let temp_file = NamedTempFile::new().unwrap();
545 let config_path = temp_file.path().with_extension("json");
546
547 std::fs::write(&config_path, "{ invalid json }").unwrap();
549
550 let result = McpServerConfig::from_file(&config_path);
552 assert!(result.is_err());
553 }
554
555 #[tokio::test]
556 async fn test_config_hot_reloader_file_permission_error() {
557 let temp_file = NamedTempFile::new().unwrap();
558 let config_path = temp_file.path().with_extension("json");
559
560 let config = McpServerConfig::default();
561 config.to_file(&config_path, "json").unwrap();
562
563 let reloader =
565 ConfigHotReloader::new(config, config_path.clone(), Duration::from_secs(1)).unwrap();
566
567 std::fs::remove_file(&config_path).unwrap();
569
570 let result = reloader.reload_now().await;
572 assert!(result.is_err());
573 }
574
575 #[tokio::test]
576 async fn test_config_hot_reloader_concurrent_updates() {
577 let temp_file = NamedTempFile::new().unwrap();
578 let config_path = temp_file.path().with_extension("json");
579
580 let config = McpServerConfig::default();
581 config.to_file(&config_path, "json").unwrap();
582
583 let reloader =
584 ConfigHotReloader::new(config, config_path.clone(), Duration::from_secs(1)).unwrap();
585 let mut change_rx = reloader.subscribe_to_changes();
586
587 let mut config1 = McpServerConfig::default();
589 config1.server.name = "config1".to_string();
590
591 let mut config2 = McpServerConfig::default();
592 config2.server.name = "config2".to_string();
593
594 let reloader_clone = Arc::new(reloader);
596 let reloader1 = Arc::clone(&reloader_clone);
597 let reloader2 = Arc::clone(&reloader_clone);
598
599 let handle1 = tokio::spawn(async move { reloader1.update_config(config1).await });
600
601 let handle2 = tokio::spawn(async move { reloader2.update_config(config2).await });
602
603 let _ = handle1.await.unwrap();
605 let _ = handle2.await.unwrap();
606
607 let _received_config = change_rx.recv().await.unwrap();
609 }
610
611 #[tokio::test]
612 async fn test_config_hot_reloader_validation_error() {
613 let temp_file = NamedTempFile::new().unwrap();
614 let config_path = temp_file.path().with_extension("json");
615
616 let config = McpServerConfig::default();
617 config.to_file(&config_path, "json").unwrap();
618
619 let reloader = ConfigHotReloader::new(config, config_path, Duration::from_secs(1)).unwrap();
620
621 let mut invalid_config = McpServerConfig::default();
623 invalid_config.server.name = String::new(); let result = reloader.update_config(invalid_config).await;
626 assert!(result.is_err());
627 }
628
629 #[tokio::test]
630 async fn test_config_hot_reloader_disabled_start() {
631 let temp_file = NamedTempFile::new().unwrap();
632 let config_path = temp_file.path().with_extension("json");
633
634 let config = McpServerConfig::default();
635 config.to_file(&config_path, "json").unwrap();
636
637 let mut reloader =
638 ConfigHotReloader::new(config, config_path, Duration::from_secs(1)).unwrap();
639 reloader.set_enabled(false);
640
641 let result = reloader.start();
643 assert!(result.is_ok());
644 }
645
646 #[tokio::test]
647 async fn test_config_hot_reloader_reload_interval() {
648 let temp_file = NamedTempFile::new().unwrap();
649 let config_path = temp_file.path().with_extension("json");
650
651 let config = McpServerConfig::default();
652 config.to_file(&config_path, "json").unwrap();
653
654 let mut reloader =
655 ConfigHotReloader::new(config, config_path, Duration::from_secs(5)).unwrap();
656
657 assert_eq!(reloader.reload_interval(), Duration::from_secs(5));
658
659 reloader.set_reload_interval(Duration::from_secs(10));
660 assert_eq!(reloader.reload_interval(), Duration::from_secs(10));
661 }
662
663 #[tokio::test]
664 async fn test_config_hot_reloader_metadata_error() {
665 let temp_file = NamedTempFile::new().unwrap();
666 let config_path = temp_file.path().with_extension("json");
667
668 let config = McpServerConfig::default();
669 config.to_file(&config_path, "json").unwrap();
670
671 let reloader =
672 ConfigHotReloader::new(config, config_path.clone(), Duration::from_secs(1)).unwrap();
673
674 std::fs::remove_file(&config_path).unwrap();
676
677 let result = reloader.reload_now().await;
679 assert!(result.is_err());
680 }
681
682 #[tokio::test]
683 async fn test_config_hot_reloader_with_handler_start() {
684 let temp_file = NamedTempFile::new().unwrap();
685 let config_path = temp_file.path().with_extension("json");
686
687 let config = McpServerConfig::default();
688 config.to_file(&config_path, "json").unwrap();
689
690 let handler = Arc::new(DefaultConfigChangeHandler);
691 let reloader =
692 ConfigHotReloaderWithHandler::new(config, config_path, Duration::from_secs(1), handler)
693 .unwrap();
694
695 let result = reloader.start_with_handler();
697 assert!(result.is_ok());
698 }
699
700 #[tokio::test]
701 async fn test_config_hot_reloader_file_modified_time() {
702 let temp_file = NamedTempFile::new().unwrap();
703 let config_path = temp_file.path().with_extension("json");
704
705 let config = McpServerConfig::default();
706 config.to_file(&config_path, "json").unwrap();
707
708 let modified_time = ConfigHotReloader::get_file_modified_time(&config_path);
710 assert!(modified_time.is_ok());
711 }
712
713 #[tokio::test]
714 async fn test_config_hot_reloader_file_modified_time_nonexistent() {
715 let config_path = PathBuf::from("/nonexistent/file.json");
716
717 let result = ConfigHotReloader::get_file_modified_time(&config_path);
719 assert!(result.is_err());
720 }
721
722 #[tokio::test]
723 async fn test_config_hot_reloader_config_path() {
724 let temp_file = NamedTempFile::new().unwrap();
725 let config_path = temp_file.path().with_extension("json");
726
727 let config = McpServerConfig::default();
728 config.to_file(&config_path, "json").unwrap();
729
730 let reloader =
731 ConfigHotReloader::new(config, config_path.clone(), Duration::from_secs(1)).unwrap();
732
733 assert_eq!(reloader.config_path(), &config_path);
734 }
735}