Skip to content

vm: inotify: relay file creation, removal, rename and new directories - #1650

Open
fredericmorin-flare wants to merge 1 commit into
abiosoft:mainfrom
fredericmorin-flare:inotify-structural-events
Open

fredericmorin-flare wants to merge 1 commit into
abiosoft:mainfrom
fredericmorin-flare:inotify-structural-events

Conversation

@fredericmorin-flare

Copy link
Copy Markdown

Related: #1341, #1643, #1649.

Problem

With --mount-inotify, the host watcher only subscribes to write events, and it ignores directories. Containers are never notified when:

  • a file is replaced by an atomic rename. Most editors and tools (claude) save this way: they write a temporary file, then rename it over the target.
  • a file is created empty, removed or renamed.
  • a directory is created. Files written inside it are synced, but no watcher in the VM watches the new directory yet, and its parent gets no event.

Separately, the channel passed to notify has a buffer of one. notify drops events when that channel is full, and events are handled one at a time by running a command in the VM. Bursts lose events: in 3 out of 15 runs of mkdir -p a/b/c && echo > a/b/c/f, the event for a never arrived.

Observed with Pants: pantsd caches the file system and relies on inotify to invalidate it. A new test file fails with Unmatched glob. Worse, a test edited to fail is reported as passing from its memoized result, because the edit was an atomic save.

Change

  • The host watcher also subscribes to create, remove and rename events.
  • A file that still exists is synced as before (chmod and : >>, emitting IN_ATTRIB and IN_CLOSE_WRITE).
  • For a directory, or a path that no longer exists, the parent directory is synced instead. A temporary file .colima-inotify.XXXXXX is created and removed in it from the VM, emitting IN_CREATE and IN_DELETE. Watchers rescan the directory and pick up the added, removed or renamed entry. Host events for this temporary file are ignored, so syncing can't loop.
  • The notify channel gets a buffer of 1024.

Results

macOS 26 (Apple M3 Pro), vz + virtiofs, docker runtime. Each file operation was run on the host while inotifywait -m -r watched the directory in a container:

Host operation Before After
echo > new, append, cp ATTRIB, CLOSE_WRITE same
touch new empty file nothing ATTRIB, CLOSE_WRITE
atomic replace (write tmp && mv tmp file) nothing ATTRIB, CLOSE_WRITE on the file
atomic write of a new file nothing CREATE, DELETE in the directory
rm nothing CREATE, DELETE in the directory
mv a b nothing ATTRIB, CLOSE_WRITE on b, CREATE, DELETE in the directory
mkdir + file inside nothing in the watched tree CREATE, DELETE in the parent
mkdir -p a/b/c + file, 30 runs event for a lost in 3 of 15 runs 30/30

With pantsd watching the tree from a container:

Scenario Before After
New test file (atomic write) Unmatched glob runs on the first try
Test edited to fail (atomic save) passes, from the memoized result fails
New directory with a BUILD file and a test Unmatched glob runs on the first try
File renamed, then removed not seen seen

Tests

  • watch_test.go: syncTarget for an existing file (synced, keeping its mode), a directory and a removed file (the parent is synced), a removed directory tree (skipped), and the temporary file (ignored).
  • events_test.go: the generated command for a directory.
  • events_linux_test.go (Linux only, runs in CI): the directory command emits IN_CREATE and IN_DELETE and leaves no file behind.

All tests pass on macOS and on Linux, and golangci-lint reports no issues.

Depends on #1649

This PR probably depends on #1649, and should land after it. It relays more events (creations, removals, renames, new directories), but each one still goes through the current sync: two limactl shell commands per event, about 4 events per second, and a rate limit that drops events above 50 unique paths per 500ms. On its own, it would overflow that dispatch during bursts, for example a branch switch or an npm install. #1649 batches the syncs.

Both PRs change events.go. I'll rebase this one once #1649 is merged.

LLM usage disclosure

This change was written with the help of an LLM (Claude): the investigation, the fix and the tests. I reviewed every line and ran the tests and measurements above myself.

🤖 Generated with Claude Code

Only write events for existing files were relayed to the VM, so watchers
never learned about new directories, removed files or files replaced by
an atomic rename (how most editors and tools save). Pants (pantsd) kept a
stale view: new files failed with "Unmatched glob" and edited files could
be reported as passing from a memoized result.

- Also watch create, remove and rename events on the host.
- A file that still exists is synced as before (IN_ATTRIB, IN_CLOSE_WRITE).
- For a directory or a path that no longer exists, a temporary file is
  created and removed in the parent directory in the VM, emitting
  IN_CREATE and IN_DELETE so watchers rescan it. Events for that
  temporary file are ignored on the host.
- Buffer the notify channel: notify drops events when the receiver is
  busy, and every event is handled by running a command in the VM, so
  bursts such as `mkdir -p a/b/c` lost events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Frederic Morin <frederic.morin@flare.io>
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.

1 participant