From 08307338133a08cdcb67cfc45697d13b2edbc02a Mon Sep 17 00:00:00 2001 From: Aaron Hiniker Date: Mon, 27 Jul 2026 10:27:41 -0700 Subject: [PATCH] fix(watcher): release DashMap guard before insert to stop file-event deadlock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `process_fs_event`'s Modify branch held a DashMap read guard across a write: if let Some(old_metadata) = file_registry.get(&file_id) { let old_metadata = old_metadata.value().clone(); if old_metadata.content_hash != new_metadata.content_hash { file_registry.insert(file_id.clone(), new_metadata.clone()); `DashMap::get` returns a `Ref` holding a read lock on the key's shard. The `let old_metadata = ...clone()` line looks like it releases that guard, but shadowing a binding does not drop the original value — the `Ref` lives to the end of the `if let` block. The `insert` then blocks forever waiting for a write lock on the shard the same task is still read-locking. The task hangs, no `FileChangeEvent::Modified` is ever sent, and the shard stays locked, so every later event for a file on it is lost too. Net effect: watch-mode auto-reindexing silently stops working. It looked intermittent because macOS emits both `Create(File)` and `Modify(Data(Content))` for a single append, spawned as concurrent tasks. When `Create` won the race it inserted first, so `Modify` saw equal hashes, skipped the deadlocking branch, and `Create`'s unconditional send still delivered an event — the first edit often appeared to work. In steady state only `Modify` arrives and the watcher wedges. Fix: clone out of the registry in a single statement so the temporary guard is dropped at the semicolon, then match on the owned `Option`. Matching on an owned value (rather than `if let Some(..) = registry.get(..)`) also keeps a future edit from silently re-holding the guard across the branch. Verified on macOS with a release build: three successive edits to a watched file produced zero reindexes before the change and three after, via both `codegraph daemon start` and `codegraph start --watch`. Note: the existing tests cannot catch this. `best_effort_poll_for_changes` is `#[cfg(test)]`, so when no event arrives `next_batch` falls back to polling and synthesizes `Modified` events — tests pass whether or not event delivery works in a release binary. Co-Authored-By: Claude Opus 5 --- crates/codegraph-parser/src/watcher.rs | 38 +++++++++++++++++--------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/crates/codegraph-parser/src/watcher.rs b/crates/codegraph-parser/src/watcher.rs index 123b6588..0062f5f6 100644 --- a/crates/codegraph-parser/src/watcher.rs +++ b/crates/codegraph-parser/src/watcher.rs @@ -383,21 +383,33 @@ impl FileSystemWatcher { if let Ok(new_metadata) = Self::create_file_metadata_static(&path, language_registry).await { - if let Some(old_metadata) = file_registry.get(&file_id) { - let old_metadata = old_metadata.value().clone(); - if old_metadata.content_hash != new_metadata.content_hash { + // Clone out of the registry in a single statement so the + // DashMap read guard is dropped before the insert below. + // Holding a `Ref` across `insert` deadlocks the task: the + // insert needs a write lock on the very shard the guard is + // read-locking, and nothing will ever release it. + let previous = file_registry + .get(&file_id) + .map(|entry| entry.value().clone()); + + match previous { + Some(old_metadata) => { + if old_metadata.content_hash != new_metadata.content_hash { + file_registry + .insert(file_id.clone(), new_metadata.clone()); + let _ = event_sender.send(FileChangeEvent::Modified( + file_id, + new_metadata, + old_metadata, + )); + } + } + None => { + // File wasn't tracked before, treat as creation file_registry.insert(file_id.clone(), new_metadata.clone()); - let _ = event_sender.send(FileChangeEvent::Modified( - file_id, - new_metadata, - old_metadata, - )); + let _ = event_sender + .send(FileChangeEvent::Created(file_id, new_metadata)); } - } else { - // File wasn't tracked before, treat as creation - file_registry.insert(file_id.clone(), new_metadata.clone()); - let _ = event_sender - .send(FileChangeEvent::Created(file_id, new_metadata)); } } }