use crate::Result;
use crate::config::Config;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, info};
pub struct ConfigReloader {
config_path: PathBuf,
config: Arc<RwLock<Config>>,
}
impl ConfigReloader {
pub fn new(config_path: impl AsRef<Path>, config: Arc<RwLock<Config>>) -> Self {
Self {
config_path: config_path.as_ref().to_path_buf(),
config,
}
}
pub async fn start_watching(self) -> Result<()> {
let path = self.config_path.clone();
let config = Arc::clone(&self.config);
crate::utils::spawn_file_watcher(
format!("config-reloader:{}", path.display()),
vec![path.clone()],
500, move |p, _files| {
let p = p.to_path_buf();
let config = Arc::clone(&config);
tokio::spawn(async move {
info!("Config file changed ({:?}), reloading...", p);
match ConfigReloader::reload_config(&p, &config).await {
Ok(()) => info!("Configuration reloaded successfully"),
Err(e) => error!("Failed to reload configuration: {}", e),
}
});
},
);
Ok(())
}
async fn reload_config(path: &Path, config: &Arc<RwLock<Config>>) -> Result<()> {
let new_config = crate::config::loader::load_from_file(path)?;
new_config.validate()?;
let mut config_guard = config.write().await;
*config_guard = new_config;
Ok(())
}
pub async fn reload(&self) -> Result<()> {
info!("Manual configuration reload triggered");
Self::reload_config(&self.config_path, &self.config).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[tokio::test]
async fn test_config_reloader_creation() {
let config = Arc::new(RwLock::new(Config::new()));
let reloader = ConfigReloader::new("test.yaml", config);
assert_eq!(reloader.config_path, PathBuf::from("test.yaml"));
}
#[tokio::test]
async fn test_manual_reload() {
let mut temp_file = NamedTempFile::new().unwrap();
let config_content = r#"
log:
level: info
server:
timeout_secs: 5
"#;
write!(temp_file, "{}", config_content).unwrap();
temp_file.flush().unwrap();
let config = Arc::new(RwLock::new(Config::new()));
let reloader = ConfigReloader::new(temp_file.path(), Arc::clone(&config));
let result = reloader.reload().await;
assert!(result.is_ok());
let config_guard = config.read().await;
assert_eq!(config_guard.log.level, "info");
}
#[tokio::test]
async fn test_reload_invalid_config() {
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "invalid: yaml: {{").unwrap();
temp_file.flush().unwrap();
let config = Arc::new(RwLock::new(Config::new()));
let reloader = ConfigReloader::new(temp_file.path(), config);
let result = reloader.reload().await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_start_watching_detects_change() {
use tokio::time::Duration;
let mut temp_file = NamedTempFile::new().unwrap();
let initial = r#"
log:
level: info
server:
timeout_secs: 5
"#;
write!(temp_file, "{}", initial).unwrap();
temp_file.flush().unwrap();
let config = Arc::new(RwLock::new(Config::new()));
let reloader = ConfigReloader::new(temp_file.path(), Arc::clone(&config));
reloader.start_watching().await.unwrap();
tokio::time::sleep(Duration::from_millis(300)).await;
let new_content = r#"
log:
level: debug
server:
timeout_secs: 5
"#;
let dir = temp_file.path().parent().unwrap();
let mut success = false;
let start = std::time::Instant::now();
while start.elapsed() < Duration::from_secs(15) {
if let Ok(mut f) = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(temp_file.path())
{
use std::io::Write as _;
let _ = f.write_all(new_content.as_bytes());
let _ = f.sync_all();
}
if let Ok(mut replace) = NamedTempFile::new_in(dir) {
write!(replace, "{}", new_content).unwrap();
replace.flush().unwrap();
let _ = std::fs::rename(replace.path(), temp_file.path());
}
for _ in 0..40 {
{
let guard = config.read().await;
if guard.log.level == "debug" {
success = true;
break;
}
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
if success {
break;
}
}
assert!(success, "timeout waiting for config reload");
let guard = config.read().await;
assert_eq!(guard.log.level, "debug");
}
}