use std::path::PathBuf;
use std::time::Instant;
use serde_json::json;
use crate::errors::{ErrorContext, log_debug, log_warn};
use super::backend::WatchStartError;
use super::state::FileWatcher;
use super::types::{
EXHAUSTION_REASON, INOTIFY_LIMIT_REASON, MAX_LOCK_RETRIES, MAX_LOCK_RETRY_DELAY_MS,
WatchSyncError,
};
use super::util::{context, now_ms, path_string};
impl FileWatcher {
pub(super) fn degrade(&mut self, reason: &str, mut context: ErrorContext) {
if self.degraded_reason.is_some() {
return;
}
self.degraded_reason = Some(reason.to_owned());
context.insert(
"projectRoot".to_owned(),
json!(path_string(&self.project_root)),
);
context.insert("reason".to_owned(), json!(reason));
log_warn("File watcher disabled", Some(&context));
if let Some(callback) = &self.on_degraded {
callback(reason);
}
self.stop();
}
pub(super) fn warn_inotify_limit(&mut self, mut context: ErrorContext) {
if self.inotify_limit_warned {
return;
}
self.inotify_limit_warned = true;
context.insert("watchedDirs".to_owned(), json!(self.dir_watchers.len()));
log_warn(INOTIFY_LIMIT_REASON, Some(&context));
}
pub(super) fn schedule_sync(&mut self) {
self.scheduled_at = Some(Instant::now());
self.scheduled_delay_ms = Some(self.debounce_ms);
}
fn schedule_retry_sync(&mut self, delay_ms: u64) {
self.scheduled_at = Some(Instant::now());
self.scheduled_delay_ms = Some(delay_ms);
}
pub(super) fn flush(&mut self) {
if self.syncing || self.stopped {
return;
}
self.sync_started_ms = now_ms();
self.syncing = true;
let result = (self.sync_fn)();
match result {
Ok(result) => {
self.lock_retry_count = 0;
let sync_started_ms = self.sync_started_ms;
self.pending_files
.retain(|_, info| info.last_seen_ms > sync_started_ms);
if let Some(callback) = &self.on_sync_complete {
callback(&result);
}
}
Err(WatchSyncError::LockUnavailable(err)) => {
self.lock_retry_count += 1;
let ctx = context([
("pendingFiles", json!(self.pending_files.len())),
("retryCount", json!(self.lock_retry_count)),
]);
log_debug("Watch sync skipped: file lock unavailable", Some(&ctx));
if self.lock_retry_count > MAX_LOCK_RETRIES {
self.degrade(
"RustCodeGraph file lock held by another process past the retry budget; \
auto-sync disabled. Run `rustcodegraph sync` once the other writer finishes \
(or install git sync hooks) to refresh the graph.",
context([
("pendingFiles", json!(self.pending_files.len())),
("retryCount", json!(self.lock_retry_count)),
("error", json!(err.message())),
]),
);
}
}
Err(err) => {
self.lock_retry_count = 0;
let ctx = context([("error", json!(err.message()))]);
log_warn("Watch sync failed", Some(&ctx));
if let Some(callback) = &self.on_sync_error {
callback(&err);
}
}
}
self.syncing = false;
if !self.pending_files.is_empty() && !self.stopped {
if self.lock_retry_count > 0 {
let exponent = self.lock_retry_count.saturating_sub(1).min(31);
let multiplier = 1u64 << exponent;
let retry_delay_ms = self
.debounce_ms
.saturating_mul(multiplier)
.min(MAX_LOCK_RETRY_DELAY_MS);
self.schedule_retry_sync(retry_delay_ms);
} else {
self.schedule_sync();
}
}
}
pub(super) fn handle_start_error(&mut self, err: WatchStartError) {
match err {
WatchStartError::ResourceExhaustion(message) => {
self.degrade(EXHAUSTION_REASON, context([("error", json!(message))]));
}
other => {
let ctx = context([("error", json!(other.message()))]);
log_warn("Could not start file watcher", Some(&ctx));
self.stop();
}
}
}
pub(super) fn handle_runtime_watch_error(
&mut self,
err: WatchStartError,
dir: Option<PathBuf>,
) {
match err {
WatchStartError::ResourceExhaustion(message) => {
let mut ctx = context([("error", json!(message))]);
if let Some(dir) = &dir {
ctx.insert("dir".to_owned(), json!(path_string(dir)));
}
self.degrade(EXHAUSTION_REASON, ctx);
}
WatchStartError::InotifyExhaustion(message) => {
let mut ctx = context([("error", json!(message))]);
if let Some(dir) = &dir {
ctx.insert("dir".to_owned(), json!(path_string(dir)));
}
self.warn_inotify_limit(ctx);
if let Some(dir) = dir {
self.unwatch_dir(&dir);
}
}
WatchStartError::Other(message) => {
let mut ctx = context([("error", json!(message))]);
if let Some(dir) = &dir {
ctx.insert("dir".to_owned(), json!(path_string(dir)));
self.unwatch_dir(dir);
}
log_warn("File watcher error", Some(&ctx));
}
}
}
}