use crate::config::Config;
use crate::error::{Error, Result};
use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ConfigChangeEvent {
Reloaded {
path: PathBuf,
timestamp: SystemTime,
},
ReloadFailed {
path: PathBuf,
error: String,
timestamp: SystemTime,
},
FileModified {
path: PathBuf,
timestamp: SystemTime,
},
FileDeleted {
path: PathBuf,
timestamp: SystemTime,
},
}
pub struct HotReloadConfig {
current: Arc<RwLock<Config>>,
file_path: PathBuf,
last_modified: SystemTime,
event_sender: Option<Sender<ConfigChangeEvent>>,
poll_interval: Duration,
}
impl HotReloadConfig {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref().to_path_buf();
let config = Config::from_file(&path)?;
let last_modified = std::fs::metadata(&path)
.map_err(|e| Error::io(path.display().to_string(), e))?
.modified()
.map_err(|e| Error::io(path.display().to_string(), e))?;
Ok(Self {
current: Arc::new(RwLock::new(config)),
file_path: path,
last_modified,
event_sender: None,
poll_interval: Duration::from_secs(1), })
}
pub fn with_poll_interval(mut self, interval: Duration) -> Self {
self.poll_interval = interval;
self
}
pub fn with_change_notifications(mut self) -> (Self, Receiver<ConfigChangeEvent>) {
let (sender, receiver) = mpsc::channel();
self.event_sender = Some(sender);
(self, receiver)
}
pub fn config(&self) -> Arc<RwLock<Config>> {
Arc::clone(&self.current)
}
pub fn snapshot(&self) -> Result<Config> {
let _config = self
.current
.read()
.map_err(|_| Error::concurrency("Failed to acquire read lock".to_string()))?;
let _content = std::fs::read_to_string(&self.file_path)
.map_err(|e| Error::io(self.file_path.display().to_string(), e))?;
Config::from_file(&self.file_path)
}
pub fn reload(&mut self) -> Result<bool> {
let metadata = std::fs::metadata(&self.file_path)
.map_err(|e| Error::io(self.file_path.display().to_string(), e))?;
let modified = metadata
.modified()
.map_err(|e| Error::io(self.file_path.display().to_string(), e))?;
if modified <= self.last_modified {
return Ok(false); }
match Config::from_file(&self.file_path) {
Ok(new_config) => {
{
let mut config = self.current.write().map_err(|_| {
Error::concurrency("Failed to acquire write lock".to_string())
})?;
*config = new_config;
}
self.last_modified = modified;
if let Some(ref sender) = self.event_sender {
let _ = sender.send(ConfigChangeEvent::Reloaded {
path: self.file_path.clone(),
timestamp: SystemTime::now(),
});
}
Ok(true)
}
Err(e) => {
if let Some(ref sender) = self.event_sender {
let _ = sender.send(ConfigChangeEvent::ReloadFailed {
path: self.file_path.clone(),
error: e.to_string(),
timestamp: SystemTime::now(),
});
}
Err(e)
}
}
}
pub fn start_watching(self) -> HotReloadHandle {
let (stop_sender, stop_receiver) = mpsc::channel();
let config_clone = Arc::clone(&self.current);
let file_path = self.file_path.clone();
let event_sender = self.event_sender.clone();
let poll_interval = self.poll_interval;
let mut last_modified = self.last_modified;
let handle = thread::spawn(move || {
loop {
if stop_receiver.try_recv().is_ok() {
break;
}
if let Ok(metadata) = std::fs::metadata(&file_path) {
if let Ok(modified) = metadata.modified() {
if modified > last_modified {
if let Some(ref sender) = event_sender {
let _ = sender.send(ConfigChangeEvent::FileModified {
path: file_path.clone(),
timestamp: SystemTime::now(),
});
}
match Config::from_file(&file_path) {
Ok(new_config) => {
if let Ok(mut config) = config_clone.write() {
*config = new_config;
last_modified = modified;
if let Some(ref sender) = event_sender {
let _ = sender.send(ConfigChangeEvent::Reloaded {
path: file_path.clone(),
timestamp: SystemTime::now(),
});
}
}
}
Err(e) => {
if let Some(ref sender) = event_sender {
let _ = sender.send(ConfigChangeEvent::ReloadFailed {
path: file_path.clone(),
error: e.to_string(),
timestamp: SystemTime::now(),
});
}
}
}
}
}
}
thread::sleep(poll_interval);
}
});
HotReloadHandle {
handle: Some(handle),
stop_sender,
}
}
pub fn file_path(&self) -> &Path {
&self.file_path
}
pub fn last_modified(&self) -> SystemTime {
self.last_modified
}
}
pub struct HotReloadHandle {
handle: Option<thread::JoinHandle<()>>,
stop_sender: Sender<()>,
}
impl HotReloadHandle {
pub fn stop(mut self) -> Result<()> {
if self.stop_sender.send(()).is_err() {
return Err(Error::concurrency("Failed to send stop signal".to_string()));
}
if let Some(handle) = self.handle.take() {
handle
.join()
.map_err(|_| Error::concurrency("Failed to join background thread".to_string()))?;
}
Ok(())
}
}
impl Drop for HotReloadHandle {
fn drop(&mut self) {
let _ = self.stop_sender.send(());
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;
#[test]
fn test_hot_reload_basic() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("test.conf");
let mut file = File::create(&config_path).unwrap();
writeln!(file, "key=value1").unwrap();
file.flush().unwrap();
drop(file);
let mut hot_config = HotReloadConfig::from_file(&config_path).unwrap();
{
let config = hot_config.config();
let config_read = config.read().unwrap();
assert_eq!(
config_read.get("key").unwrap().as_string().unwrap(),
"value1"
);
}
thread::sleep(Duration::from_millis(10));
let mut file = File::create(&config_path).unwrap();
writeln!(file, "key=value2").unwrap();
file.flush().unwrap();
drop(file);
let reloaded = hot_config.reload().unwrap();
assert!(reloaded);
{
let config = hot_config.config();
let config_read = config.read().unwrap();
assert_eq!(
config_read.get("key").unwrap().as_string().unwrap(),
"value2"
);
}
}
#[test]
fn test_hot_reload_notifications() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("test.conf");
let mut file = File::create(&config_path).unwrap();
writeln!(file, "key=value1").unwrap();
file.flush().unwrap();
drop(file);
let (mut hot_config, receiver) = HotReloadConfig::from_file(&config_path)
.unwrap()
.with_change_notifications();
thread::sleep(Duration::from_millis(10));
let mut file = File::create(&config_path).unwrap();
writeln!(file, "key=value2").unwrap();
file.flush().unwrap();
drop(file);
hot_config.reload().unwrap();
let event = receiver.try_recv().unwrap();
match event {
ConfigChangeEvent::Reloaded { path, .. } => {
assert_eq!(path, config_path);
}
_ => panic!("Expected Reloaded event"),
}
}
#[test]
fn test_automatic_watching() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("test.conf");
let mut file = File::create(&config_path).unwrap();
writeln!(file, "key=value1").unwrap();
file.flush().unwrap();
drop(file);
let (hot_config, receiver) = HotReloadConfig::from_file(&config_path)
.unwrap()
.with_poll_interval(Duration::from_millis(50))
.with_change_notifications();
let config_ref = hot_config.config();
let handle = hot_config.start_watching();
thread::sleep(Duration::from_millis(100));
let mut file = File::create(&config_path).unwrap();
writeln!(file, "key=value2").unwrap();
file.flush().unwrap();
drop(file);
thread::sleep(Duration::from_millis(200));
{
let config_read = config_ref.read().unwrap();
assert_eq!(
config_read.get("key").unwrap().as_string().unwrap(),
"value2"
);
}
let mut received_events = Vec::new();
while let Ok(event) = receiver.try_recv() {
received_events.push(event);
}
assert!(!received_events.is_empty());
let has_reloaded = received_events
.iter()
.any(|event| matches!(event, ConfigChangeEvent::Reloaded { .. }));
assert!(has_reloaded);
handle.stop().unwrap();
}
}