fix(cli): settle file events before comparing contents - #1905
Merged
Merged
Conversation
Contributor
|
Member
Author
|
@greptile review |
ChiragAgg5k
force-pushed
the
cli-watcher-debounce
branch
from
September 12, 2026 17:51
880a38c to
c7bfc63
Compare
Member
Author
|
@greptile review |
An editor saving a file truncates it and writes the bytes back, so a notification can arrive while the file is briefly empty and the watcher reports a reload the user never asked for. Events are debounced and fingerprinted once the path settles. Each path settles on its own clock: one shared deadline would hand a file first touched late in a busy stretch only the milliseconds left on it. A per-path cap of a second keeps a file that never falls quiet from being deferred forever. Close drains the watcher rather than only signalling it, so no callback runs against a queue the caller has already torn down.
ChiragAgg5k
force-pushed
the
cli-watcher-debounce
branch
from
September 12, 2026 18:01
c7bfc63 to
a216968
Compare
Member
Author
|
@greptile review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
appwrite runreloads a function when a watched file's contents change. This fixes two ways that goes wrong, and the second only exists because of the first.What breaks
An editor saving a file truncates it and writes the bytes back. Those are two filesystem operations, so between them the file is empty on disk. The watcher fingerprints on every notification, so it sees the empty file, decides the contents changed, and reloads — then sees the restored bytes, decides they changed again, and reloads a second time. Saving a file you did not edit rebuilds your function, twice.
Debouncing fixes that: wait for the notifications to stop, then fingerprint once. But a single timer reset by every event never fires while events keep arriving. A build tool, a test runner, or a dependency install writing into the same tree resets the timer every few milliseconds, and the edit the user is actually waiting on never reaches the reload queue at all — it sits in the pending map, along with everything else, for as long as the noise lasts. Trading spurious reloads for no reloads is not a fix.
So the wait is per path rather than shared. Each path settles on its own clock, and each carries its own cap of one second from its first event, so a tree that never falls quiet still reports within a bounded time. Sharing one deadline would have the same shape of bug in miniature: a file first touched late in a busy stretch gets only whatever milliseconds are left on it, and is fingerprinted mid-save — the spurious reload this exists to prevent.
Reproduction
Three watcher variants —
main, the debounce without the cap, and this PR — driven through the publicStartAPI by the same harness. Two scenarios: saving an unchanged file ten times (every reload is one the user did not ask for), and editing a source file while a background writer rewrites a log every 5ms (the edit has to arrive).macOS 15 (kqueue), Go 1.25
mainLinux (inotify),
golang:1.25mainTen unchanged saves cost ten to twenty rebuilds today, depending on whether the kernel coalesces the truncate and the write into one notification or two. Both platforms starve identically without the cap, and both land on the one-second bound with it.
What the cap costs
Almost nothing, because the clock is per path. An edit is reported once its own events stop, whatever the rest of the tree is doing. Over 25 edits made at random points during continuous background writing:
That is the 100ms settle and nothing more — the same latency an edit in a completely quiet tree pays. The one-second cap only binds for a file that is itself being written continuously, which is a log, not a source file.
An earlier revision of this PR shared one deadline across all pending paths. It fixed the starvation but charged every edit for the tree's noise: the same measurement gave a median of 575ms and a p90 of 878ms, spread uniformly across the cap window. Per-path settling is what makes the cap cheap.
Changes
Closedrains the watcher instead of only signalling it, so no callback runs against a queue the caller has torn down. It stays idempotent.Tests
Three tests drive the watcher through
Startrather than its internals, so a behaviour-preserving change to how the debounce is implemented cannot break them:mainmain, and fails with one shared deadline rather than per-path onesClosedoes not return while a report is still in flight — fails without the drainEach was run against the version lacking its fix to confirm it fails there, rather than assumed to cover it. The saves in the busy test are spaced so they drift against any batching instead of landing at the same point in it each time, because a save is only ignored if it is ignored whenever it happens.
Verified with
gofmtandgo vet, and the suite run repeatedly under-raceon macOS and on Linux (golang:1.25).This was previously carried on the renovate deps branch (#1904); it belongs in its own PR.