featherbit/hot_reload/mod.rs
1//! Hot-reload of the gateway configuration: watches `gateway.yaml` for
2//! changes (via the `notify` crate) and triggers a `SharedState` reload so
3//! route and policy edits apply without restarting the process.
4
5use std::path::PathBuf;
6use std::sync::Arc;
7use std::time::Duration;
8
9use notify::{Event, EventKind, RecursiveMode, Watcher};
10use tokio::sync::mpsc;
11use tracing::{error, info};
12
13use crate::state::SharedState;
14
15/// Watches config files for changes and triggers hot-reload.
16///
17/// Spawns a dedicated OS thread running a `notify` watcher on the config
18/// file's parent directory (recursively), forwarding modify/create events
19/// over a channel to this async loop. Events are debounced: after the first
20/// event the loop waits 500ms and drains any further events, so a burst of
21/// filesystem notifications (as editors typically produce) results in a
22/// single `SharedState::reload_from_disk` call. Reload failures are logged
23/// and leave the previously loaded configuration in place. Runs until the
24/// event channel closes; intended to be spawned as a long-lived task.
25pub async fn watch_config(state: Arc<SharedState>, config_path: PathBuf) {
26 let (tx, mut rx) = mpsc::channel::<()>(1);
27
28 let path = config_path.clone();
29 std::thread::spawn(move || {
30 let rt_tx = tx.clone();
31 let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
32 if let Ok(event) = res {
33 match event.kind {
34 EventKind::Modify(_) | EventKind::Create(_) => {
35 let _ = rt_tx.blocking_send(());
36 }
37 _ => {}
38 }
39 }
40 })
41 .expect("Failed to create file watcher");
42
43 // Watch the parent directory of the config file
44 let watch_dir = path.parent().unwrap_or(&path);
45 watcher
46 .watch(watch_dir, RecursiveMode::Recursive)
47 .expect("Failed to watch config directory");
48
49 info!("File watcher started on {:?}", watch_dir);
50
51 // Keep the watcher alive
52 loop {
53 std::thread::sleep(Duration::from_secs(3600));
54 }
55 });
56
57 // Debounce: wait for changes, batch them, then reload
58 loop {
59 if rx.recv().await.is_none() {
60 break;
61 }
62
63 // Debounce: drain any additional events within 500ms
64 tokio::time::sleep(Duration::from_millis(500)).await;
65 while rx.try_recv().is_ok() {}
66
67 info!("Config change detected, reloading...");
68 match state.reload_from_disk().await {
69 Ok(_) => info!("Config reloaded successfully"),
70 Err(e) => error!("Config reload failed: {}", e),
71 }
72 }
73}