Repository navigation
fs.watch: report watcher queue overflow as ('change', null) - #33110
Conversation
…stead of dropping it
|
Updated 7:02 PM PT - Jun 29th, 2026
❌ @robobun, your commit d8e06d1 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 33110That installs a local version of the PR into your bun-33110 --bun |
|
@robobun adopt |
|
Adopted and ready. The Linux overflow test fails without the fix (it times out) and passes with it under a debug ASAN build; the full CI (build 67023) is red only on lanes this diff cannot affect: two darwin-26-aarch64 jobs where |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughAdds ChangesNull-filename fs.watch event support
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/runtime/node/win_watcher.rs`:
- Around line 245-256: The Windows-specific null-filename branch in the watcher
logic is currently untested, so add a regression test that exercises the
`filename === null` path in `win_watcher.rs` around the `Event::NoFilename`
handling. Use the existing watcher/update flow symbols such as
`on_path_update_fn`, `on_update_end_fn`, and the `handlers` dispatch to verify
the callback path is triggered on Windows when libuv returns a null filename.
Mirror the Linux overflow test structure, but make it Windows-only and assert
the null-name case is covered.
In `@test/js/node/watch/fs.watch.test.ts`:
- Around line 625-637: The pending promises in this fs.watch test are only
rejected on "error", so a close or other watcher failure can leave overflow,
bufferOverflow, or survived hanging until timeout. Update the watcher setup in
fs.watch.test.ts so the main watcher and bufferWatcher both reject every pending
resolver on all failure paths, and ensure the awaited promises are wired to the
watcher’s failure/close lifecycle rather than only the null-filename or f.txt
callbacks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: e98514e7-43e0-4c9e-824f-9abc3e2fbe54
📒 Files selected for processing (5)
src/runtime/node/node_fs_watcher.rssrc/runtime/node/path_watcher.rssrc/runtime/node/win_watcher.rssrc/sys/lib.rstest/js/node/watch/fs.watch.test.ts
|
On the duplicate flag: #33109 and this PR are two takes on the same bug, opened a minute apart by the same author, and both rewrite the same block in win_watcher.rs, so only one can merge. This one also handles the Linux inotify IN_Q_OVERFLOW case (the part #33109 does not touch), via a single Event::NoFilename variant shared by both backends. The one thing #33109 has that this PR lacks is a Windows overflow test, which CodeRabbit also asked for here, so I am folding that test into this branch to make it a strict superset. #33109 can then be closed in favor of this one; leaving that call to a maintainer. |
There was a problem hiding this comment.
I didn't find any issues, but this touches unsafe cross-thread watcher dispatch on two platforms, deliberately diverges from Node's Linux behavior, and the Windows null-filename path has no automated coverage — worth a human pass.
Extended reasoning...
Overview
This PR surfaces OS watcher queue-overflow signals to fs.watch consumers as ('change', null) instead of silently dropping them. It adds a new Event::NoFilename(EventType) variant threaded through both the POSIX and Windows task pipelines, an IN_Q_OVERFLOW branch in the Linux inotify reader thread that fans out to every live PathWatcher, and reworks the Windows libuv callback so a NULL filename is forwarded (as change for overflow or rename for UTF-8 conversion failure) rather than discarded. A new IN_Q_OVERFLOW constant is exposed in src/sys/lib.rs, win_watcher.rs re-exports EventType as pub(crate) so the shared path_watcher::EventType path resolves on Windows, and a deterministic Linux test is added.
Security risks
None apparent. There is no new untrusted input parsing — the only new data is a kernel-supplied flag bit and a NULL pointer check. The fan-out iterates manager.watchers under manager.mutex exactly like the existing fatal-error path does, and the Windows branch mirrors the adjacent error-path handler iteration with the same emit_in_progress / maybe_deinit guard pattern.
Level of scrutiny
Medium-high. The change is conceptually small but lives in hand-managed unsafe Rust: raw *mut PathWatcher derefs under a mutex on the inotify reader thread, a re-export visibility change to make the Windows alias compile, and a control-flow restructure of uv_event_callback that hoists the event_type computation and replaces the let-else return with an explicit null branch. All of this looks correct and follows the patterns already in the file (the new overflow fan-out is structurally identical to the existing emit_error fan-out a few lines below), but it is exactly the kind of code where a second pair of eyes on the lock/lifetime invariants and the emit_in_progress ordering on Windows is valuable.
Other factors
- The PR makes an explicit design decision to diverge from Node on Linux (Node/libuv drops
IN_Q_OVERFLOWthere; this PR delivers it). That's a reasonable product call but is the kind of behavioral choice a maintainer should sign off on. - The Windows code path (both overflow and the
UV_RENAME-with-NULL conversion-failure case) has no automated test, onlycargo checkacross targets — acknowledged in the description. - The Linux test is clever (overflows the queue by mass-
inotify_rm_watchunder the reader lock) and self-skips when host sysctls make it impractical, which seems sound but adds a moderately heavy (~22k mkdirs) test that someone familiar with the CI fleet may want to glance at. - No prior reviewer comments and no bugs surfaced by the bug-hunting pass.
…erage - Wire watcher 'error' and unexpected 'close' to reject every pending promise in the Linux overflow test so a failure is reported instead of hanging to the timeout, and give it an explicit 90s budget: the directory count is fixed by the kernel (max_queued_events=16384), so the ~22k syscalls of setup cannot finish inside the 5s default on a debug/ASAN build. - Add the Windows ReadDirectoryChangesW overflow test: 100 sync writes of ~100-char names while the event loop is blocked overflow libuv's 4KB notification buffer, which libuv reports by invoking the callback with a NULL filename. - Skip the two EACCES permission tests when running as root: root has CAP_DAC_OVERRIDE, so chmod 0o200 never produces the expected error.
|
Pushed d8e06d1, which folds in the Windows ReadDirectoryChangesW overflow test from #33109. That was the one gap claude[bot] and CodeRabbit both flagged, so this PR is now a strict superset of #33109. It also fixes CodeRabbit's other finding (a watcher error or unexpected close now rejects every pending promise in the Linux test) and carries two small drive-bys called out in the updated PR body: an explicit 90s budget on the overflow test, whose directory count is fixed by the kernel's max_queued_events, and a root skip on the two pre-existing EACCES permission tests, which were already failing in a root container on an unpatched build. Full watch suite and all 33 node fs-watch parallel tests pass under a debug ASAN build. |
Summary
fs.watchon Linux silently dropped the kernel's queue-overflow signal: once more thanfs.inotify.max_queued_events(default 16384) events pile up, the kernel discards events and queues a singleIN_Q_OVERFLOWrecord withwd == -1. The reader thread'swd_maplookup found no watch forwd == -1, so consumers got no signal that events were lost. Windows had the same gap: libuv reports aReadDirectoryChangesWoverflow by invoking the callback with a NULL filename (uv__fs_event_process,src/win/fs-event.c:562), whichuv_event_callbackdiscarded with an early return.Both backends now deliver the loss signal the way node does on Windows: a
'change'event with anullfilename, sent to every live watcher (the inotify queue is shared by all watchers on the fd). Node on Linux drops the overflow (libuv cannot mapwd == -1to a handle), so on Linux this is deliberately better than node rather than node parity.Event::NoFilename(EventType)variant flows through both task pipelines and reaches JS as(eventType, null)for everyencoding, including'buffer'.UV_RENAMEwhen a filename fails UTF-16 to UTF-8 conversion (the conversion result is unchecked inuv__fs_event_process). The branch keys the event type offevents & UV_RENAME, so that case now surfaces as('rename', null)exactly like node, instead of being dropped.Supersedes #33109, which is the Windows-only half of the same fix; its Windows overflow test is folded in here.
Deliberately excluded:
src/watcher/INotifyWatcher.rs(the bundler /--watch/--hotwatcher) still dropsIN_Q_OVERFLOW; the right response there is a rescan or reload decision in the hot reloader, not a JS event, so it needs its own change.kFSEventStreamEventFlagKernelDropped/UserDroppedare not surfaced. libuv and node do not surface them either.Test plan
test/js/node/watch/fs.watch.test.tstriggers a realIN_Q_OVERFLOWdeterministically without root: closing a recursive watcher overmax_queued_events + 6144directories unregisters one watch per directory inside a single reader-lock critical section, and eachinotify_rm_watchqueues anIN_IGNOREDthe blocked reader cannot drain (it gets at most one 64KB read, 4096 events). Asserts('change', null)on a default-encoding watcher and anencoding: 'buffer'watcher, then that the watcher still delivers normal events afterwards. Self-skips when/proc/sys/fs/inotifylimits make the setup impractical.ReadDirectoryChangesWbuffer, which libuv reports by calling back with a NULL filename. Asserts a null-filename'change'reaches both a utf8 and a buffer watcher, or that every write was observed.test/js/node/watch/suite (56 pass, 0 fail) and all 33test/js/node/test/parallel/test-fs-watch*.jspass under a debug ASAN build.build-rustlanes (linux-x64, windows-x64, windows-x64-baseline, windows-aarch64) pass on this diff, covering the#[cfg(windows)]path.('rename', null)) has no test: a filename libuv cannot convert is not creatable from JS. It compiles for both Windows targets and shares the delivery code the overflow test exercises.Two test changes beyond the new coverage:
max_queued_eventsplus one 64KB read), so its ~22k syscalls of setup cannot fit the 5s default under a debug ASAN build (6s measured). AcpSync-based setup was measured 6x slower, so the flatmkdirSyncloop is already the cheapest form.EACCESpermission tests in the same file now skip when running as root: root hasCAP_DAC_OVERRIDE, so thechmod 0o200they rely on never produces the error they assert. They fail identically on an unpatched build, so this is environmental, not a regression.