Skip to content

Make watcher shutdown synchronous - #31

Merged
fohara merged 1 commit into
mainfrom
fix-watcher-shutdown-race
Aug 8, 2026
Merged

fohara merged 1 commit into
mainfrom
fix-watcher-shutdown-race

Conversation

@fohara

@fohara fohara commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Fixes the intermittent Test (macos-latest, stable) failure seen on #30:

no events expected after handle is dropped; got [".../tasks.md"]

The bug

FileWatcherHandle held _debouncer_thread: JoinHandle<()>. Dropping a
JoinHandle detaches the thread rather than joining it, so drop(handle)
only started teardown. The test's fixed 50 ms settling sleep was the only
thing between that and the next write — with the debounce window also 50 ms,
right on the boundary. A loaded runner tips it over.

Why joining alone is not enough

The notify backend can briefly outlive its own drop (FSEvents does). In that
window the debouncer can reach the flush deadline for an already-pending path
and emit it before it ever observes the channel disconnect. A join would simply
wait through that emission rather than prevent it.

So Drop now, in this order:

  1. sets an Arc<AtomicBool> shutdown flag — the loop checks it before every
    emit, so a pending path is abandoned rather than raced;
  2. drops the watcher, which destroys the notify closure holding the
    debouncer's input sender (that disconnect is what lets the loop return);
  3. joins the thread.

The order is load-bearing and commented: joining before dropping the watcher
would deadlock.

Testing

The sleep-based test was a poor regression guard — I could not make it fail
locally even under CPU contention (40/40 passes), and it still passed 5/5
against the unfixed code once the sleep was removed.

Replaced it with shutdown_flag_suppresses_due_emissions, which drives
debouncer_loop directly with a zero debounce, so the path is due the instant
it is recorded — exactly the race. It asserts both directions: emitted when not
shutting down, abandoned when it is. No sleeps, no filesystem, 0.00s, and
verified to FAIL when the guard is removed.

dropping_handle_stops_events also loses its post-drop settling sleep, since
the entire point of the fix is that none is needed.

Drop signalled teardown but detached the debouncer thread, so events could
still arrive after drop. Signal a shutdown flag, drop the watcher, then join.
@fohara
fohara merged commit 02196b7 into main Aug 8, 2026
21 checks passed
@fohara
fohara deleted the fix-watcher-shutdown-race branch August 8, 2026 19:12
@fohara fohara mentioned this pull request Aug 8, 2026
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