feature_flag/
hot_reload.rs1use std::path::PathBuf;
11use std::sync::Arc;
12use std::time::{Duration, SystemTime};
13
14use tokio::fs;
15use tokio::sync::Notify;
16use tokio::task::JoinHandle;
17use tokio::time::sleep;
18
19use crate::error::FeatureFlagError;
20use crate::evaluator::FlagEvaluator;
21use crate::model::FlagSet;
22
23pub struct HotReloader {
25 handle: JoinHandle<()>,
26 stop: Arc<Notify>,
27}
28
29impl HotReloader {
30 pub async fn spawn(
33 path: impl Into<PathBuf>,
34 evaluator: FlagEvaluator,
35 interval: Duration,
36 ) -> Result<Self, FeatureFlagError> {
37 let path = path.into();
38
39 let raw = fs::read_to_string(&path)
41 .await
42 .map_err(|err| FeatureFlagError::Io {
43 path: path.clone(),
44 source: err,
45 })?;
46 let flagset = FlagSet::from_json(&raw)?;
47 evaluator.swap(flagset);
48
49 let stop = Arc::new(Notify::new());
50 let stop_clone = stop.clone();
51 let handle = tokio::spawn(async move {
52 run_loop(path, evaluator, interval, stop_clone).await;
53 });
54
55 Ok(Self { handle, stop })
56 }
57
58 pub async fn shutdown(self) {
60 self.stop.notify_waiters();
61 let _ = self.handle.await;
64 }
65}
66
67async fn run_loop(path: PathBuf, evaluator: FlagEvaluator, interval: Duration, stop: Arc<Notify>) {
68 let mut last_modified: Option<SystemTime> = read_modified(&path).await;
69
70 loop {
71 tokio::select! {
72 () = sleep(interval) => {}
73 () = stop.notified() => return,
74 }
75
76 let current = read_modified(&path).await;
77 if current.is_some() && current != last_modified {
78 last_modified = current;
79 match fs::read_to_string(&path).await {
80 Ok(raw) => match FlagSet::from_json(&raw) {
81 Ok(flagset) => evaluator.swap(flagset),
82 Err(err) => {
83 eprintln!("feature-flag hot-reload: parse error from {path:?}: {err}");
86 }
87 },
88 Err(err) => {
89 eprintln!("feature-flag hot-reload: read error from {path:?}: {err}");
90 }
91 }
92 }
93 }
94}
95
96async fn read_modified(path: &PathBuf) -> Option<SystemTime> {
97 fs::metadata(path)
98 .await
99 .ok()
100 .and_then(|m| m.modified().ok())
101}