Skip to content

fix(fsevent): stop the run loop via a CFRunLoopSource to avoid a shutdown race - #7

Merged
Anthony-Eid merged 120 commits into
mainfrom
fix-fsevents-runloop-shutdown
Jun 30, 2026
Merged

fix(fsevent): stop the run loop via a CFRunLoopSource to avoid a shutdown race#7
Anthony-Eid merged 120 commits into
mainfrom
fix-fsevents-runloop-shutdown

Conversation

@Anthony-Eid

Copy link
Copy Markdown

Brings zed-industries/notify up to current notify-rs/notify main and adds our FSEvents run-loop shutdown fix on top. The same fix commit will be proposed upstream separately.

The fix

CFRunLoopStop only takes effect while the target run loop is actually running. The FSEvents watcher thread publishes its run-loop handle and then enters CFRunLoopRun, so a concurrent stop() can call CFRunLoopStop in the window before the loop starts. That stop is lost, the thread parks forever, and the join() in stop() deadlocks. Under heavy event load the loop may also never reach a "waiting" state, so waiting on that is unreliable too.

This replaces the atomic stop flag plus direct CFRunLoopStop with a custom CFRunLoopSource, created and added to the watcher thread's run loop before its handles are published. stop() signals the source and wakes the loop; the source's perform callback then calls CFRunLoopStop from inside the run-loop thread, where it cannot be lost. Because source signals stay pending until processed, a stop that arrives before CFRunLoopRun is entered still stops the loop once it starts.

Tests

Adds a deterministic test that a pre-run stop signal still stops the loop, and a rapid watch/unwatch stress test that previously hung. All fsevent tests pass, and clippy reports no new warnings versus upstream.

JohnTitor and others added 30 commits February 18, 2026 06:32
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…rs#816)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…rs#817)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…rs#818)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…rs#821)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…rs#822)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…rs#826)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
)

Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
…rs#828)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…-rs#829)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…-rs#830)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…-rs#831)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…-rs#833)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…-rs#836)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…-rs#838)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…-rs#839)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…-rs#840)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* feat: get watched paths

* Add changelog entry
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
dmtrKovalenko and others added 28 commits May 2, 2026 12:35
* perf: Optimize debouncer.watch() lookup time

Use sorted VecDeque on 30k invocations cuts down all the linear scanning
work that has be done before. Cuts down 10s to 300ms

* fix: preserve recursive root lookup with sorted watch roots

* add changelog entry

---------

Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…-rs#916)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…tify-rs#923)

* fix(windows): emit Remove event when watched directory is deleted

ReadDirectoryChangesW returns ERROR_ACCESS_DENIED when the watched
directory's handle is invalidated. The existing handler unwatches
but does not emit any event (PR notify-rs#674), leaving consumers with a dead
watch and no way to react. Bring Windows in line with inotify (PR
notify-rs#540) and FSEvents, which both emit a Remove event when their
watched root disappears.

Adds a delete_self_dir test to each backend's test module covering
the cross-platform contract.

* prefer `try_exists`

---------

Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…tify-rs#924)

* perf(notify): avoid filesystem walks for recursive kqueue unwatch

* Correct the changelog entry place
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…-rs#932)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* feat(notify): make FSEvents stream latency configurable

Adds Config::with_fsevent_latency to control the latency passed to
FSEventStreamCreate. Defaults to Duration::ZERO, preserving current
behavior. Set at construction time only.

* docs: add changelog entry

---------

Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
* chore(deps): bump kqueue to 1.2.0

This version adds a number of fixes to major problems, specifically in that it
eliminates panics in failure conditions and substantially increases performance
of watch removals.

* update Cargo.lock

---------

Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
* add test

* windows: report watch error correctly
…y-rs#933)

* feat(notify-debouncer-full): use condvar for no idle CPU usage

The existing code spawned a thread that looped forever and slept for one tick. This is valid, but results in some idle CPU usage.

This adds a condvar that means that the debouncer loop will go back to sleep until such time as there are actually events to handle, eliminating the idle CPU usage.

* refactor

* add changelog entry

---------

Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
…down race

`CFRunLoopStop` only takes effect while the target run loop is actually
running. The FSEvents watcher thread publishes its run-loop handle and
then enters `CFRunLoopRun`, so a concurrent `stop()` could call
`CFRunLoopStop` in the window before the loop starts. That stop request
is lost, the thread parks in `CFRunLoopRun` forever, and the `join()` in
`stop()` deadlocks. Under heavy event load the loop may also never reach
a "waiting" state, so approaches that wait for that are unreliable too.

Replace the atomic stop flag plus direct `CFRunLoopStop` with a custom
`CFRunLoopSource`. The source is created and added to the watcher
thread's run loop before its handles are published, so the caller can
never signal a source that isn't registered yet. `stop()` signals the
source and wakes the loop; the source's perform callback then calls
`CFRunLoopStop` from inside the run-loop thread, where it cannot be lost.
Because source signals stay pending until processed, a stop that arrives
before `CFRunLoopRun` is entered still stops the loop once it starts.

Adds a deterministic test that a pre-run stop signal still stops the
loop, and a rapid watch/unwatch stress test that would previously hang.
@Anthony-Eid
Anthony-Eid merged commit 1824186 into main Jun 30, 2026
18 checks passed
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.