Repository navigation
Go back to waiting when the watch folder disappears, and stop cleanly during start-up - #390
Merged
Merged
Conversation
… runs A watcher whose watch folder was deleted or unmounted kept reporting `watching`, because nothing looked at the folder once watching had started. Every heartbeat tick of a running watcher, and watchdog's report that the watch folder itself was deleted or moved, now ask the supervisor to check the folder. The check runs on the supervisor's poll thread. If the folder can't be listed, the runtime is torn down and the watcher enters the waiting state with the reason, as it does when the folder is missing at start. It queues the `watch_directory_unavailable` error and no second `watcher_started` event. Uploads in progress get 10 seconds to finish instead of the 120 seconds of a config rebuild, because the parts of a large file that haven't started can't open the file any more. The existing waiting-state check starts a new runtime once the folder is back. A folder that was deleted and created again before the check is also found: watchdog stops watching the deleted folder, so the runtime is rebuilt when its file monitor is no longer watching. Co-Authored-By: Claude Opus 5.5 <[email protected]>
`watch` installed its Ctrl+C and SIGTERM handlers only after the supervisor had started. A SIGTERM during the config sync, the first heartbeat or the start-up scan ended the process, and Ctrl+C raised KeyboardInterrupt, so neither sent a final `stopped` heartbeat or a `watcher_stopped` event. The handlers are now installed right after the supervisor is built. A stop that arrives during start-up is recorded, and once the start returns `run_supervisor` returns at once and the command stops the watcher as for any other Ctrl+C, with exit code 0, as the Windows service already does. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… folder The tests that drive the check with a heartbeat tick could pass on watchdog's own report of the deleted folder instead, which hid a missing tick hook. They now ignore events from watchdog's thread, and a tick has to ask for the check itself. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Describe how a running watcher moves to the waiting state when its folder disappears, why that move waits 10 seconds for uploads, and that `watch` now installs its signal handlers before the start. The known-bug examples in the end-to-end docs no longer name bugs that are fixed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
A stop that arrived while the watcher was starting used to wait for the start-up scan, which takes minutes on a large folder. Ctrl+C printed "Shutting down..." and nothing happened. The supervisor now has one stop signal, `request_stop`. The `watch` command's signal handler and the Windows service's stop both call it, the service from a thread that starts before `supervisor.start()`. It wakes `poll`, and the file monitor's scan reads it between entries and ends early. The scan writes nothing to the state database for a file it did not reach, and a baseline scan on the first start of a `new-only` environment records nothing at all when it stops early, so the next start scans or seeds the whole folder. The upload worker is not started for a watcher that is already stopping, and `poll` leaves a rebuild or a folder check alone once a stop is requested. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ing to waiting
The running-folder check made `exists`, `is_dir` and `scandir` calls on the
thread that handles stops. On a hung network share one of them can block for
a minute or for good, so a stop was ignored while the watcher waited for it.
The look at the folder now runs on a daemon thread that is waited for ten
seconds (`FolderProbe`). A look that takes longer counts as a problem
("not responding"), and a hung look is waited for by the next ones and not
started again. The waiting state's check and the start of a runtime use the
same look.
The move to the waiting state is bounded where a dead share could hold it:
the observer's stop runs on its own thread, the heartbeat join and the event
flush have limits, and upload threads that the cut-off can't reach are left
behind with their state database open. The next runtime waits until they
have exited.
One failed look no longer moves the watcher. A second look three seconds
later has to fail too, and its reason is the one reported, so a share that
drops for a moment costs no upload, and a folder that is still being deleted
on Windows is reported once, as "not found".
Before an upload thread drops a pending upload, or counts a missing or
failed attempt, it asks whether the whole watch folder is gone. If it is,
the row or queue entry stays, nothing is counted, and the supervisor is
asked to check the folder.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A folder that is there can still leave the runtime blind. watchdog's emitter may have stopped, or the folder may not be the one the runtime started on. Renaming a folder and making another at its path, mounting something on it, or moving it to the Recycle Bin and making it again are not reported by watchdog 6.0.0. The supervisor now records the folder's device and inode number from the look that found it fit to watch, and compares it at each check. An inode number of 0, which some SMB servers report for everything, is ignored. A mismatch, like a monitor that stopped watching, rebuilds the runtime. A runtime that goes blind again before it has watched for two minutes is not rebuilt again, because that would send a heartbeat and a full scan at every check on a share whose emitter dies at once. The watcher waits with the reason "watch_directory can't be watched" and tries again on the waiting state's backoff, which each further blind runtime lengthens. A rebuild whose start fails, or whose folder is gone when it starts, no longer queues a `watcher_started` event that says the watcher resumed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…scan The developer docs now say what the move to the waiting state includes and which of its waits are bounded, how the folder is looked at and confirmed, how a folder that was replaced or can't be watched is handled, and how a stop ends the start-up scan under `watch` and the Windows service. The user docs say that a stop no longer waits for the whole scan, and that a drop of a moment doesn't count. The examples of an issue number in the end-to-end docs and docstrings now use a placeholder that can't be mistaken for a real issue. Co-Authored-By: Claude Opus 5.5 <[email protected]>
watchdog's `Observer.stop` has no timeout, so the monitor already runs it on its own thread and waits for it for a limited time. That wait was one blocking join, which a stop of the watcher could not end. `FileMonitor.stop` now takes a `should_cancel` callable and waits in steps of 0.1 s, so a caller that is stopping goes on within a fraction of a second and leaves the observer's thread to finish by itself. `FileMonitor.has_exited` says whether the stability checker is gone without logging. `join` logs "FileMonitor stopped" every time it finds the checker gone, which made a caller that asks over and over write a line each time. The test of the observer's wait asserts the timeout it passes, with the clock held still, where it used to compare it with the wall clock. Co-Authored-By: Claude Opus 5.5 <[email protected]>
When a file can't be found, the uploader asks whether the whole folder is gone before it drops the file's row or counts an attempt. That look has a limit of its own (10 s on a hung share), which `Uploader.abort` can't cut short, so an upload thread in it during a stop held the stop up. `Uploader._folder_is_gone` now answers yes at once when a stop was requested, without a look: a stop is not a missing file. The file's row or queue entry stays, and the next start deals with it. `UploadQueueWorker.has_exited` says whether the worker's thread is gone without signalling it. The tests have the stop arrive as the file is looked for, which is the only moment the check can see one: every caller checks for a stop before it gets that far. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The service logged "is running" or "is waiting" after its start even when a stop had arrived during it, and then "was asked to stop while it was starting". The two lines contradicted each other. The first is now left out when a stop is pending, as the `watch` command leaves out its status lines. The end-to-end drivers wait for "is running" or "is waiting" as the ready signal, or for the service to have stopped, which a service that was asked to stop does, so the drivers still see the end of a start. The two unit tests that read "is running" now stop the loop after it has started. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… threads from stalling the watcher A stop during a folder check waited for the whole check. The look at the folder always waited its full 10 s on a hung share, and neither the second look, the move to the waiting state nor the rebuild of a blind runtime looked at the stop, so a stop could start its budget about 30 s late and overrun the service's wait hint, or miss a Windows preshutdown's 7 s altogether. * `FolderProbe.look` takes a `should_cancel` callable and waits in steps of 0.1 s. A look that a stop cancelled says `None`, and the caller does nothing. The supervisor's checks pass its stop signal, and so does the uploader's callback, which now ends its look on a stop too. * The checks look at the stop after each look and before the teardown. The drain of the move and of a blind rebuild, the wait after the cut-off, the observer's stop and the wait for a heartbeat tick all end on a stop. The runtime is then left in place for `stop`, which cuts its uploads off within its own budget. A folder whose inode number isn't stable no longer stops being watched live. On FUSE mounts without `use_ino` and on SMB or virtual drives without persistent file IDs, two identity mismatches in the window used to send the watcher to "can't be watched" with a growing backoff. That wait is now only for a monitor that has stopped watching. On a second mismatch alone the watcher logs both identities, stops comparing the identity until the next config change or restart, and keeps the runtime. The threads of a runtime that the move had to leave behind no longer block config changes without limit. `_reap_cut_off` asks about the threads without logging, where `FileMonitor.join` wrote a line at every ask. A change that moves the watch folder doesn't wait for them, because they can't touch the new folder and their rows are dropped. A change that keeps the folder waits up to two minutes, logs every minute, and ends on a stop. If they are still there, the start waits with the reason "uploads stuck on <folder> have not ended", which the waiting state's check now sets in place of a stale "not found". A rebuild of a blind runtime is bounded the same way, because the identity can change while upload threads are blocked on the old filesystem. Smaller changes: the log of the move says what was left behind (upload threads, or only a heartbeat tick), a single "not responding" no longer ends the backoff of a start that keeps failing (a "not found" or two problems in a row still do), and the tests whose looks must succeed use 2 s timeouts with a longer hang. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…cut-off waits The developer docs now say which waits of a folder check end on a stop and what a stop still waits for (a request in flight, the first heartbeat of the waiting state, and the look that starts a runtime). They give the corrected time the move takes on a dead share, which the earlier line left out, and say that only a request in flight and the waiting state's first heartbeat remain uncovered. They also describe the identity fallback for filesystems that give a folder no lasting identity, the waits for threads that an earlier move left behind (per folder, bounded for a config change, and with a reason), the rule for when the waiting state's backoff is cleared, and that an upload thread asks for a folder check only when its look finds the folder itself unavailable. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This branch was successfully deployed
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.
What changes
Watcher 1.4.3 fixes two things.
A watch folder that disappears while the watcher runs. Before, a watcher whose folder was deleted or unmounted kept reporting
watching, because nothing looked at the folder once watching had started. Now:watch_directory not responding: <path>. A look that is still running is waited for by the next ones and not started again. The waiting state's check and the start of a runtime use the same look. A caller that is stopping doesn't wait for it:FolderProbe.looktakes a cancel callable and waits in steps of 0.1 s, and the supervisor's checks and the uploader pass their stop signal. The look that starts a runtime keeps its 10 seconds.watch_directory_unavailableerror, and queues no secondwatcher_startedevent, because the watcher has not stopped.stop, which cuts its uploads off within its own budget (the stop is also checked after each look and before the teardown). The first heartbeat of the waiting state, and a request already in flight, still have only the HTTP timeouts. The log says what was left behind, the upload threads or only a heartbeat tick.pending_uploadsrow for good and counted "Queued file missing" attempts toward the cancel. The uploader now asks whether the whole folder is gone first. If it is, the row or queue entry stays, nothing is counted, and the check is requested at once.watch_directory can't be watched: <path>and retries on the waiting state's backoff, so an emitter that dies at once doesn't cause a rescan at every check. When watchdog is still watching and only the folder's identity differs (FUSE mounts withoutuse_ino, SMB or virtual drives without persistent file IDs report a new number at every look), the watcher keeps the runtime, logs both identities once, and stops comparing the identity until the next config change or restart, so live uploads don't stop. The rebuild of a blind runtime is bounded like the move, because the identity can change while upload threads are blocked on the old filesystem.watcher_startedevent that says the watcher resumed.watcher_startedevent says it resumed. A runtime does not start next to threads that the move left behind on the same folder. While they run, the waiting reason saysuploads stuck on <folder> have not ended, in place of the stale "not found". A config change that moves the watch folder doesn't wait for them (they can't touch the new folder), and one that keeps the folder waits up to 2 minutes, logs every minute, and ends on a stop. Asking whether those threads have exited logs nothing, where it wrote a line about twice a second.A stop during
watchstart-up.watchinstalled its Ctrl+C and SIGTERM handlers only after the supervisor had started, so a SIGTERM during the config sync, the first heartbeat or the start-up scan ended the process and Ctrl+C raisedKeyboardInterrupt, with nostoppedheartbeat and nowatcher_stoppedevent. The handlers are now installed before the start. They callWatcherSupervisor.request_stop, the supervisor's one stop signal, and so does the Windows service (from a thread that starts beforesupervisor.start()). The start-up scan reads it between entries and ends early, so a stop during a long scan is acted on within seconds, and the normal clean stop follows, exit code 0. The files the scan hadn't reached are scanned at the next start: the scan writes nothing to the state database for them, and a baseline scan on the first start of anew-onlyenvironment records nothing at all when it stops early, because a partial baseline would make the next start upload the rest of the backlog as new. The upload worker isn't started for a watcher that is already stopping, andpollleaves a rebuild or a folder check alone once a stop is requested. The config sync and the first heartbeat are not cut short: each has its HTTP timeout.The release is watcher 1.4.3 (version, lock file and docs snapshots). There is no changelog entry, because it is a watcher-only release.
Docs
concepts.mdx,manage-watchers.mdxandtroubleshoot-a-watcher.mdxnow says the watcher also goes back to waiting when its folder disappears while it runs, and resumes when it is back.windows-service.mdxsays a stop ends the scan at its next file from 1.4.3.docs/developer/watcher.mddescribes the stop signal and the scan stop, the bounded and cancelable look and the second look, what the move to the waiting state includes, which of its waits are bounded and end on a stop and what a stop still waits for, the folder identity check and its fallback, the wait for a runtime that keeps going blind, and the waits for threads that a move left behind. The known-bug examples in the end-to-end docs use a placeholder that is not a real issue number.How it was tested
make check: clean, and it rewrote nothing.make py-test-unit: 2486 passed. The changed unit test files also pass on Python 3.12 in a separate environment (452 passed).make py-test-integration(with its own test database): 95 passed.watchcommand:watcher/e2e/test_setup_startup.pyandtest_shutdown.py, 32 passed. The Windows service variants were not run here.uv run pytest watcher/e2e --collect-onlycollects 208 tests, andwatcher/tests/test_e2e_catalog.pypasses (56).make docs-catalogschanges no snapshot.🤖 Generated with Claude Code