use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::fs;
use tokio::sync::Notify;
use tokio::task::JoinHandle;
use tokio::time::sleep;
use crate::error::FeatureFlagError;
use crate::evaluator::FlagEvaluator;
use crate::model::FlagSet;
pub struct HotReloader {
handle: JoinHandle<()>,
stop: Arc<Notify>,
}
impl HotReloader {
pub async fn spawn(
path: impl Into<PathBuf>,
evaluator: FlagEvaluator,
interval: Duration,
) -> Result<Self, FeatureFlagError> {
let path = path.into();
let raw = fs::read_to_string(&path)
.await
.map_err(|err| FeatureFlagError::Io {
path: path.clone(),
source: err,
})?;
let flagset = FlagSet::from_json(&raw)?;
evaluator.swap(flagset);
let stop = Arc::new(Notify::new());
let stop_clone = stop.clone();
let handle = tokio::spawn(async move {
run_loop(path, evaluator, interval, stop_clone).await;
});
Ok(Self { handle, stop })
}
pub async fn shutdown(self) {
self.stop.notify_waiters();
let _ = self.handle.await;
}
}
async fn run_loop(path: PathBuf, evaluator: FlagEvaluator, interval: Duration, stop: Arc<Notify>) {
let mut last_modified: Option<SystemTime> = read_modified(&path).await;
loop {
tokio::select! {
() = sleep(interval) => {}
() = stop.notified() => return,
}
let current = read_modified(&path).await;
if current.is_some() && current != last_modified {
last_modified = current;
match fs::read_to_string(&path).await {
Ok(raw) => match FlagSet::from_json(&raw) {
Ok(flagset) => evaluator.swap(flagset),
Err(err) => {
eprintln!("feature-flag hot-reload: parse error from {path:?}: {err}");
}
},
Err(err) => {
eprintln!("feature-flag hot-reload: read error from {path:?}: {err}");
}
}
}
}
}
async fn read_modified(path: &PathBuf) -> Option<SystemTime> {
fs::metadata(path)
.await
.ok()
.and_then(|m| m.modified().ok())
}