use std::path::PathBuf;
use std::time::Duration;
use anyhow::Result;
use notify::{Config as NotifyConfig, Event, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use tracing::{info, warn};
use crate::config;
#[cfg(test)]
#[path = "tests/config_reload_test.rs"]
mod tests;
pub async fn watch(
path: PathBuf,
shutdown: CancellationToken,
on_reload: impl Fn(config::AppConfig) + Send + 'static,
) -> Result<()> {
let (event_tx, mut event_rx) = mpsc::channel::<notify::Result<Event>>(16);
let mut watcher = RecommendedWatcher::new(
move |res: notify::Result<Event>| {
let _ = event_tx.blocking_send(res);
},
NotifyConfig::default(),
)?;
watcher.watch(&path, RecursiveMode::NonRecursive)?;
info!(path = %path.display(), "config file watch started");
loop {
tokio::select! {
event = event_rx.recv() => {
let Some(event) = event else {
info!("config watcher channel closed");
break;
};
match event {
Ok(e) if e.kind.is_modify() => {
tokio::time::sleep(Duration::from_millis(300)).await;
while event_rx.try_recv().is_ok() {}
match config::load_from_path(&path) {
Ok(new_config) => {
info!("config reloaded from {}", path.display());
on_reload(new_config);
}
Err(e) => {
warn!(error = ?e, "config reload failed — keeping current config");
}
}
}
Ok(_) => {} Err(e) => warn!(error = ?e, "config watcher error"),
}
}
_ = shutdown.cancelled() => {
info!("config watcher shutting down");
break;
}
}
}
Ok(())
}