Skip to content

fix: watcher leak, registry write races, and stalled scans - #28

Merged
anbturki merged 3 commits into
mainfrom
fix/watcher-registry-scan
Jul 18, 2026
Merged

fix: watcher leak, registry write races, and stalled scans#28
anbturki merged 3 commits into
mainfrom
fix/watcher-registry-scan

Conversation

@anbturki

Copy link
Copy Markdown
Owner

Fixes the three code-analysis findings filed after the v0.9.1 sync work. Each was proven with a test that fails against the current code, and the registry fix was additionally measured end to end with real concurrent sidecar processes.

Changes

Watcher leak (useTabs) - a tab's watch() could resolve after its path changed. The old id-based guard still passed, so the real unwatch was assigned to an orphaned slot and the FSEvents stream leaked for the session. Now the resolved attach checks whether the map still holds the exact slot it created, which covers path swaps, tab close, unmount, and rapid successive swaps in one comparison. The rejection path no longer evicts the live watcher that replaced it.

Registry atomicity (core) - concurrent MCP sidecars each read the registry, added an entry, and wrote the whole file back, silently dropping each other's workspaces. Now an exclusive advisory lock spans the load-modify-save, and writes go through a temp file plus rename. The lock is on a sidecar path deliberately: the rename swaps the registry inode, so a lock on the registry itself would leave writers holding different inodes and never contending.

Stalled scans (scan) - a scan on unreachable storage never settles, pinning the workspace spinner. It now fails after 60s without progress, an inactivity window rather than a fixed deadline so healthy large scans are never cut off.

Evidence

Registry, real concurrent sidecar processes (not threads):

build 24 procs 48 procs
pre-fix 12, 14, 22 of 24 41, 43, 43, 45 of 48
fixed 24, 24, 24 48, 48, 48

Zero leftover temp files across all fixed runs.

Scanner load test on synthetic workspaces:

  • 25,000 files: ~400ms, max gap between progress events 100-117ms.
  • 55,000 files (over the 50k cap): capped at 50,000 with truncated=true, ~1.9-3.3s, max gap 122ms. The 60s inactivity window therefore has roughly a 490x margin over the worst observed healthy gap.
  • Adversarial input (oversize file, symlink loop, permission-denied directory): all three counted as skipped, all real files still found, and the symlink loop did not hang the walk.

Flakiness: registry concurrency test 12/12, full frontend suite 5/5.

Suite: 135 frontend tests, 81 core tests, tsc --noEmit clean, cargo clippy --workspace --all-targets clean.

Fixes #25
Fixes #26
Refs #27

anbturki added 3 commits July 18, 2026 17:09
A tab's watch() call could resolve after its path changed, at which
point the id-based guard still passed (the id is wanted and the map
holds a slot) and the real unwatch was assigned to an orphaned slot,
leaking an FSEvents stream for the rest of the session.

Compare slot identity instead: if the map no longer holds the exact
slot this attach created, unwatch immediately. That covers path swaps,
tab close, unmount, and rapid successive swaps with one check. The
rejection path now deletes only its own slot, so a failed stale attach
cannot evict the live watcher that replaced it.

Fixes #25
Concurrent MCP sidecars each read the registry, added their entry, and
wrote the whole file back, so simultaneous registrations silently
dropped each other. Measured with 48 concurrent sidecar processes:
41-45 of 48 workspaces survived before, 48 of 48 after.

Hold an exclusive advisory lock across the load-modify-save sequence,
and replace the in-place write with a temp file plus rename so a reader
never observes a partial file. The lock lives on a sidecar path because
the rename swaps the registry inode, which would leave writers holding
locks on different inodes and never contending.

Fixes #26
A scan on unreachable storage never settles, leaving the workspace
spinner running indefinitely. Fail it when no progress arrives for 60s
rather than on a fixed deadline, so a large but healthy scan is never
cut off: a 55k-file workspace scans in ~3s with at most 122ms between
progress events, leaving a wide margin.

This is the mitigation half of #27. The upstream defect it guards
against (tauri-apps/tauri#12338) is still open, so #27 stays open to
track that fix landing in our Tauri version.

Refs #27
@anbturki
anbturki merged commit 2f28898 into main Jul 18, 2026
3 checks passed
@anbturki
anbturki deleted the fix/watcher-registry-scan branch July 18, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Concurrent MCP sidecars can permanently drop a workspace from the registry Per-tab watcher leaks when a tab's path changes mid-attach

1 participant