use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::RwLock;
use tokio::time::interval;
use tracing::{debug, info, warn};
use crate::indexing::facade::IndexFacade;
use crate::mcp::notifications::{FileChangeEvent, NotificationBroadcaster};
use crate::{IndexPersistence, Settings};
pub struct HotReloadWatcher {
index_path: PathBuf,
facade: Arc<RwLock<IndexFacade>>,
settings: Arc<Settings>,
persistence: IndexPersistence,
last_modified: Option<SystemTime>,
last_doc_modified: Option<SystemTime>,
check_interval: Duration,
broadcaster: Option<Arc<NotificationBroadcaster>>,
}
impl HotReloadWatcher {
pub fn new(
facade: Arc<RwLock<IndexFacade>>,
settings: Arc<Settings>,
check_interval: Duration,
) -> Self {
let index_path = settings.index_path.clone();
let persistence = IndexPersistence::new(index_path.clone());
let meta_file_path = index_path.join("tantivy").join("meta.json");
let last_modified = std::fs::metadata(&meta_file_path)
.ok()
.and_then(|meta| meta.modified().ok());
let doc_state_path = index_path.join("documents").join("state.json");
let last_doc_modified = std::fs::metadata(&doc_state_path)
.ok()
.and_then(|meta| meta.modified().ok());
Self {
index_path,
facade,
settings,
persistence,
last_modified,
last_doc_modified,
check_interval,
broadcaster: None,
}
}
pub fn with_broadcaster(mut self, broadcaster: Arc<NotificationBroadcaster>) -> Self {
self.broadcaster = Some(broadcaster);
self
}
pub async fn watch(mut self) {
let mut ticker = interval(self.check_interval);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
ticker.tick().await;
if let Err(e) = self.check_and_reload().await {
tracing::error!("Error checking/reloading index: {e}");
}
}
}
async fn check_and_reload(&mut self) -> Result<(), Box<dyn std::error::Error>> {
self.check_document_changes();
if !self.persistence.exists() {
debug!("Index file does not exist at {:?}", self.index_path);
return Ok(());
}
let meta_file_path = self.index_path.join("tantivy").join("meta.json");
let metadata = std::fs::metadata(&meta_file_path)?;
let current_modified = metadata.modified()?;
let should_reload = match self.last_modified {
Some(last) => current_modified > last,
None => true,
};
if !should_reload {
tracing::trace!("Index file unchanged");
return Ok(());
}
crate::log_event!("hot-reload", "reloading", "{}", self.index_path.display());
match self.persistence.load_facade(self.settings.clone()) {
Ok(new_facade) => {
let mut facade_guard = self.facade.write().await;
*facade_guard = new_facade;
self.last_modified = Some(current_modified);
let mut restored_semantic = false;
if !facade_guard.has_semantic_search() && !facade_guard.is_semantic_incompatible() {
let semantic_path = self.index_path.join("semantic");
let metadata_exists = semantic_path.join("metadata.json").exists();
if metadata_exists {
match facade_guard.load_semantic_search(&semantic_path) {
Ok(true) => {
restored_semantic = true;
}
Ok(false) => {
crate::debug_event!(
"hot-reload",
"semantic metadata present but reload returned false"
);
}
Err(crate::IndexError::SemanticSearch(
crate::semantic::SemanticSearchError::DimensionMismatch {
ref suggestion,
..
},
)) => {
warn!(
"Semantic index dimension mismatch after hot-reload: {suggestion}. \
Semantic search disabled until re-indexed with --force."
);
}
Err(e) => {
warn!("Failed to reload semantic search after index update: {e}");
}
}
} else {
crate::debug_event!(
"hot-reload",
"semantic metadata missing",
"{}",
semantic_path.display()
);
}
}
let symbol_count = facade_guard.symbol_count();
let has_semantic = facade_guard.has_semantic_search();
if restored_semantic {
let count = facade_guard.semantic_search_embedding_count();
crate::debug_event!("hot-reload", "restored semantic", "{count} embeddings");
}
crate::log_event!("hot-reload", "reloaded", "{symbol_count} symbols");
crate::debug_event!("hot-reload", "semantic search", "{has_semantic}");
if let Some(ref broadcaster) = self.broadcaster {
broadcaster.send(FileChangeEvent::IndexReloaded);
crate::debug_event!("hot-reload", "broadcast", "IndexReloaded");
}
Ok(())
}
Err(e) => {
warn!("Failed to reload index: {e}");
Err(Box::new(std::io::Error::other(format!(
"Failed to reload index: {e}"
))))
}
}
}
fn check_document_changes(&mut self) {
let doc_state_path = self.index_path.join("documents").join("state.json");
let current_modified = match std::fs::metadata(&doc_state_path) {
Ok(meta) => match meta.modified() {
Ok(time) => time,
Err(_) => return,
},
Err(_) => return,
};
let changed = match self.last_doc_modified {
Some(last) => current_modified > last,
None => true,
};
if changed {
self.last_doc_modified = Some(current_modified);
info!("Document store changed, notifying watchers");
if let Some(ref broadcaster) = self.broadcaster {
broadcaster.send(FileChangeEvent::IndexReloaded);
}
}
}
pub async fn get_stats(&self) -> IndexStats {
let indexer = self.facade.read().await;
IndexStats {
symbol_count: indexer.symbol_count(),
last_modified: self.last_modified,
index_path: self.index_path.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct IndexStats {
pub symbol_count: usize,
pub last_modified: Option<SystemTime>,
pub index_path: PathBuf,
}