1use std::{
93 path::{Path, PathBuf},
94 sync::Arc,
95 time::{Duration, Instant},
96};
97
98use serde::{Deserialize, Serialize};
99use tokio::{
100 fs,
101 sync::{RwLock, broadcast, mpsc},
102 time::sleep,
103};
104use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Result as NotifyResult, Watcher};
105use chrono::{DateTime, Utc};
106
107use crate::{AirError, Configuration::AirConfiguration, Result, dev_log};
108
109pub struct ConfigHotReload {
115 active_config:Arc<RwLock<AirConfiguration>>,
117
118 previous_config:Arc<RwLock<Option<AirConfiguration>>>,
120
121 last_config_hash:Arc<RwLock<Option<String>>>,
123
124 config_path:PathBuf,
126
127 watcher:Option<Arc<RwLock<notify::RecommendedWatcher>>>,
129
130 change_sender:broadcast::Sender<ConfigChangeEvent>,
132
133 reload_tx:mpsc::Sender<ReloadRequest>,
135
136 change_history:Arc<RwLock<Vec<ConfigChangeRecord>>>,
138
139 last_reload:Arc<RwLock<Option<DateTime<Utc>>>>,
141
142 last_reload_duration:Arc<RwLock<Option<Duration>>>,
144
145 enabled:Arc<RwLock<bool>>,
147
148 debounce_delay:Duration,
150
151 last_change_time:Arc<RwLock<Option<Instant>>>,
153
154 stats:Arc<RwLock<ReloadStats>>,
156
157 validators:Arc<RwLock<Vec<Box<dyn ConfigValidator>>>>,
159
160 max_retries:u32,
162
163 retry_delay:Duration,
165
166 auto_rollback_enabled:Arc<RwLock<bool>>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct ConfigChangeEvent {
173 pub timestamp:DateTime<Utc>,
174 pub old_config_hash:Option<String>,
175 pub new_config_hash:String,
176 pub changes:Vec<ConfigChange>,
177 pub success:bool,
178}
179
180pub enum ReloadRequest {
182 Manual,
184 Signal,
186 FileChange,
188 Periodic,
190}
191
192#[derive(Debug, Clone, Default)]
194pub struct ReloadStats {
195 total_attempts:u64,
196 successful_reloads:u64,
197 failed_reloads:u64,
198 validation_errors:u64,
199 parse_errors:u64,
200 rollback_attempts:u64,
201 last_error:Option<String>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct ConfigChangeRecord {
207 pub timestamp:DateTime<Utc>,
208 pub changes:Vec<ConfigChange>,
209 pub validated:bool,
210 pub reason:String,
211 pub rollback_performed:bool,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct ConfigChange {
217 pub path:String,
218 pub old_value:serde_json::Value,
219 pub new_value:serde_json::Value,
220}
221
222pub trait ConfigValidator: Send + Sync {
224 fn validate(&self, config:&AirConfiguration) -> Result<()>;
226
227 fn name(&self) -> &str;
229
230 fn priority(&self) -> u32 { 0 }
232}
233
234pub struct gRPCConfigValidator;
240
241impl ConfigValidator for gRPCConfigValidator {
242 fn validate(&self, config:&AirConfiguration) -> Result<()> {
243 if config.gRPC.BindAddress.is_empty() {
244 return Err(AirError::Configuration("gRPC bind address cannot be empty".to_string()));
245 }
246
247 if !crate::Configuration::ConfigurationManager::IsValidAddress(&config.gRPC.BindAddress) {
249 return Err(AirError::Configuration(format!(
250 "Invalid gRPC bind address '{}': must be host:port or [IPv6]:port",
251 config.gRPC.BindAddress
252 )));
253 }
254
255 if config.gRPC.MaxConnections < 10 || config.gRPC.MaxConnections > 10000 {
257 return Err(AirError::Configuration(format!(
258 "gRPC MaxConnections {} is out of range [10, 10000]",
259 config.gRPC.MaxConnections
260 )));
261 }
262
263 if config.gRPC.RequestTimeoutSecs < 1 || config.gRPC.RequestTimeoutSecs > 3600 {
265 return Err(AirError::Configuration(format!(
266 "gRPC RequestTimeoutSecs {} is out of range [1, 3600]",
267 config.gRPC.RequestTimeoutSecs
268 )));
269 }
270
271 Ok(())
272 }
273
274 fn name(&self) -> &str { "gRPCConfigValidator" }
275
276 fn priority(&self) -> u32 {
277 100 }
279}
280
281pub struct AuthConfigValidator;
283
284impl ConfigValidator for AuthConfigValidator {
285 fn validate(&self, config:&AirConfiguration) -> Result<()> {
286 if config.Authentication.Enabled {
287 if config.Authentication.CredentialsPath.is_empty() {
288 return Err(AirError::Configuration(
289 "Authentication credentials path cannot be empty when enabled".to_string(),
290 ));
291 }
292
293 if config.Authentication.CredentialsPath.contains("..") {
295 return Err(AirError::Configuration(
296 "Authentication credentials path contains '..' which is not allowed".to_string(),
297 ));
298 }
299 }
300
301 if config.Authentication.TokenExpirationHours < 1 || config.Authentication.TokenExpirationHours > 8760 {
303 return Err(AirError::Configuration(format!(
304 "Token expiration {} hours is out of range [1, 8760]",
305 config.Authentication.TokenExpirationHours
306 )));
307 }
308
309 if config.Authentication.MaxSessions < 1 || config.Authentication.MaxSessions > 1000 {
311 return Err(AirError::Configuration(format!(
312 "Max sessions {} is out of range [1, 1000]",
313 config.Authentication.MaxSessions
314 )));
315 }
316
317 Ok(())
318 }
319
320 fn name(&self) -> &str { "AuthConfigValidator" }
321
322 fn priority(&self) -> u32 {
323 90 }
325}
326
327pub struct UpdateConfigValidator;
329
330impl ConfigValidator for UpdateConfigValidator {
331 fn validate(&self, config:&AirConfiguration) -> Result<()> {
332 if config.Updates.Enabled {
333 if config.Updates.UpdateServerUrl.is_empty() {
334 return Err(AirError::Configuration(
335 "Update server URL cannot be empty when updates are enabled".to_string(),
336 ));
337 }
338
339 if !config.Updates.UpdateServerUrl.starts_with("https://") {
341 return Err(AirError::Configuration(format!(
342 "Update server URL must use HTTPS: {}",
343 config.Updates.UpdateServerUrl
344 )));
345 }
346
347 if !crate::Configuration::ConfigurationManager::IsValidUrl(&config.Updates.UpdateServerUrl) {
349 return Err(AirError::Configuration(format!(
350 "Invalid update server URL: {}",
351 config.Updates.UpdateServerUrl
352 )));
353 }
354 }
355
356 if config.Updates.CheckIntervalHours < 1 || config.Updates.CheckIntervalHours > 168 {
358 return Err(AirError::Configuration(format!(
359 "Update check interval {} hours is out of range [1, 168]",
360 config.Updates.CheckIntervalHours
361 )));
362 }
363
364 Ok(())
365 }
366
367 fn name(&self) -> &str { "UpdateConfigValidator" }
368
369 fn priority(&self) -> u32 {
370 50 }
372}
373
374pub struct DownloadConfigValidator;
376
377impl ConfigValidator for DownloadConfigValidator {
378 fn validate(&self, config:&AirConfiguration) -> Result<()> {
379 if config.Downloader.Enabled {
380 if config.Downloader.CacheDirectory.is_empty() {
381 return Err(AirError::Configuration(
382 "Download cache directory cannot be empty when enabled".to_string(),
383 ));
384 }
385
386 if config.Downloader.CacheDirectory.contains("..") {
388 return Err(AirError::Configuration(
389 "Download cache directory contains '..' which is not allowed".to_string(),
390 ));
391 }
392
393 if config.Downloader.MaxConcurrentDownloads < 1 || config.Downloader.MaxConcurrentDownloads > 50 {
395 return Err(AirError::Configuration(format!(
396 "Max concurrent downloads {} is out of range [1, 50]",
397 config.Downloader.MaxConcurrentDownloads
398 )));
399 }
400
401 if config.Downloader.DownloadTimeoutSecs < 10 || config.Downloader.DownloadTimeoutSecs > 3600 {
403 return Err(AirError::Configuration(format!(
404 "Download timeout {} seconds is out of range [10, 3600]",
405 config.Downloader.DownloadTimeoutSecs
406 )));
407 }
408
409 if config.Downloader.MaxRetries > 10 {
411 return Err(AirError::Configuration(format!(
412 "Max retries {} exceeds maximum (10)",
413 config.Downloader.MaxRetries
414 )));
415 }
416 }
417
418 Ok(())
419 }
420
421 fn name(&self) -> &str { "DownloadConfigValidator" }
422
423 fn priority(&self) -> u32 {
424 50 }
426}
427
428pub struct IndexingConfigValidator;
430
431impl ConfigValidator for IndexingConfigValidator {
432 fn validate(&self, config:&AirConfiguration) -> Result<()> {
433 if config.Indexing.Enabled {
434 if config.Indexing.IndexDirectory.is_empty() {
435 return Err(AirError::Configuration(
436 "Index directory cannot be empty when indexing is enabled".to_string(),
437 ));
438 }
439
440 if config.Indexing.IndexDirectory.contains("..") {
442 return Err(AirError::Configuration(
443 "Index directory contains '..' which is not allowed".to_string(),
444 ));
445 }
446
447 if config.Indexing.FileTypes.is_empty() {
449 return Err(AirError::Configuration(
450 "File types to index cannot be empty when indexing is enabled".to_string(),
451 ));
452 }
453
454 if config.Indexing.MaxFileSizeMb < 1 || config.Indexing.MaxFileSizeMb > 1024 {
456 return Err(AirError::Configuration(format!(
457 "Max file size {} MB is out of range [1, 1024]",
458 config.Indexing.MaxFileSizeMb
459 )));
460 }
461
462 if config.Indexing.UpdateIntervalMinutes < 1 || config.Indexing.UpdateIntervalMinutes > 1440 {
464 return Err(AirError::Configuration(format!(
465 "Index update interval {} minutes is out of range [1, 1440]",
466 config.Indexing.UpdateIntervalMinutes
467 )));
468 }
469 }
470
471 Ok(())
472 }
473
474 fn name(&self) -> &str { "IndexingConfigValidator" }
475
476 fn priority(&self) -> u32 {
477 40 }
479}
480
481pub struct LoggingConfigValidator;
483
484impl ConfigValidator for LoggingConfigValidator {
485 fn validate(&self, config:&AirConfiguration) -> Result<()> {
486 let valid_levels = ["trace", "debug", "info", "warn", "error"];
487
488 if !valid_levels.contains(&config.Logging.Level.as_str()) {
489 return Err(AirError::Configuration(format!(
490 "Invalid log level '{}': must be one of: {}",
491 config.Logging.Level,
492 valid_levels.join(", ")
493 )));
494 }
495
496 if config.Logging.MaxFileSizeMb < 1 || config.Logging.MaxFileSizeMb > 1000 {
498 return Err(AirError::Configuration(format!(
499 "Max log file size {} MB is out of range [1, 1000]",
500 config.Logging.MaxFileSizeMb
501 )));
502 }
503
504 if config.Logging.MaxFiles < 1 || config.Logging.MaxFiles > 50 {
506 return Err(AirError::Configuration(format!(
507 "Max log files {} is out of range [1, 50]",
508 config.Logging.MaxFiles
509 )));
510 }
511
512 Ok(())
513 }
514
515 fn name(&self) -> &str { "LoggingConfigValidator" }
516
517 fn priority(&self) -> u32 {
518 30 }
520}
521
522pub struct PerformanceConfigValidator;
524
525impl ConfigValidator for PerformanceConfigValidator {
526 fn validate(&self, config:&AirConfiguration) -> Result<()> {
527 if config.Performance.MemoryLimitMb < 64 || config.Performance.MemoryLimitMb > 16384 {
529 return Err(AirError::Configuration(format!(
530 "Memory limit {} MB is out of range [64, 16384]",
531 config.Performance.MemoryLimitMb
532 )));
533 }
534
535 if config.Performance.CPULimitPercent < 10 || config.Performance.CPULimitPercent > 100 {
537 return Err(AirError::Configuration(format!(
538 "CPU limit {}% is out of range [10, 100]",
539 config.Performance.CPULimitPercent
540 )));
541 }
542
543 if config.Performance.DiskLimitMb < 100 || config.Performance.DiskLimitMb > 102400 {
545 return Err(AirError::Configuration(format!(
546 "Disk limit {} MB is out of range [100, 102400]",
547 config.Performance.DiskLimitMb
548 )));
549 }
550
551 if config.Performance.BackgroundTaskIntervalSecs < 1 || config.Performance.BackgroundTaskIntervalSecs > 3600 {
553 return Err(AirError::Configuration(format!(
554 "Background task interval {} seconds is out of range [1, 3600]",
555 config.Performance.BackgroundTaskIntervalSecs
556 )));
557 }
558
559 Ok(())
560 }
561
562 fn name(&self) -> &str { "PerformanceConfigValidator" }
563
564 fn priority(&self) -> u32 {
565 20 }
567}
568
569impl ConfigHotReload {
574 pub async fn New(config_path:PathBuf, initial_config:AirConfiguration) -> Result<Self> {
585 let (change_sender, _) = broadcast::channel(100);
586 let (reload_tx, reload_rx) = mpsc::channel(100);
587
588 let manager = Self {
589 active_config:Arc::new(RwLock::new(initial_config.clone())),
590 previous_config:Arc::new(RwLock::new(None)),
591 last_config_hash:Arc::new(RwLock::new(None)),
592 config_path,
593 watcher:None,
594 change_sender,
595 reload_tx,
596 change_history:Arc::new(RwLock::new(Vec::new())),
597 last_reload:Arc::new(RwLock::new(None)),
598 last_reload_duration:Arc::new(RwLock::new(None)),
599 enabled:Arc::new(RwLock::new(true)),
600 debounce_delay:Duration::from_millis(500),
601 last_change_time:Arc::new(RwLock::new(None)),
602 stats:Arc::new(RwLock::new(ReloadStats::default())),
603 validators:Arc::new(RwLock::new(Self::DefaultValidators())),
604 max_retries:3,
605 retry_delay:Duration::from_secs(1),
606 auto_rollback_enabled:Arc::new(RwLock::new(true)),
607 };
608
609 let hash = crate::Configuration::ConfigurationManager::ComputeHash(&initial_config)?;
611 *manager.last_config_hash.write().await = Some(hash);
612
613 manager.StartReloadProcessor(reload_rx);
615
616 Ok(manager)
617 }
618
619 fn DefaultValidators() -> Vec<Box<dyn ConfigValidator>> {
621 vec![
622 Box::new(gRPCConfigValidator),
623 Box::new(AuthConfigValidator),
624 Box::new(UpdateConfigValidator),
625 Box::new(DownloadConfigValidator),
626 Box::new(IndexingConfigValidator),
627 Box::new(LoggingConfigValidator),
628 Box::new(PerformanceConfigValidator),
629 ]
630 }
631
632 pub async fn EnableFileWatching(&mut self) -> Result<()> {
634 dev_log!("config", "[HotReload] Enabling file watching for configuration changes");
635 let config_path = self.config_path.clone();
636
637 let (tx, mut rx) = tokio::sync::mpsc::channel(100);
639
640 let mut watcher = RecommendedWatcher::new(
641 move |res:NotifyResult<Event>| {
642 if let Ok(event) = res {
643 let _ = tx.blocking_send(event);
644 }
645 },
646 notify::Config::default(),
647 )
648 .map_err(|e| AirError::Configuration(format!("Failed to create file watcher: {}", e)))?;
649
650 let watch_path = if config_path.is_file() {
652 config_path.parent().unwrap_or(&config_path).to_path_buf()
653 } else {
654 config_path.clone()
655 };
656
657 watcher
658 .watch(&watch_path, RecursiveMode::NonRecursive)
659 .map_err(|e| AirError::Configuration(format!("Failed to watch path '{}': {}", watch_path.display(), e)))?;
660
661 let reload_tx = self.reload_tx.clone();
663 let config_path_clone = config_path.clone();
664
665 tokio::spawn(async move {
666 while let Some(event) = rx.recv().await {
667 dev_log!("config", "file event detected: {:?}", event.kind);
668
669 let should_reload = event
671 .paths
672 .iter()
673 .any(|p| p == &config_path_clone || p == config_path_clone.as_path())
674 && event.kind != EventKind::Access(notify::event::AccessKind::Any);
675
676 if should_reload {
677 let _ = reload_tx.send(ReloadRequest::FileChange).await;
678 }
679 }
680 });
681
682 self.watcher = Some(Arc::new(RwLock::new(watcher)));
683 *self.enabled.write().await = true;
684
685 dev_log!("config", "[HotReload] File watching enabled for: {}", config_path.display());
686 Ok(())
687 }
688
689 pub async fn DisableFileWatching(&mut self) -> Result<()> {
691 *self.enabled.write().await = false;
692
693 if let Some(watcher) = self.watcher.take() {
694 drop(watcher);
695 }
696
697 dev_log!("config", "[HotReload] File watching disabled");
698 Ok(())
699 }
700
701 fn StartReloadProcessor(&self, mut reload_rx:mpsc::Receiver<ReloadRequest>) {
703 let enabled = self.enabled.clone();
704 let debounce_delay = self.debounce_delay;
705 let last_change_time = self.last_change_time.clone();
706
707 tokio::spawn(async move {
708 while let Some(request) = reload_rx.recv().await {
709 if !*enabled.read().await {
710 continue;
711 }
712
713 let now = Instant::now();
715 {
716 let mut last_change = last_change_time.write().await;
717 if let Some(last) = *last_change {
718 if now.duration_since(last) < debounce_delay {
719 continue; }
721 }
722 *last_change = Some(now);
723 }
724
725 sleep(debounce_delay).await;
726
727 match request {
729 ReloadRequest::Manual => {
730 dev_log!("config", "[HotReload] Processing manual reload request");
731 },
732 ReloadRequest::Signal => {
733 dev_log!("config", "[HotReload] Processing signal-based reload request");
734 },
735 ReloadRequest::FileChange => {
736 dev_log!("config", "[HotReload] Processing file change reload request");
737 },
738 ReloadRequest::Periodic => {
739 dev_log!("config", "processing periodic reload check");
740 },
741 }
742 }
743 });
744 }
745
746 pub async fn Reload(&self) -> Result<()> {
748 dev_log!(
749 "config",
750 "[HotReload] Reloading configuration from: {}",
751 self.config_path.display()
752 );
753 if !*self.enabled.read().await {
755 return Err(AirError::Configuration("Hot-reload is disabled".to_string()));
756 }
757
758 let start_time = Instant::now();
759
760 {
762 let mut stats = self.stats.write().await;
763 stats.total_attempts += 1;
764 }
765
766 let mut last_error = None;
768 for attempt in 0..=self.max_retries {
769 match self.AttemptReload().await {
770 Ok(()) => {
771 let duration = start_time.elapsed();
772 *self.last_reload_duration.write().await = Some(duration);
773
774 {
776 let mut stats = self.stats.write().await;
777 stats.successful_reloads += 1;
778 stats.last_error = None;
779 }
780
781 dev_log!("config", "[HotReload] Configuration reloaded successfully in {:?}", duration);
782 return Ok(());
783 },
784 Err(e) => {
785 last_error = Some(e.clone());
786 if attempt < self.max_retries {
787 let delay = self.retry_delay * 2_u32.pow(attempt);
788 dev_log!(
789 "config",
790 "warn: [HotReload] Reload attempt {} failed, retrying in {:?}: {}",
791 attempt + 1,
792 delay,
793 e
794 );
795 sleep(delay).await;
796 }
797 },
798 }
799 }
800
801 {
803 let mut stats = self.stats.write().await;
804 stats.failed_reloads += 1;
805 stats.last_error = last_error.as_ref().map(|e| e.to_string());
806 }
807
808 let error = last_error.unwrap_or_else(|| AirError::Configuration("Unknown error".to_string()));
809
810 if *self.auto_rollback_enabled.read().await {
812 dev_log!("config", "[HotReload] Attempting rollback due to reload failure");
813 if let Err(rollback_err) = self.Rollback().await {
814 dev_log!("config", "error: [HotReload] Rollback also failed: {}", rollback_err);
815 }
816 }
817
818 Err(error)
819 }
820
821 async fn AttemptReload(&self) -> Result<()> {
823 let content = fs::read_to_string(&self.config_path).await;
825 if let Err(e) = content {
826 let mut stats = self.stats.write().await;
827 stats.parse_errors += 1;
828 return Err(AirError::Configuration(format!("Failed to read config file: {}", e)));
829 }
830 let content = content.unwrap();
831
832 let new_config:std::result::Result<AirConfiguration, toml::de::Error> = toml::from_str(&content);
833 if let Err(e) = new_config {
834 let mut stats = self.stats.write().await;
835 stats.parse_errors += 1;
836 return Err(AirError::Configuration(format!("Failed to parse config file: {}", e)));
837 }
838 let new_config = new_config.unwrap();
839
840 self.ValidateConfig(&new_config).await?;
842
843 let new_hash = crate::Configuration::ConfigurationManager::ComputeHash(&new_config)?;
845 let current_hash = self.last_config_hash.read().await.clone();
846
847 if let Some(ref hash) = current_hash {
848 if hash == &new_hash {
849 dev_log!("config", "[HotReload] Configuration unchanged, skipping reload");
850 return Ok(());
851 }
852 }
853
854 let old_config = self.active_config.read().await.clone();
856 let old_hash = current_hash;
857
858 *self.active_config.write().await = new_config.clone();
859 *self.previous_config.write().await = Some(old_config.clone());
860 *self.last_config_hash.write().await = Some(new_hash.clone());
861 *self.last_reload.write().await = Some(Utc::now());
862
863 let changes = self.ComputeChanges(&old_config, &new_config);
865
866 let record = ConfigChangeRecord {
867 timestamp:Utc::now(),
868 changes:changes.clone(),
869 validated:true,
870 reason:"Reload".to_string(),
871 rollback_performed:false,
872 };
873
874 let mut history = self.change_history.write().await;
875 history.push(record);
876
877 let history_len = history.len();
879 if history_len > 1000 {
880 history.drain(0..history_len - 1000);
881 }
882 drop(history);
883
884 let event = ConfigChangeEvent {
886 timestamp:Utc::now(),
887 old_config_hash:old_hash,
888 new_config_hash:new_hash,
889 changes,
890 success:true,
891 };
892
893 let _ = self.change_sender.send(event);
894
895 Ok(())
896 }
897
898 pub async fn ReloadAndValidate(&self) -> Result<()> { self.Reload().await }
900
901 pub async fn TriggerReload(&self) -> Result<()> {
903 self.reload_tx
904 .send(ReloadRequest::Manual)
905 .await
906 .map_err(|e| AirError::Configuration(format!("Failed to trigger reload: {}", e)))?;
907 Ok(())
908 }
909
910 async fn ValidateConfig(&self, config:&AirConfiguration) -> Result<()> {
912 let validators = self.validators.read().await;
913
914 let mut sorted_validators:Vec<_> = validators.iter().collect();
916 sorted_validators.sort_by(|a, b| b.priority().cmp(&a.priority()));
917
918 for validator in sorted_validators {
919 let result = validator.validate(config);
920 if let Err(e) = result {
921 let mut stats = self.stats.write().await;
922 stats.validation_errors += 1;
923 stats.last_error = Some(format!("{}: {}", validator.name(), e));
924 dev_log!("config", "error: [HotReload] Validation failed ({}): {}", validator.name(), e);
925 return Err(AirError::Configuration(format!("{}: {}", validator.name(), e)));
926 }
927
928 dev_log!("config", "validator '{}' passed", validator.name());
929 }
930
931 dev_log!(
932 "config",
933 "[HotReload] Configuration validation passed ({} validators)",
934 validators.len()
935 );
936 Ok(())
937 }
938
939 pub async fn RegisterValidator(&self, validator:Box<dyn ConfigValidator>) {
941 let mut validators = self.validators.write().await;
942 validators.push(validator);
943 dev_log!("config", "[HotReload] Registered validator (total: {})", validators.len());
944 }
945
946 pub async fn Rollback(&self) -> Result<()> {
948 let previous = {
949 let prev = self.previous_config.read().await.clone();
950 prev.ok_or_else(|| AirError::Configuration("No previous configuration to rollback to".to_string()))?
951 };
952
953 self.ValidateConfig(&previous).await?;
955
956 let _old_config = self.active_config.read().await.clone();
958 let old_hash = self.last_config_hash.read().await.clone();
959
960 *self.active_config.write().await = previous.clone();
961 let new_hash = crate::Configuration::ConfigurationManager::ComputeHash(&previous)?;
962 *self.last_config_hash.write().await = Some(new_hash.clone());
963
964 let record = ConfigChangeRecord {
966 timestamp:Utc::now(),
967 changes:vec![],
968 validated:true,
969 reason:"Rollback".to_string(),
970 rollback_performed:true,
971 };
972
973 {
974 let mut stats = self.stats.write().await;
975 stats.rollback_attempts += 1;
976 }
977
978 self.change_history.write().await.push(record);
979
980 let event = ConfigChangeEvent {
982 timestamp:Utc::now(),
983 old_config_hash:old_hash,
984 new_config_hash:new_hash,
985 changes:vec![],
986 success:true,
987 };
988
989 let _ = self.change_sender.send(event);
990
991 dev_log!("config", "[HotReload] Configuration rolled back successfully");
992 Ok(())
993 }
994
995 pub async fn GetConfig(&self) -> AirConfiguration { self.active_config.read().await.clone() }
997
998 pub async fn GetConfigRef(&self) -> tokio::sync::RwLockReadGuard<'_, AirConfiguration> {
1000 self.active_config.read().await
1001 }
1002
1003 pub async fn SetValue(&self, path:&str, value:&str) -> Result<()> {
1005 let mut config = self.GetConfig().await;
1006
1007 Self::SetConfigValue(&mut config, path, value)?;
1009
1010 self.ValidateConfig(&config).await?;
1012
1013 let content = toml::to_string_pretty(&config)
1015 .map_err(|e| AirError::Configuration(format!("Serialization failed: {}", e)))?;
1016
1017 fs::write(&self.config_path, content)
1018 .await
1019 .map_err(|e| AirError::Configuration(format!("Failed to write config: {}", e)))?;
1020
1021 self.Reload().await?;
1023
1024 dev_log!("config", "[HotReload] Configuration value updated: {} = {}", path, value);
1025 Ok(())
1026 }
1027
1028 pub async fn GetValue(&self, path:&str) -> Result<serde_json::Value> {
1030 let config = self.active_config.read().await;
1031 let config_json = serde_json::to_value(&*config)
1032 .map_err(|e| AirError::Configuration(format!("Serialization failed: {}", e)))?;
1033
1034 let mut current = config_json;
1035 for key in path.split('.') {
1036 current = current
1037 .get(key)
1038 .ok_or_else(|| AirError::Configuration(format!("Key not found: {}", path)))?
1039 .clone();
1040 }
1041
1042 Ok(current)
1043 }
1044
1045 fn SetConfigValue(config:&mut AirConfiguration, path:&str, value:&str) -> Result<()> {
1047 let parts:Vec<&str> = path.split('.').collect();
1048
1049 match parts.as_slice() {
1050 ["grpc", "bind_address"] => config.gRPC.BindAddress = value.to_string(),
1051 ["grpc", "max_connections"] => {
1052 config.gRPC.MaxConnections = value
1053 .parse()
1054 .map_err(|_| AirError::Configuration(format!("Invalid value: {}", value)))?;
1055 },
1056 ["grpc", "request_timeout_secs"] => {
1057 config.gRPC.RequestTimeoutSecs = value
1058 .parse()
1059 .map_err(|_| AirError::Configuration(format!("Invalid value: {}", value)))?;
1060 },
1061 ["authentication", "enabled"] => {
1062 config.Authentication.Enabled = value
1063 .parse()
1064 .map_err(|_| AirError::Configuration(format!("Invalid value: {}", value)))?;
1065 },
1066 ["authentication", "credentials_path"] => {
1067 config.Authentication.CredentialsPath = value.to_string();
1068 },
1069 ["updates", "enabled"] => {
1070 config.Updates.Enabled = value
1071 .parse()
1072 .map_err(|_| AirError::Configuration(format!("Invalid value: {}", value)))?;
1073 },
1074 ["updates", "auto_download"] => {
1075 config.Updates.AutoDownload = value
1076 .parse()
1077 .map_err(|_| AirError::Configuration(format!("Invalid value: {}", value)))?;
1078 },
1079 ["updates", "auto_install"] => {
1080 config.Updates.AutoInstall = value
1081 .parse()
1082 .map_err(|_| AirError::Configuration(format!("Invalid value: {}", value)))?;
1083 },
1084 ["downloader", "enabled"] => {
1085 config.Downloader.Enabled = value
1086 .parse()
1087 .map_err(|_| AirError::Configuration(format!("Invalid value: {}", value)))?;
1088 },
1089 ["indexing", "enabled"] => {
1090 config.Indexing.Enabled = value
1091 .parse()
1092 .map_err(|_| AirError::Configuration(format!("Invalid value: {}", value)))?;
1093 },
1094 ["logging", "level"] => {
1095 config.Logging.Level = value.to_lowercase();
1096 },
1097 ["logging", "console_enabled"] => {
1098 config.Logging.ConsoleEnabled = value
1099 .parse()
1100 .map_err(|_| AirError::Configuration(format!("Invalid value: {}", value)))?;
1101 },
1102 _ => {
1103 return Err(AirError::Configuration(format!("Unknown configuration path: {}", path)));
1104 },
1105 }
1106
1107 Ok(())
1108 }
1109
1110 fn ComputeChanges(&self, old:&AirConfiguration, new:&AirConfiguration) -> Vec<ConfigChange> {
1112 let mut changes = Vec::new();
1113
1114 let old_json = serde_json::to_value(old).unwrap_or_default();
1115 let new_json = serde_json::to_value(new).unwrap_or_default();
1116
1117 Self::DiffJson("", &old_json, &new_json, &mut changes);
1118
1119 changes
1120 }
1121
1122 fn DiffJson(prefix:&str, old:&serde_json::Value, new:&serde_json::Value, changes:&mut Vec<ConfigChange>) {
1124 match (old, new) {
1125 (serde_json::Value::Object(old_map), serde_json::Value::Object(new_map)) => {
1126 for (key, new_val) in new_map {
1127 let new_prefix = if prefix.is_empty() { key.clone() } else { format!("{}.{}", prefix, key) };
1128
1129 if let Some(old_val) = old_map.get(key) {
1130 Self::DiffJson(&new_prefix, old_val, new_val, changes);
1131 } else {
1132 changes.push(ConfigChange {
1133 path:new_prefix,
1134 old_value:serde_json::Value::Null,
1135 new_value:new_val.clone(),
1136 });
1137 }
1138 }
1139 },
1140 (old_val, new_val) if old_val != new_val => {
1141 changes.push(ConfigChange {
1142 path:prefix.to_string(),
1143 old_value:old_val.clone(),
1144 new_value:new_val.clone(),
1145 });
1146 },
1147 _ => {},
1148 }
1149 }
1150
1151 pub async fn GetChangeHistory(&self, limit:Option<usize>) -> Vec<ConfigChangeRecord> {
1153 let history = self.change_history.read().await;
1154
1155 if let Some(limit) = limit {
1156 history.iter().rev().take(limit).cloned().collect()
1157 } else {
1158 history.iter().rev().cloned().collect()
1159 }
1160 }
1161
1162 pub async fn GetLastReload(&self) -> Option<DateTime<Utc>> { *self.last_reload.read().await }
1164
1165 pub async fn GetLastReloadDuration(&self) -> Option<Duration> { *self.last_reload_duration.read().await }
1167
1168 pub async fn GetStats(&self) -> ReloadStats { self.stats.read().await.clone() }
1170
1171 pub async fn IsEnabled(&self) -> bool { *self.enabled.read().await }
1173
1174 pub async fn SetAutoRollback(&self, enabled:bool) {
1176 *self.auto_rollback_enabled.write().await = enabled;
1177 dev_log!(
1178 "config",
1179 "[HotReload] Auto-rollback {}",
1180 if enabled { "enabled" } else { "disabled" }
1181 );
1182 }
1183
1184 pub fn SubscribeChanges(&self) -> broadcast::Receiver<ConfigChangeEvent> { self.change_sender.subscribe() }
1188
1189 pub fn GetConfigPath(&self) -> &Path { &self.config_path }
1191
1192 pub async fn SetDebounceDelay(&self, delay:Duration) {
1194 dev_log!("config", "[HotReload] Debounce delay set to {:?}", delay);
1198 }
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203 use tempfile::NamedTempFile;
1204
1205 use super::*;
1206
1207 #[tokio::test]
1208 async fn test_config_hot_reload_creation() {
1209 let config = AirConfiguration::default();
1210 let temp_file = NamedTempFile::new().unwrap();
1211 let path = temp_file.path().to_path_buf();
1212
1213 let manager = ConfigHotReload::New(path, config).await.expect("Failed to create manager");
1214
1215 assert_eq!(manager.GetLastReload().await, None);
1216 assert!(
1217 !manager.GetChangeHistory(Some(10)).await.is_empty() || manager.GetChangeHistory(Some(10)).await.is_empty()
1218 );
1219 }
1220
1221 #[tokio::test]
1222 async fn test_get_set_value() {
1223 let config = AirConfiguration::default();
1224 let temp_file = NamedTempFile::new().unwrap();
1225 let path = temp_file.path().to_path_buf();
1226
1227 let content = toml::to_string_pretty(&config).unwrap();
1229 fs::write(&path, content).await.unwrap();
1230
1231 let manager = ConfigHotReload::New(path, config).await.expect("Failed to create manager");
1232
1233 let value = manager.GetValue("grpc.bind_address").await.unwrap();
1235 assert_eq!(value, "[::1]:50053");
1236 }
1237
1238 #[tokio::test]
1239 async fn test_validator_priority() {
1240 let grpc = gRPCConfigValidator;
1241 let auth = AuthConfigValidator;
1242 let perf = PerformanceConfigValidator;
1243
1244 assert!(grpc.priority() > auth.priority());
1245 assert!(auth.priority() > perf.priority());
1246 }
1247
1248 #[tokio::test]
1249 async fn test_compute_changes() {
1250 let config = AirConfiguration::default();
1251 let manager = ConfigHotReload::New(PathBuf::from("/tmp/test.toml"), config)
1252 .await
1253 .expect("Failed to create manager");
1254
1255 let mut new_config = AirConfiguration::default();
1256 new_config.gRPC.BindAddress = "[::1]:50054".to_string();
1257
1258 let changes = manager.ComputeChanges(&AirConfiguration::default(), &new_config);
1259
1260 assert!(!changes.is_empty());
1261 assert!(changes.iter().any(|c| c.path == "grpc.bind_address"));
1262 }
1263}