ricecoder_ide/
hot_reload.rs

1//! Hot-reload support for configuration and provider availability changes
2//!
3//! This module provides file watching and change detection for configuration files,
4//! as well as callbacks for provider availability changes. It enables runtime updates
5//! without requiring application restart.
6
7use crate::error::{IdeError, IdeResult};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use tokio::sync::RwLock;
11use tracing::{debug, info, warn};
12
13/// Callback type for configuration changes
14pub type ConfigChangeCallback = Box<dyn Fn() + Send + Sync>;
15
16/// Callback type for provider availability changes
17pub type ProviderAvailabilityCallback = Box<dyn Fn(&str, bool) + Send + Sync>;
18
19/// Configuration hot-reload manager
20pub struct HotReloadManager {
21    /// Path to the configuration file being watched
22    config_path: PathBuf,
23    /// Last known modification time
24    last_modified: Arc<RwLock<Option<std::time::SystemTime>>>,
25    /// Configuration change callbacks
26    config_callbacks: Arc<RwLock<Vec<Arc<ConfigChangeCallback>>>>,
27    /// Provider availability callbacks
28    provider_callbacks: Arc<RwLock<Vec<Arc<ProviderAvailabilityCallback>>>>,
29}
30
31impl HotReloadManager {
32    /// Create a new hot-reload manager
33    pub fn new(config_path: impl AsRef<Path>) -> Self {
34        HotReloadManager {
35            config_path: config_path.as_ref().to_path_buf(),
36            last_modified: Arc::new(RwLock::new(None)),
37            config_callbacks: Arc::new(RwLock::new(Vec::new())),
38            provider_callbacks: Arc::new(RwLock::new(Vec::new())),
39        }
40    }
41
42    /// Register a callback for configuration changes
43    pub async fn on_config_change(&self, callback: ConfigChangeCallback) -> IdeResult<()> {
44        debug!("Registering configuration change callback");
45        let mut callbacks = self.config_callbacks.write().await;
46        callbacks.push(Arc::new(callback));
47        Ok(())
48    }
49
50    /// Register a callback for provider availability changes
51    pub async fn on_provider_availability_change(
52        &self,
53        callback: ProviderAvailabilityCallback,
54    ) -> IdeResult<()> {
55        debug!("Registering provider availability change callback");
56        let mut callbacks = self.provider_callbacks.write().await;
57        callbacks.push(Arc::new(callback));
58        Ok(())
59    }
60
61    /// Check if configuration file has changed
62    pub async fn check_config_changed(&self) -> IdeResult<bool> {
63        let metadata = tokio::fs::metadata(&self.config_path)
64            .await
65            .map_err(|e| {
66                IdeError::config_error(format!(
67                    "Failed to check configuration file metadata: {}",
68                    e
69                ))
70            })?;
71
72        let modified = metadata.modified().map_err(|e| {
73            IdeError::config_error(format!(
74                "Failed to get file modification time: {}",
75                e
76            ))
77        })?;
78
79        let mut last_modified = self.last_modified.write().await;
80        let changed = match *last_modified {
81            None => {
82                *last_modified = Some(modified);
83                true
84            }
85            Some(prev) => {
86                if modified > prev {
87                    *last_modified = Some(modified);
88                    true
89                } else {
90                    false
91                }
92            }
93        };
94
95        if changed {
96            debug!("Configuration file has changed");
97        }
98
99        Ok(changed)
100    }
101
102    /// Notify all configuration change callbacks
103    pub async fn notify_config_changed(&self) -> IdeResult<()> {
104        info!("Notifying configuration change callbacks");
105        let callbacks = self.config_callbacks.read().await;
106        for callback in callbacks.iter() {
107            callback();
108        }
109        Ok(())
110    }
111
112    /// Notify all provider availability change callbacks
113    pub async fn notify_provider_availability_changed(
114        &self,
115        language: &str,
116        available: bool,
117    ) -> IdeResult<()> {
118        info!(
119            "Notifying provider availability change: {} (available: {})",
120            language, available
121        );
122        let callbacks = self.provider_callbacks.read().await;
123        for callback in callbacks.iter() {
124            callback(language, available);
125        }
126        Ok(())
127    }
128
129    /// Start watching configuration file for changes
130    pub async fn start_watching(&self, check_interval_ms: u64) -> IdeResult<()> {
131        info!(
132            "Starting configuration file watcher with {}ms interval",
133            check_interval_ms
134        );
135
136        let config_path = self.config_path.clone();
137        let last_modified = self.last_modified.clone();
138        let config_callbacks = self.config_callbacks.clone();
139
140        tokio::spawn(async move {
141            loop {
142                tokio::time::sleep(tokio::time::Duration::from_millis(check_interval_ms)).await;
143
144                match tokio::fs::metadata(&config_path).await {
145                    Ok(metadata) => {
146                        if let Ok(modified) = metadata.modified() {
147                            let mut last_mod = last_modified.write().await;
148                            if let Some(prev) = *last_mod {
149                                if modified > prev {
150                                    *last_mod = Some(modified);
151                                    info!("Configuration file changed, notifying callbacks");
152                                    let callbacks = config_callbacks.read().await;
153                                    for callback in callbacks.iter() {
154                                        callback();
155                                    }
156                                }
157                            } else {
158                                *last_mod = Some(modified);
159                            }
160                        }
161                    }
162                    Err(e) => {
163                        warn!("Failed to check configuration file: {}", e);
164                    }
165                }
166            }
167        });
168
169        Ok(())
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use std::sync::atomic::{AtomicUsize, Ordering};
177
178    #[tokio::test]
179    async fn test_hot_reload_manager_creation() {
180        let manager = HotReloadManager::new("/tmp/config.yaml");
181        assert_eq!(manager.config_path, PathBuf::from("/tmp/config.yaml"));
182    }
183
184    #[tokio::test]
185    async fn test_register_config_change_callback() {
186        let manager = HotReloadManager::new("/tmp/config.yaml");
187        let callback = Box::new(|| {});
188        assert!(manager.on_config_change(callback).await.is_ok());
189    }
190
191    #[tokio::test]
192    async fn test_register_provider_availability_callback() {
193        let manager = HotReloadManager::new("/tmp/config.yaml");
194        let callback = Box::new(|_: &str, _: bool| {});
195        assert!(manager.on_provider_availability_change(callback).await.is_ok());
196    }
197
198    #[tokio::test]
199    async fn test_notify_config_changed() {
200        let manager = HotReloadManager::new("/tmp/config.yaml");
201        let call_count = Arc::new(AtomicUsize::new(0));
202        let count_clone = call_count.clone();
203
204        let callback = Box::new(move || {
205            count_clone.fetch_add(1, Ordering::SeqCst);
206        });
207
208        manager.on_config_change(callback).await.unwrap();
209        manager.notify_config_changed().await.unwrap();
210
211        assert_eq!(call_count.load(Ordering::SeqCst), 1);
212    }
213
214    #[tokio::test]
215    async fn test_notify_provider_availability_changed() {
216        let manager = HotReloadManager::new("/tmp/config.yaml");
217        let call_count = Arc::new(AtomicUsize::new(0));
218        let count_clone = call_count.clone();
219
220        let callback = Box::new(move |_: &str, _: bool| {
221            count_clone.fetch_add(1, Ordering::SeqCst);
222        });
223
224        manager
225            .on_provider_availability_change(callback)
226            .await
227            .unwrap();
228        manager
229            .notify_provider_availability_changed("rust", true)
230            .await
231            .unwrap();
232
233        assert_eq!(call_count.load(Ordering::SeqCst), 1);
234    }
235
236    #[tokio::test]
237    async fn test_multiple_callbacks() {
238        let manager = HotReloadManager::new("/tmp/config.yaml");
239        let count1 = Arc::new(AtomicUsize::new(0));
240        let count2 = Arc::new(AtomicUsize::new(0));
241
242        let c1 = count1.clone();
243        manager
244            .on_config_change(Box::new(move || {
245                c1.fetch_add(1, Ordering::SeqCst);
246            }))
247            .await
248            .unwrap();
249
250        let c2 = count2.clone();
251        manager
252            .on_config_change(Box::new(move || {
253                c2.fetch_add(1, Ordering::SeqCst);
254            }))
255            .await
256            .unwrap();
257
258        manager.notify_config_changed().await.unwrap();
259
260        assert_eq!(count1.load(Ordering::SeqCst), 1);
261        assert_eq!(count2.load(Ordering::SeqCst), 1);
262    }
263}