Skip to main content

AirLibrary/Configuration/
HotReload.rs

1//! # Configuration Hot-Reload System
2//!
3//! This module provides live configuration reloading capabilities with
4//! comprehensive error handling, validation, atomic updates, and rollback
5//! support for the Air daemon.
6//!
7//! ## Features
8//!
9//! - **File System Monitoring**: Real-time detection of configuration file
10//!   changes
11//! - **Signal Handling**: SIGHUP support for manual configuration reload
12//!   triggers
13//! - **Atomic Swaps**: Thread-safe configuration updates without service
14//!   interruption
15//! - **Automatic Rollback**: Revert to previous configuration on validation
16//!   failure
17//! - **Change Tracking**: Detailed audit trail of all configuration changes
18//! - **Validation Pipeline**: Multi-stage validation with custom validators
19//! - **Retry Logic**: Automatic retry with exponential backoff on transient
20//!   failures
21//! - **Notification System**: Callback system for configuration change events
22//! - **Graceful Degradation**: System continues operating even if hot-reload
23//!   fails
24//!
25//! ## Integration with Configuration System
26//!
27//! The hot-reload system works in tandem with the main configuration module:
28//! - Uses same validation logic from Configuration module
29//! - Shares configuration schema and structure
30//! - Provides runtime updates without requiring service restart
31//! - Scales horizontally across multiple Air instances
32//!
33//! ## Connection to Mountain and Wind Services
34//!
35//! Configuration changes detected by hot-reload are propagated to:
36//! - Mountain: User settings synchronized in real-time
37//! - Wind: All background services notified of configuration updates
38//! - VSCode: Frontend receives configuration change events
39//!
40//! ## Signal Handling
41//!
42//! Supports the following Unix signals for manual control:
43//! - `SIGHUP`: Force configuration reload from disk
44//! - `SIGUSR1`: Hot-reload status information
45//! - `SIGUSR2`: Disable/enable hot-reload monitoring
46//!
47//! ## Notification Flow
48//!
49//! ```text
50//! Config file changed → File watcher detected → Load & Validate
51//! ↓ ↓
52//! Atomic swap ←- Validation passed ←-- Migration applied
53//! ↓
54//! Notify subscribers → Wind services update → Mountain sync
55//! ↓
56//! Change history logged → Rollback state updated
57//! ```
58//!
59//! ## Error Recovery
60//!
61//! The system implements a robust error recovery strategy:
62//! - Validation failures: Automatic rollback to previous valid configuration
63//! - Parse errors: Keep existing configuration, log error, continue monitoring
64//! - File system errors: Temporary pause in monitoring, retry with backoff
65//! - Concurrent modifications: Use atomic file operations, retry on conflict
66//!
67//! ## Performance Considerations
68//!
69//! - Debouncing: Multiple rapid changes trigger single reload after cooldown
70//! - Async operations: Non-blocking file I/O and validation
71//! - Lock-free reads: Configuration reads don't block other operations
72//! - Efficient diffing: Only process changed configuration sections
73//!
74//! ## Future Enhancements
75//!
76//! The following features are planned for production deployments:
77//!
78//! - **Distributed synchronization**: Configuration changes propagated across
79//!   multiple Air instances via a consensus algorithm (Raft/Paxos) or
80//!   centralized configuration store
81//!
82//! - **Change broadcasting**: Real-time notification to connected Wind
83//!   (Mountain) services via gRPC streaming or WebSocket push subscriptions
84//!
85//! - **Conflict resolution**: Multi-master scenarios with automatic merge
86//!   strategies and version vectors to detect and resolve concurrent
87//!   modifications
88//!
89//! These features require additional infrastructure and are not required for
90//! basic hot reload functionality.
91
92use 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
109// =============================================================================
110// Configuration Hot-Reload Manager
111// =============================================================================
112
113/// Configuration hot-reload manager with file watching and validation
114pub struct ConfigHotReload {
115	/// Current active configuration
116	active_config:Arc<RwLock<AirConfiguration>>,
117
118	/// Previous configuration for rollback
119	previous_config:Arc<RwLock<Option<AirConfiguration>>>,
120
121	/// Last successful configuration hash
122	last_config_hash:Arc<RwLock<Option<String>>>,
123
124	/// Configuration file path
125	config_path:PathBuf,
126
127	/// File watcher for monitoring changes
128	watcher:Option<Arc<RwLock<notify::RecommendedWatcher>>>,
129
130	/// Change notification sender for subscribers
131	change_sender:broadcast::Sender<ConfigChangeEvent>,
132
133	/// Reload request channel (for signal handling and manual triggers)
134	reload_tx:mpsc::Sender<ReloadRequest>,
135
136	/// Change history for auditing
137	change_history:Arc<RwLock<Vec<ConfigChangeRecord>>>,
138
139	/// Last reload timestamp
140	last_reload:Arc<RwLock<Option<DateTime<Utc>>>>,
141
142	/// Last reload duration
143	last_reload_duration:Arc<RwLock<Option<Duration>>>,
144
145	/// Whether hot-reload is enabled
146	enabled:Arc<RwLock<bool>>,
147
148	/// Reload debounce delay to prevent rapid successive reloads
149	debounce_delay:Duration,
150
151	/// Last file change timestamp (for debouncing)
152	last_change_time:Arc<RwLock<Option<Instant>>>,
153
154	/// Reload statistics
155	stats:Arc<RwLock<ReloadStats>>,
156
157	/// Validation callbacks
158	validators:Arc<RwLock<Vec<Box<dyn ConfigValidator>>>>,
159
160	/// Maximum retry attempts for failed reloads
161	max_retries:u32,
162
163	/// Retry delay with exponential backoff
164	retry_delay:Duration,
165
166	/// Whether automatic rollback is enabled on validation failure
167	auto_rollback_enabled:Arc<RwLock<bool>>,
168}
169
170/// Configuration change event for subscribers
171#[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
180/// Reload request from external sources
181pub enum ReloadRequest {
182	/// Manual reload request
183	Manual,
184	/// Signal-based reload (SIGHUP)
185	Signal,
186	/// File change detected
187	FileChange,
188	/// Periodic health check reload
189	Periodic,
190}
191
192/// Reload statistics for monitoring
193#[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/// Configuration change record
205#[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/// Individual configuration change
215#[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
222/// Configuration validation trait
223pub trait ConfigValidator: Send + Sync {
224	/// Validate a configuration
225	fn validate(&self, config:&AirConfiguration) -> Result<()>;
226
227	/// Get validator name
228	fn name(&self) -> &str;
229
230	/// Get priority (higher validators run first)
231	fn priority(&self) -> u32 { 0 }
232}
233
234// =============================================================================
235// Configuration Validators
236// =============================================================================
237
238/// Validator for GRPC configuration
239pub 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		// Validate address format
248		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		// Validate range [10, 10000]
256		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		// Validate range [1, 3600]
264		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 // High priority - network configuration is critical
278	}
279}
280
281/// Validator for authentication configuration
282pub 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			// Validate path security
294			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		// Validate range [1, 8760]
302		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		// Validate range [1, 1000]
310		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 // High priority - security configuration
324	}
325}
326
327/// Validator for update configuration
328pub 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			// Must be HTTPS
340			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			// Validate URL format
348			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		// Validate range [1, 168]
357		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 // Medium priority
371	}
372}
373
374/// Validator for downloader configuration
375pub 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			// Validate path security
387			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			// Validate range [1, 50]
394			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			// Validate range [10, 3600]
402			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			// Validate range [0, 10]
410			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 // Medium priority
425	}
426}
427
428/// Validator for indexing configuration
429pub 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			// Validate path security
441			if config.Indexing.IndexDirectory.contains("..") {
442				return Err(AirError::Configuration(
443					"Index directory contains '..' which is not allowed".to_string(),
444				));
445			}
446
447			// Validate file types is not empty
448			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			// Validate range [1, 1024]
455			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			// Validate range [1, 1440]
463			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 // Lower priority
478	}
479}
480
481/// Validator for logging configuration
482pub 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		// Validate range [1, 1000]
497		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		// Validate range [1, 50]
505		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 // Lower priority
519	}
520}
521
522/// Validator for performance configuration
523pub struct PerformanceConfigValidator;
524
525impl ConfigValidator for PerformanceConfigValidator {
526	fn validate(&self, config:&AirConfiguration) -> Result<()> {
527		// Validate range [64, 16384]
528		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		// Validate range [10, 100]
536		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		// Validate range [100, 102400]
544		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		// Validate range [1, 3600]
552		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 // Lowest priority
566	}
567}
568
569// =============================================================================
570// Implementation
571// =============================================================================
572
573impl ConfigHotReload {
574	/// Create a new hot-reload manager
575	///
576	/// # Arguments
577	///
578	/// * `config_path` - Path to the configuration file to monitor
579	/// * `initial_config` - Initial configuration to load
580	///
581	/// # Returns
582	///
583	/// New ConfigHotReload instance with validation chain initialized
584	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		// Initialize last config hash
610		let hash = crate::Configuration::ConfigurationManager::ComputeHash(&initial_config)?;
611		*manager.last_config_hash.write().await = Some(hash);
612
613		// Start reload request processor
614		manager.StartReloadProcessor(reload_rx);
615
616		Ok(manager)
617	}
618
619	/// Get the default set of validators
620	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	/// Enable file watching for configuration changes
633	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		// Create watcher
638		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		// Watch the configuration file's directory
651		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		// Start event processing task
662		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				// Check if the event is for our config file
670				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	/// Disable file watching
690	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	/// Start the reload request processor
702	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				// Debounce: wait before processing the request
714				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; // Skip, too soon since last change
720						}
721					}
722					*last_change = Some(now);
723				}
724
725				sleep(debounce_delay).await;
726
727				// Process the reload
728				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	/// Reload configuration from file with retry logic and rollback support
747	pub async fn Reload(&self) -> Result<()> {
748		dev_log!(
749			"config",
750			"[HotReload] Reloading configuration from: {}",
751			self.config_path.display()
752		);
753		// Check if enabled
754		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		// Update statistics
761		{
762			let mut stats = self.stats.write().await;
763			stats.total_attempts += 1;
764		}
765
766		// Retry logic
767		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					// Update success statistics
775					{
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		// All attempts failed
802		{
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		// Attempt rollback if enabled
811		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	/// Attempt to reload configuration (single attempt)
822	async fn AttemptReload(&self) -> Result<()> {
823		// Load new configuration
824		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		// Validate new configuration
841		self.ValidateConfig(&new_config).await?;
842
843		// Check for actual changes
844		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		// Atomically swap configurations
855		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		// Record changes
864		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		// Limit history size
878		let history_len = history.len();
879		if history_len > 1000 {
880			history.drain(0..history_len - 1000);
881		}
882		drop(history);
883
884		// Send change notification
885		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	/// Reload and validate configuration (alias for Reload)
899	pub async fn ReloadAndValidate(&self) -> Result<()> { self.Reload().await }
900
901	/// Trigger a manual reload
902	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	/// Validate configuration using all registered validators
911	async fn ValidateConfig(&self, config:&AirConfiguration) -> Result<()> {
912		let validators = self.validators.read().await;
913
914		// Sort validators by priority (higher first)
915		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	/// Register a custom validator
940	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	/// Rollback to previous configuration
947	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		// Validate previous configuration
954		self.ValidateConfig(&previous).await?;
955
956		// Perform rollback
957		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		// Record rollback
965		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		// Send change notification
981		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	/// Get current configuration
996	pub async fn GetConfig(&self) -> AirConfiguration { self.active_config.read().await.clone() }
997
998	/// Get current configuration (read-only, non-copying)
999	pub async fn GetConfigRef(&self) -> tokio::sync::RwLockReadGuard<'_, AirConfiguration> {
1000		self.active_config.read().await
1001	}
1002
1003	/// Set configuration value by path (e.g., "grpc.bind_address")
1004	pub async fn SetValue(&self, path:&str, value:&str) -> Result<()> {
1005		let mut config = self.GetConfig().await;
1006
1007		// Parse and update value
1008		Self::SetConfigValue(&mut config, path, value)?;
1009
1010		// Validate
1011		self.ValidateConfig(&config).await?;
1012
1013		// Save to file
1014		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		// Trigger reload
1022		self.Reload().await?;
1023
1024		dev_log!("config", "[HotReload] Configuration value updated: {} = {}", path, value);
1025		Ok(())
1026	}
1027
1028	/// Get configuration value by path
1029	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	/// Set a nested configuration value
1046	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	/// Compute configuration changes
1111	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	/// Recursively diff JSON objects
1123	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	/// Get change history
1152	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	/// Get last reload timestamp
1163	pub async fn GetLastReload(&self) -> Option<DateTime<Utc>> { *self.last_reload.read().await }
1164
1165	/// Get last reload duration
1166	pub async fn GetLastReloadDuration(&self) -> Option<Duration> { *self.last_reload_duration.read().await }
1167
1168	/// Get reload statistics
1169	pub async fn GetStats(&self) -> ReloadStats { self.stats.read().await.clone() }
1170
1171	/// Check if hot-reload is enabled
1172	pub async fn IsEnabled(&self) -> bool { *self.enabled.read().await }
1173
1174	/// Set whether auto-rollback is enabled
1175	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	/// Get configuration change event receiver
1185	///
1186	/// This can be used to subscribe to configuration change notifications
1187	pub fn SubscribeChanges(&self) -> broadcast::Receiver<ConfigChangeEvent> { self.change_sender.subscribe() }
1188
1189	/// Get configuration path
1190	pub fn GetConfigPath(&self) -> &Path { &self.config_path }
1191
1192	/// Set debounce delay
1193	pub async fn SetDebounceDelay(&self, delay:Duration) {
1194		// For now, just log that debounce delay would be changed
1195		// In a proper implementation, we'd make debounce_delay mutable or use
1196		// Arc<RwLock<Duration>>
1197		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		// Write initial config
1228		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		// Test getting value
1234		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}