1mod coordinator;
13mod signals;
14mod validators;
15
16pub use coordinator::GracefulReloadCoordinator;
17pub use signals::{SignalManager, SignalType};
18pub use validators::{RouteValidator, UpstreamValidator};
19
20use arc_swap::ArcSwap;
23use notify::{Event, EventKind, RecursiveMode, Watcher};
24use std::path::{Path, PathBuf};
25use std::sync::Arc;
26use std::time::{Duration, Instant};
27use tokio::sync::{broadcast, Mutex, RwLock};
28use tracing::{debug, error, info, trace, warn};
29
30use zentinel_common::errors::{ZentinelError, ZentinelResult};
31use zentinel_config::Config;
32
33use crate::logging::{AuditLogEntry, SharedLogManager};
34use crate::tls::CertificateReloader;
35
36#[derive(Debug, Clone)]
42pub enum ReloadEvent {
43 Started {
45 timestamp: Instant,
46 trigger: ReloadTrigger,
47 },
48 Validated { timestamp: Instant },
50 Applied { timestamp: Instant, version: String },
52 Failed { timestamp: Instant, error: String },
54 RolledBack { timestamp: Instant, reason: String },
56}
57
58#[derive(Debug, Clone)]
60pub enum ReloadTrigger {
61 Manual,
63 FileChange,
65 Signal,
67 Scheduled,
69 GatewayApi,
71}
72
73#[async_trait::async_trait]
79pub trait ConfigValidator: Send + Sync {
80 async fn validate(&self, config: &Config) -> ZentinelResult<()>;
82
83 fn name(&self) -> &str;
85}
86
87#[async_trait::async_trait]
89pub trait ReloadHook: Send + Sync {
90 async fn pre_reload(&self, old_config: &Config, new_config: &Config) -> ZentinelResult<()>;
92
93 async fn post_reload(&self, old_config: &Config, new_config: &Config);
95
96 async fn on_failure(&self, config: &Config, error: &ZentinelError);
98
99 fn name(&self) -> &str;
101}
102
103#[derive(Default)]
109pub struct ReloadStats {
110 pub total_reloads: std::sync::atomic::AtomicU64,
112 pub successful_reloads: std::sync::atomic::AtomicU64,
114 pub failed_reloads: std::sync::atomic::AtomicU64,
116 pub rollbacks: std::sync::atomic::AtomicU64,
118 pub config_version: std::sync::atomic::AtomicU64,
120 pub last_success: RwLock<Option<Instant>>,
122 pub last_failure: RwLock<Option<Instant>>,
124 pub avg_duration_ms: RwLock<f64>,
126}
127
128pub struct ConfigManager {
134 current_config: Arc<ArcSwap<Config>>,
136 previous_config: Arc<RwLock<Option<Arc<Config>>>>,
138 config_path: PathBuf,
140 watcher: Arc<RwLock<Option<notify::RecommendedWatcher>>>,
142 reload_tx: broadcast::Sender<ReloadEvent>,
144 stats: Arc<ReloadStats>,
146 validators: Arc<RwLock<Vec<Box<dyn ConfigValidator>>>>,
148 reload_hooks: Arc<RwLock<Vec<Box<dyn ReloadHook>>>>,
150 cert_reloader: Arc<CertificateReloader>,
152 reload_mutex: Arc<Mutex<()>>,
154}
155
156impl ConfigManager {
157 pub async fn new(
159 config_path: impl AsRef<Path>,
160 initial_config: Config,
161 ) -> ZentinelResult<Self> {
162 let config_path = config_path.as_ref().to_path_buf();
163 let (reload_tx, _) = broadcast::channel(100);
164
165 info!(
166 config_path = %config_path.display(),
167 route_count = initial_config.routes.len(),
168 upstream_count = initial_config.upstreams.len(),
169 listener_count = initial_config.listeners.len(),
170 "Initializing configuration manager"
171 );
172
173 trace!(
174 config_path = %config_path.display(),
175 "Creating ArcSwap for configuration"
176 );
177
178 Ok(Self {
179 current_config: Arc::new(ArcSwap::from_pointee(initial_config)),
180 previous_config: Arc::new(RwLock::new(None)),
181 config_path,
182 watcher: Arc::new(RwLock::new(None)),
183 reload_tx,
184 stats: Arc::new(ReloadStats::default()),
185 validators: Arc::new(RwLock::new(Vec::new())),
186 reload_hooks: Arc::new(RwLock::new(Vec::new())),
187 cert_reloader: Arc::new(CertificateReloader::new()),
188 reload_mutex: Arc::new(Mutex::new(())),
189 })
190 }
191
192 pub fn cert_reloader(&self) -> Arc<CertificateReloader> {
194 Arc::clone(&self.cert_reloader)
195 }
196
197 pub fn current(&self) -> Arc<Config> {
199 self.current_config.load_full()
200 }
201
202 pub async fn start_watching(&self) -> ZentinelResult<()> {
208 if self.watcher.read().await.is_some() {
210 warn!("File watcher already active, skipping");
211 return Ok(());
212 }
213
214 let config_path = self.config_path.clone();
215
216 let notify = Arc::new(tokio::sync::Notify::new());
220 let notify_sender = Arc::clone(¬ify);
221
222 let watched_path = config_path.clone();
223 let mut watcher =
224 notify::recommended_watcher(move |event: Result<Event, notify::Error>| {
225 match event {
226 Ok(event) => {
227 if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) {
228 let dominated = event.paths.iter().any(|p| {
230 p == &watched_path || p.extension().is_some_and(|ext| ext == "kdl")
231 });
232 if dominated {
233 notify_sender.notify_one();
234 }
235 }
236 }
237 Err(e) => {
238 warn!(error = %e, "File watcher error");
239 }
240 }
241 })
242 .map_err(|e| ZentinelError::Config {
243 message: format!("Failed to create file watcher: {}", e),
244 source: None,
245 })?;
246
247 watcher
249 .watch(&config_path, RecursiveMode::NonRecursive)
250 .map_err(|e| ZentinelError::Config {
251 message: format!("Failed to watch config file: {}", e),
252 source: None,
253 })?;
254
255 if let Some(parent) = config_path.parent() {
259 if let Err(e) = watcher.watch(parent, RecursiveMode::Recursive) {
260 warn!(
261 path = %parent.display(),
262 error = %e,
263 "Could not watch config directory for included files, \
264 only the main config file will trigger auto-reload"
265 );
266 } else {
267 debug!(
268 path = %parent.display(),
269 "Watching config directory recursively for included file changes"
270 );
271 }
272 }
273
274 *self.watcher.write().await = Some(watcher);
276
277 let manager = Arc::new(self.clone_for_task());
279 let config_path_log = self.config_path.clone();
280 tokio::spawn(async move {
281 loop {
282 notify.notified().await;
284
285 while let Ok(()) =
289 tokio::time::timeout(Duration::from_millis(200), notify.notified()).await
290 {
291 trace!("Debounce: additional file change, resetting timer");
293 }
294
295 info!("Configuration file changed, triggering reload");
296
297 if let Err(e) = manager.reload(ReloadTrigger::FileChange).await {
298 error!(error = %e, "Auto-reload failed, continuing with current configuration");
299 }
300 }
301 });
302
303 let poll_manager = Arc::new(self.clone_for_task());
309 let poll_path = self.config_path.clone();
310 tokio::spawn(async move {
311 use std::io::Read;
312 let content_sig = |p: &std::path::Path| -> Option<(u64, Vec<u8>)> {
313 let mut f = std::fs::File::open(p).ok()?;
314 let meta = f.metadata().ok()?;
315 let len = meta.len();
316 let mut buf = vec![0u8; 256.min(len as usize)];
318 f.read_exact(&mut buf).ok()?;
319 Some((len, buf))
320 };
321 let mut last_sig = content_sig(&poll_path);
322
323 loop {
324 tokio::time::sleep(Duration::from_secs(1)).await;
325
326 let current_sig = content_sig(&poll_path);
327 if current_sig != last_sig {
328 last_sig = current_sig;
329 debug!("Config file content changed (poll fallback), triggering reload");
330 if let Err(e) = poll_manager.reload(ReloadTrigger::FileChange).await {
331 error!(error = %e, "Poll-triggered reload failed");
332 }
333 }
334 }
335 });
336
337 info!(
338 config_file = %self.config_path.display(),
339 "Auto-reload enabled: watching for configuration changes (with poll fallback)"
340 );
341 Ok(())
342 }
343
344 pub async fn reload(&self, trigger: ReloadTrigger) -> ZentinelResult<()> {
350 let _reload_guard = self.reload_mutex.lock().await;
352
353 let start = Instant::now();
354 let reload_num = self
355 .stats
356 .total_reloads
357 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
358 + 1;
359
360 info!(
361 trigger = ?trigger,
362 reload_num = reload_num,
363 config_path = %self.config_path.display(),
364 "Starting configuration reload"
365 );
366
367 let _ = self.reload_tx.send(ReloadEvent::Started {
369 timestamp: Instant::now(),
370 trigger: trigger.clone(),
371 });
372
373 trace!(
374 config_path = %self.config_path.display(),
375 "Reading configuration file"
376 );
377
378 let new_config = match Config::from_file(&self.config_path) {
380 Ok(config) => {
381 debug!(
382 route_count = config.routes.len(),
383 upstream_count = config.upstreams.len(),
384 listener_count = config.listeners.len(),
385 "Configuration file parsed successfully"
386 );
387 config
388 }
389 Err(e) => {
390 let error_msg = format!("Failed to load configuration: {}", e);
391 error!(
392 config_path = %self.config_path.display(),
393 error = %e,
394 "Failed to load configuration file"
395 );
396 self.stats
397 .failed_reloads
398 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
399 *self.stats.last_failure.write().await = Some(Instant::now());
400
401 let _ = self.reload_tx.send(ReloadEvent::Failed {
402 timestamp: Instant::now(),
403 error: error_msg.clone(),
404 });
405
406 return Err(ZentinelError::Config {
407 message: error_msg,
408 source: None,
409 });
410 }
411 };
412
413 trace!("Starting configuration validation");
414
415 if let Err(e) = self.validate_config(&new_config).await {
418 error!(
419 error = %e,
420 "Configuration validation failed - new configuration REJECTED"
421 );
422 self.stats
423 .failed_reloads
424 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
425 *self.stats.last_failure.write().await = Some(Instant::now());
426
427 let _ = self.reload_tx.send(ReloadEvent::Failed {
428 timestamp: Instant::now(),
429 error: e.to_string(),
430 });
431
432 return Err(e);
433 }
434
435 info!(
436 route_count = new_config.routes.len(),
437 upstream_count = new_config.upstreams.len(),
438 "Configuration validation passed, applying new configuration"
439 );
440
441 let _ = self.reload_tx.send(ReloadEvent::Validated {
442 timestamp: Instant::now(),
443 });
444
445 let old_config = self.current_config.load_full();
447
448 trace!(
449 old_routes = old_config.routes.len(),
450 new_routes = new_config.routes.len(),
451 "Preparing configuration swap"
452 );
453
454 let old_listeners = serde_json::to_value(&old_config.listeners).ok();
457 let new_listeners = serde_json::to_value(&new_config.listeners).ok();
458 if old_listeners != new_listeners {
459 warn!(
460 "Listener configuration changed in the new config, but listeners \
461 (addresses, ports, TLS bindings) are NOT applied by hot reload. \
462 The proxy continues serving on the previously bound listeners; \
463 restart zentinel to apply listener changes."
464 );
465 }
466 if serde_json::to_value(&old_config.server).ok()
467 != serde_json::to_value(&new_config.server).ok()
468 {
469 warn!(
470 "system/server configuration changed in the new config, but \
471 worker threads and process-level settings are NOT applied by \
472 hot reload; restart zentinel to apply them."
473 );
474 }
475
476 let hooks = self.reload_hooks.read().await;
478 for hook in hooks.iter() {
479 trace!(hook_name = %hook.name(), "Running pre-reload hook");
480 if let Err(e) = hook.pre_reload(&old_config, &new_config).await {
481 warn!(
482 hook_name = %hook.name(),
483 error = %e,
484 "Pre-reload hook failed"
485 );
486 }
488 }
489 drop(hooks);
490
491 trace!("Saving previous configuration for potential rollback");
493 *self.previous_config.write().await = Some(old_config.clone());
494
495 trace!("Applying new configuration atomically");
497 self.current_config.store(Arc::new(new_config.clone()));
498
499 let hooks = self.reload_hooks.read().await;
501 for hook in hooks.iter() {
502 trace!(hook_name = %hook.name(), "Running post-reload hook");
503 hook.post_reload(&old_config, &new_config).await;
504 }
505 drop(hooks);
506
507 let duration = start.elapsed();
509 let successful_count = self
510 .stats
511 .successful_reloads
512 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
513 + 1;
514 *self.stats.last_success.write().await = Some(Instant::now());
515
516 {
518 let mut avg = self.stats.avg_duration_ms.write().await;
519 let total = successful_count as f64;
520 *avg = (*avg * (total - 1.0) + duration.as_millis() as f64) / total;
521 }
522
523 let new_version = self
525 .stats
526 .config_version
527 .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
528 + 1;
529
530 let _ = self.reload_tx.send(ReloadEvent::Applied {
531 timestamp: Instant::now(),
532 version: format!("v{}", new_version),
533 });
534
535 let (cert_success, cert_errors) = self.cert_reloader.reload_all();
538 if !cert_errors.is_empty() {
539 for (listener_id, error) in &cert_errors {
540 error!(
541 listener_id = %listener_id,
542 error = %error,
543 "TLS certificate reload failed for listener"
544 );
545 }
546 }
547
548 info!(
549 duration_ms = duration.as_millis(),
550 successful_reloads = successful_count,
551 route_count = new_config.routes.len(),
552 upstream_count = new_config.upstreams.len(),
553 cert_reload_success = cert_success,
554 cert_reload_errors = cert_errors.len(),
555 "Configuration reload completed successfully"
556 );
557
558 Ok(())
559 }
560
561 pub async fn apply_config(
569 &self,
570 new_config: Config,
571 trigger: ReloadTrigger,
572 ) -> ZentinelResult<()> {
573 let _reload_guard = self.reload_mutex.lock().await;
575
576 let start = Instant::now();
577 let reload_num = self
578 .stats
579 .total_reloads
580 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
581 + 1;
582
583 info!(
584 trigger = ?trigger,
585 reload_num = reload_num,
586 routes = new_config.routes.len(),
587 upstreams = new_config.upstreams.len(),
588 listeners = new_config.listeners.len(),
589 "Applying programmatic configuration"
590 );
591
592 let _ = self.reload_tx.send(ReloadEvent::Started {
593 timestamp: Instant::now(),
594 trigger,
595 });
596
597 if let Err(e) = self.validate_config(&new_config).await {
599 error!(error = %e, "Programmatic configuration validation failed");
600 self.stats
601 .failed_reloads
602 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
603 *self.stats.last_failure.write().await = Some(Instant::now());
604
605 let _ = self.reload_tx.send(ReloadEvent::Failed {
606 timestamp: Instant::now(),
607 error: e.to_string(),
608 });
609
610 return Err(e);
611 }
612
613 let _ = self.reload_tx.send(ReloadEvent::Validated {
614 timestamp: Instant::now(),
615 });
616
617 let old_config = self.current_config.load_full();
619
620 let hooks = self.reload_hooks.read().await;
622 for hook in hooks.iter() {
623 if let Err(e) = hook.pre_reload(&old_config, &new_config).await {
624 warn!(hook_name = %hook.name(), error = %e, "Pre-reload hook failed");
625 }
626 }
627 drop(hooks);
628
629 *self.previous_config.write().await = Some(old_config.clone());
631
632 self.current_config.store(Arc::new(new_config.clone()));
634
635 let hooks = self.reload_hooks.read().await;
637 for hook in hooks.iter() {
638 hook.post_reload(&old_config, &new_config).await;
639 }
640 drop(hooks);
641
642 let duration = start.elapsed();
644 let successful_count = self
645 .stats
646 .successful_reloads
647 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
648 + 1;
649 *self.stats.last_success.write().await = Some(Instant::now());
650
651 {
652 let mut avg = self.stats.avg_duration_ms.write().await;
653 let total = successful_count as f64;
654 *avg = (*avg * (total - 1.0) + duration.as_millis() as f64) / total;
655 }
656
657 let new_version = self
658 .stats
659 .config_version
660 .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
661 + 1;
662
663 let _ = self.reload_tx.send(ReloadEvent::Applied {
664 timestamp: Instant::now(),
665 version: format!("v{}", new_version),
666 });
667
668 let (cert_success, cert_errors) = self.cert_reloader.reload_all();
670 if !cert_errors.is_empty() {
671 for (listener_id, error) in &cert_errors {
672 error!(
673 listener_id = %listener_id,
674 error = %error,
675 "TLS certificate reload failed for listener"
676 );
677 }
678 }
679
680 info!(
681 duration_ms = duration.as_millis(),
682 successful_reloads = successful_count,
683 route_count = new_config.routes.len(),
684 upstream_count = new_config.upstreams.len(),
685 cert_reload_success = cert_success,
686 cert_reload_errors = cert_errors.len(),
687 "Programmatic configuration applied successfully"
688 );
689
690 Ok(())
691 }
692
693 pub fn config_store(&self) -> Arc<ArcSwap<Config>> {
697 Arc::clone(&self.current_config)
698 }
699
700 pub async fn rollback(&self, reason: String) -> ZentinelResult<()> {
702 info!(
703 reason = %reason,
704 "Starting configuration rollback"
705 );
706
707 let previous = self.previous_config.read().await.clone();
708
709 if let Some(prev_config) = previous {
710 trace!(
711 route_count = prev_config.routes.len(),
712 "Found previous configuration for rollback"
713 );
714
715 trace!("Validating previous configuration");
717 if let Err(e) = self.validate_config(&prev_config).await {
718 error!(
719 error = %e,
720 "Previous configuration validation failed during rollback"
721 );
722 return Err(e);
723 }
724
725 trace!("Applying previous configuration");
727 self.current_config.store(prev_config.clone());
728 let rollback_count = self
729 .stats
730 .rollbacks
731 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
732 + 1;
733
734 let _ = self.reload_tx.send(ReloadEvent::RolledBack {
735 timestamp: Instant::now(),
736 reason: reason.clone(),
737 });
738
739 info!(
740 reason = %reason,
741 rollback_count = rollback_count,
742 route_count = prev_config.routes.len(),
743 "Configuration rolled back successfully"
744 );
745 Ok(())
746 } else {
747 warn!("No previous configuration available for rollback");
748 Err(ZentinelError::Config {
749 message: "No previous configuration available".to_string(),
750 source: None,
751 })
752 }
753 }
754
755 async fn validate_config(&self, config: &Config) -> ZentinelResult<()> {
757 trace!(
758 route_count = config.routes.len(),
759 upstream_count = config.upstreams.len(),
760 "Starting configuration validation"
761 );
762
763 trace!("Running built-in config validation");
765 config.validate()?;
766
767 let validators = self.validators.read().await;
769 trace!(
770 validator_count = validators.len(),
771 "Running custom validators"
772 );
773 for validator in validators.iter() {
774 trace!(validator_name = %validator.name(), "Running validator");
775 validator.validate(config).await.map_err(|e| {
776 error!(
777 validator_name = %validator.name(),
778 error = %e,
779 "Validator failed"
780 );
781 e
782 })?;
783 }
784
785 debug!(
786 route_count = config.routes.len(),
787 upstream_count = config.upstreams.len(),
788 "Configuration validation passed"
789 );
790
791 Ok(())
792 }
793
794 pub async fn add_validator(&self, validator: Box<dyn ConfigValidator>) {
796 info!("Adding configuration validator: {}", validator.name());
797 self.validators.write().await.push(validator);
798 }
799
800 pub async fn add_hook(&self, hook: Box<dyn ReloadHook>) {
802 info!("Adding reload hook: {}", hook.name());
803 self.reload_hooks.write().await.push(hook);
804 }
805
806 pub fn subscribe(&self) -> broadcast::Receiver<ReloadEvent> {
808 self.reload_tx.subscribe()
809 }
810
811 pub fn stats(&self) -> &ReloadStats {
813 &self.stats
814 }
815
816 fn clone_for_task(&self) -> ConfigManager {
818 ConfigManager {
819 current_config: Arc::clone(&self.current_config),
820 previous_config: Arc::clone(&self.previous_config),
821 config_path: self.config_path.clone(),
822 watcher: self.watcher.clone(),
823 reload_tx: self.reload_tx.clone(),
824 stats: Arc::clone(&self.stats),
825 validators: Arc::clone(&self.validators),
826 reload_hooks: Arc::clone(&self.reload_hooks),
827 cert_reloader: Arc::clone(&self.cert_reloader),
828 reload_mutex: Arc::clone(&self.reload_mutex),
829 }
830 }
831}
832
833pub struct AuditReloadHook {
839 log_manager: SharedLogManager,
840}
841
842impl AuditReloadHook {
843 pub fn new(log_manager: SharedLogManager) -> Self {
845 Self { log_manager }
846 }
847}
848
849#[async_trait::async_trait]
850impl ReloadHook for AuditReloadHook {
851 async fn pre_reload(&self, old_config: &Config, new_config: &Config) -> ZentinelResult<()> {
852 let trace_id = uuid::Uuid::new_v4().to_string();
854 let audit_entry = AuditLogEntry::config_change(
855 &trace_id,
856 "reload_started",
857 format!(
858 "Configuration reload starting: {} routes -> {} routes, {} upstreams -> {} upstreams",
859 old_config.routes.len(),
860 new_config.routes.len(),
861 old_config.upstreams.len(),
862 new_config.upstreams.len()
863 ),
864 );
865 self.log_manager.log_audit(&audit_entry);
866 Ok(())
867 }
868
869 async fn post_reload(&self, old_config: &Config, new_config: &Config) {
870 let trace_id = uuid::Uuid::new_v4().to_string();
872 let audit_entry = AuditLogEntry::config_change(
873 &trace_id,
874 "reload_success",
875 format!(
876 "Configuration reload successful: {} routes, {} upstreams, {} listeners",
877 new_config.routes.len(),
878 new_config.upstreams.len(),
879 new_config.listeners.len()
880 ),
881 )
882 .with_metadata("old_routes", old_config.routes.len().to_string())
883 .with_metadata("new_routes", new_config.routes.len().to_string())
884 .with_metadata("old_upstreams", old_config.upstreams.len().to_string())
885 .with_metadata("new_upstreams", new_config.upstreams.len().to_string());
886 self.log_manager.log_audit(&audit_entry);
887 }
888
889 async fn on_failure(&self, config: &Config, error: &ZentinelError) {
890 let trace_id = uuid::Uuid::new_v4().to_string();
892 let audit_entry = AuditLogEntry::config_change(
893 &trace_id,
894 "reload_failed",
895 format!("Configuration reload failed: {}", error),
896 )
897 .with_metadata("current_routes", config.routes.len().to_string())
898 .with_metadata("current_upstreams", config.upstreams.len().to_string());
899 self.log_manager.log_audit(&audit_entry);
900 }
901
902 fn name(&self) -> &str {
903 "audit_reload_hook"
904 }
905}
906
907#[cfg(test)]
908mod tests {
909 use super::*;
910
911 #[tokio::test]
912 async fn test_config_reload_rejects_invalid_config() {
913 let initial_config = Config::default_for_testing();
915 let initial_routes = initial_config.routes.len();
916
917 let temp_dir = tempfile::tempdir().unwrap();
918 let config_path = temp_dir.path().join("config.kdl");
919
920 std::fs::write(&config_path, "this is not valid KDL { {{{{ broken").unwrap();
922
923 let manager = ConfigManager::new(&config_path, initial_config)
925 .await
926 .unwrap();
927
928 assert_eq!(manager.current().routes.len(), initial_routes);
930
931 let result = manager.reload(ReloadTrigger::Manual).await;
933 assert!(result.is_err(), "Reload should fail for invalid config");
934
935 assert_eq!(
937 manager.current().routes.len(),
938 initial_routes,
939 "Original config should be preserved after failed reload"
940 );
941
942 assert_eq!(
944 manager
945 .stats()
946 .failed_reloads
947 .load(std::sync::atomic::Ordering::Relaxed),
948 1,
949 "Failed reload should be recorded"
950 );
951 }
952
953 #[tokio::test]
954 async fn test_config_reload_accepts_valid_config() {
955 let initial_config = Config::default_for_testing();
957 let temp_dir = tempfile::tempdir().unwrap();
958 let config_path = temp_dir.path().join("config.kdl");
959
960 let static_dir = temp_dir.path().join("static");
962 std::fs::create_dir_all(&static_dir).unwrap();
963
964 let valid_config = r#"
966server {
967 worker-threads 4
968}
969
970listeners {
971 listener "http" {
972 address "0.0.0.0:8080"
973 protocol "http"
974 }
975}
976
977upstreams {
978 upstream "backend" {
979 target "127.0.0.1:3000"
980 }
981}
982
983routes {
984 route "api" {
985 priority "high"
986 matches {
987 path-prefix "/api/"
988 }
989 upstream "backend"
990 }
991}
992"#;
993 std::fs::write(&config_path, valid_config).unwrap();
994
995 let manager = ConfigManager::new(&config_path, initial_config)
997 .await
998 .unwrap();
999
1000 let result = manager.reload(ReloadTrigger::Manual).await;
1002 assert!(
1003 result.is_ok(),
1004 "Reload should succeed for valid config: {:?}",
1005 result.err()
1006 );
1007
1008 assert_eq!(
1010 manager
1011 .stats()
1012 .successful_reloads
1013 .load(std::sync::atomic::Ordering::Relaxed),
1014 1,
1015 "Successful reload should be recorded"
1016 );
1017 }
1018
1019 fn write_config_with_routes(path: &Path, route_count: usize) {
1025 let mut routes = String::new();
1026 for i in 0..route_count {
1027 routes.push_str(&format!(
1028 r#"
1029 route "route{i}" {{
1030 priority "medium"
1031 matches {{
1032 path-prefix "/route{i}/"
1033 }}
1034 upstream "backend"
1035 }}
1036"#
1037 ));
1038 }
1039
1040 let config = format!(
1041 r#"
1042server {{
1043 worker-threads 4
1044}}
1045
1046listeners {{
1047 listener "http" {{
1048 address "0.0.0.0:8080"
1049 protocol "http"
1050 }}
1051}}
1052
1053upstreams {{
1054 upstream "backend" {{
1055 target "127.0.0.1:3000"
1056 }}
1057}}
1058
1059routes {{
1060{routes}
1061}}
1062"#
1063 );
1064
1065 std::fs::write(path, config).unwrap();
1066 }
1067
1068 #[tokio::test]
1069 async fn test_concurrent_config_reads_during_reload() {
1070 let initial_config = Config::default_for_testing();
1072 let temp_dir = tempfile::tempdir().unwrap();
1073 let config_path = temp_dir.path().join("config.kdl");
1074
1075 write_config_with_routes(&config_path, 5);
1076
1077 let manager = Arc::new(
1078 ConfigManager::new(&config_path, initial_config)
1079 .await
1080 .unwrap(),
1081 );
1082
1083 let mut readers = Vec::new();
1085 for _ in 0..10 {
1086 let manager_clone = Arc::clone(&manager);
1087 readers.push(tokio::spawn(async move {
1088 let mut read_count = 0;
1089 for _ in 0..100 {
1090 let config = manager_clone.current();
1091 let _ = config.routes.len();
1093 read_count += 1;
1094 tokio::task::yield_now().await;
1095 }
1096 read_count
1097 }));
1098 }
1099
1100 let manager_reload = Arc::clone(&manager);
1102 let reload_handle =
1103 tokio::spawn(async move { manager_reload.reload(ReloadTrigger::Manual).await });
1104
1105 let mut total_reads = 0;
1107 for reader in readers {
1108 total_reads += reader.await.unwrap();
1109 }
1110
1111 let reload_result = reload_handle.await.unwrap();
1112 assert!(reload_result.is_ok(), "Reload should succeed");
1113 assert_eq!(total_reads, 1000, "All reads should complete");
1114 }
1115
1116 #[tokio::test]
1117 async fn test_multiple_concurrent_reloads() {
1118 let initial_config = Config::default_for_testing();
1120 let temp_dir = tempfile::tempdir().unwrap();
1121 let config_path = temp_dir.path().join("config.kdl");
1122
1123 write_config_with_routes(&config_path, 3);
1124
1125 let manager = Arc::new(
1126 ConfigManager::new(&config_path, initial_config)
1127 .await
1128 .unwrap(),
1129 );
1130
1131 let mut reload_handles = Vec::new();
1133 for i in 0..5 {
1134 let manager_clone = Arc::clone(&manager);
1135 let trigger = if i % 2 == 0 {
1136 ReloadTrigger::Manual
1137 } else {
1138 ReloadTrigger::Signal
1139 };
1140 reload_handles.push(tokio::spawn(
1141 async move { manager_clone.reload(trigger).await },
1142 ));
1143 }
1144
1145 let mut success_count = 0;
1147 for handle in reload_handles {
1148 if handle.await.unwrap().is_ok() {
1149 success_count += 1;
1150 }
1151 }
1152
1153 assert!(success_count >= 1, "At least one reload should succeed");
1155
1156 let total = manager
1158 .stats()
1159 .total_reloads
1160 .load(std::sync::atomic::Ordering::Relaxed);
1161 assert_eq!(total, 5, "All reload attempts should be counted");
1162 }
1163
1164 #[tokio::test]
1165 async fn test_config_visibility_after_reload() {
1166 let initial_config = Config::default_for_testing();
1168 let initial_route_count = initial_config.routes.len();
1169
1170 let temp_dir = tempfile::tempdir().unwrap();
1171 let config_path = temp_dir.path().join("config.kdl");
1172
1173 write_config_with_routes(&config_path, 2);
1175
1176 let manager = ConfigManager::new(&config_path, initial_config)
1177 .await
1178 .unwrap();
1179
1180 assert_eq!(manager.current().routes.len(), initial_route_count);
1182
1183 manager.reload(ReloadTrigger::Manual).await.unwrap();
1185 assert_eq!(manager.current().routes.len(), 2);
1186
1187 write_config_with_routes(&config_path, 5);
1189 manager.reload(ReloadTrigger::Manual).await.unwrap();
1190 assert_eq!(
1191 manager.current().routes.len(),
1192 5,
1193 "New config should be visible immediately after reload"
1194 );
1195
1196 write_config_with_routes(&config_path, 1);
1198 manager.reload(ReloadTrigger::Manual).await.unwrap();
1199 assert_eq!(
1200 manager.current().routes.len(),
1201 1,
1202 "Config changes should be visible after each reload"
1203 );
1204 }
1205
1206 #[tokio::test]
1207 async fn test_rapid_successive_reloads() {
1208 let initial_config = Config::default_for_testing();
1210 let temp_dir = tempfile::tempdir().unwrap();
1211 let config_path = temp_dir.path().join("config.kdl");
1212
1213 write_config_with_routes(&config_path, 3);
1214
1215 let manager = ConfigManager::new(&config_path, initial_config)
1216 .await
1217 .unwrap();
1218
1219 for i in 0..20 {
1221 write_config_with_routes(&config_path, (i % 5) + 1);
1223 let result = manager.reload(ReloadTrigger::Manual).await;
1224 assert!(result.is_ok(), "Reload {} should succeed", i);
1225 }
1226
1227 let stats = manager.stats();
1229 assert_eq!(
1230 stats
1231 .successful_reloads
1232 .load(std::sync::atomic::Ordering::Relaxed),
1233 20,
1234 "All 20 reloads should succeed"
1235 );
1236 assert_eq!(
1237 stats
1238 .failed_reloads
1239 .load(std::sync::atomic::Ordering::Relaxed),
1240 0,
1241 "No reloads should fail"
1242 );
1243 }
1244
1245 #[tokio::test]
1246 async fn test_rollback_preserves_previous_config() {
1247 let initial_config = Config::default_for_testing();
1249 let temp_dir = tempfile::tempdir().unwrap();
1250 let config_path = temp_dir.path().join("config.kdl");
1251
1252 write_config_with_routes(&config_path, 3);
1254
1255 let manager = ConfigManager::new(&config_path, initial_config)
1256 .await
1257 .unwrap();
1258
1259 manager.reload(ReloadTrigger::Manual).await.unwrap();
1261 assert_eq!(manager.current().routes.len(), 3);
1262
1263 write_config_with_routes(&config_path, 5);
1265 manager.reload(ReloadTrigger::Manual).await.unwrap();
1266 assert_eq!(manager.current().routes.len(), 5);
1267
1268 manager
1270 .rollback("Testing rollback".to_string())
1271 .await
1272 .unwrap();
1273 assert_eq!(
1274 manager.current().routes.len(),
1275 3,
1276 "Rollback should restore previous config"
1277 );
1278
1279 assert_eq!(
1281 manager
1282 .stats()
1283 .rollbacks
1284 .load(std::sync::atomic::Ordering::Relaxed),
1285 1,
1286 "Rollback should be recorded in stats"
1287 );
1288 }
1289
1290 #[tokio::test]
1291 async fn test_reload_events_broadcast() {
1292 let initial_config = Config::default_for_testing();
1294 let temp_dir = tempfile::tempdir().unwrap();
1295 let config_path = temp_dir.path().join("config.kdl");
1296
1297 write_config_with_routes(&config_path, 2);
1298
1299 let manager = ConfigManager::new(&config_path, initial_config)
1300 .await
1301 .unwrap();
1302
1303 let mut receiver = manager.subscribe();
1305
1306 manager.reload(ReloadTrigger::Manual).await.unwrap();
1308
1309 let mut events = Vec::new();
1311 while let Ok(Ok(event)) =
1312 tokio::time::timeout(Duration::from_millis(100), receiver.recv()).await
1313 {
1314 events.push(event);
1315 }
1316
1317 assert!(
1319 events.len() >= 2,
1320 "Should receive at least Started and Applied/Validated events"
1321 );
1322
1323 assert!(
1325 events
1326 .iter()
1327 .any(|e| matches!(e, ReloadEvent::Started { .. })),
1328 "Should receive Started event"
1329 );
1330
1331 assert!(
1333 events
1334 .iter()
1335 .any(|e| matches!(e, ReloadEvent::Applied { .. })),
1336 "Should receive Applied event on success"
1337 );
1338 }
1339
1340 #[tokio::test]
1341 async fn test_graceful_coordinator_with_reload() {
1342 let coordinator = GracefulReloadCoordinator::new(Duration::from_secs(5));
1344
1345 coordinator.inc_requests();
1347 coordinator.inc_requests();
1348 coordinator.inc_requests();
1349 assert_eq!(coordinator.active_count(), 3);
1350
1351 coordinator.dec_requests();
1353 assert_eq!(coordinator.active_count(), 2);
1354
1355 let coord_clone = Arc::new(coordinator);
1357 let coord_for_drain = Arc::clone(&coord_clone);
1358 let drain_handle = tokio::spawn(async move { coord_for_drain.wait_for_drain().await });
1359
1360 tokio::time::sleep(Duration::from_millis(50)).await;
1362 coord_clone.dec_requests();
1363 tokio::time::sleep(Duration::from_millis(50)).await;
1364 coord_clone.dec_requests();
1365
1366 let drained = drain_handle.await.unwrap();
1368 assert!(drained, "All requests should drain successfully");
1369 }
1370
1371 #[tokio::test]
1372 async fn test_graceful_coordinator_drain_timeout() {
1373 let coordinator = GracefulReloadCoordinator::new(Duration::from_millis(200));
1375
1376 coordinator.inc_requests();
1378 coordinator.inc_requests();
1379
1380 let drained = coordinator.wait_for_drain().await;
1382 assert!(!drained, "Drain should timeout with stuck requests");
1383 assert_eq!(
1384 coordinator.active_count(),
1385 2,
1386 "Requests should still be tracked"
1387 );
1388 }
1389}