Skip to main content

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}