Repository navigation
Report watcher backend errors without terminating Metro #2147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -322,13 +322,34 @@ export class Watcher extends EventEmitter { | |
| clocks: rootClocks, | ||
| }; | ||
| const watcher: WatcherBackend = new WatcherImpl(root, watcherOptions); | ||
| const reportedResourceLimits = new Set<string>(); | ||
|
|
||
| return new Promise(async (resolve, reject) => { | ||
| return new Promise((resolve, reject) => { | ||
| const rejectTimeout = setTimeout( | ||
| () => reject(new Error('Failed to start watch mode.')), | ||
| MAX_WAIT_TIME, | ||
| ); | ||
|
|
||
| watcher.onError(error => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should fix · An error on NativeWatcher's FSWatcher still terminates Metro This listener only sees errors that go through Failure case: on macOS without Watchman, Metro is running on Suggested fix: in Reproduced:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 0faadf1. NativeWatcher forwards the filesystem handle's error event through the backend error channel. Tests cover original-error identity, continued file delivery, and safe late errors after stopping. |
||
| const code = (error as interface {readonly code?: unknown}).code; | ||
| let guidance = ''; | ||
| if (code === 'ENOSPC' || code === 'EMFILE') { | ||
| // A resource limit can fail once per directory in a large crawl. | ||
| if (reportedResourceLimits.has(code)) { | ||
| return; | ||
| } | ||
| reportedResourceLimits.add(code); | ||
| guidance = | ||
| ' Changes in unwatched directories will not be detected. ' + | ||
| (code === 'ENOSPC' | ||
| ? 'Increase fs.inotify.max_user_watches on Linux.' | ||
| : 'Increase the open file limit (ulimit -n).'); | ||
| } | ||
| this.#options.console.error( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should fix · Hitting the watch limit logs a separate error and stack for every remaining directory
Failure case: on Linux, a project with more directories than Suggested fix: report resource-limit errors once per backend, or as one summary after Reproduced:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in e051d28. ENOSPC and EMFILE are reported once per code per backend instead of once per remaining directory, with explicit guidance that unwatched directories will not deliver changes. The regression uses the real FallbackWatcher over 128 temporary directories with simulated fs.watch limit failures, verifies the crawl still visits them, and verifies a separate root gets its own diagnostic. No actual OS-wide watch limit was exhausted. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should fix · A lost FallbackWatcher directory watch is logged without saying changes will be missed When a directory's Failure case: on Linux or Windows, the Suggested fix: in Reproduced:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 0faadf1. A lost directory watch reports its path, the missed-change consequence, and restart guidance. The wrapper preserves the original cause and error code, retaining ENOSPC/EMFILE deduplication and existing ignorable-error behavior. Tests exercise real fallback watcher startup with controlled failing handles. |
||
| `metro-file-map: ${this.#activeWatcher ?? 'unknown'} watcher error in ${root}:${guidance}`, | ||
| error, | ||
| ); | ||
| }); | ||
| watcher.onFileEvent(change => { | ||
| const basename = path.basename(change.relativePath); | ||
| if (basename.startsWith(this.#options.healthCheckFilePrefix)) { | ||
|
|
@@ -358,9 +379,18 @@ export class Watcher extends EventEmitter { | |
| this.#bufferedEvents.push(change); | ||
| } | ||
| }); | ||
| await watcher.startWatching(); | ||
| clearTimeout(rejectTimeout); | ||
| resolve(watcher); | ||
| void Promise.resolve() | ||
| .then(() => watcher.startWatching()) | ||
| .then( | ||
| () => { | ||
| clearTimeout(rejectTimeout); | ||
| resolve(watcher); | ||
| }, | ||
| error => { | ||
| clearTimeout(rejectTimeout); | ||
| reject(error); | ||
| }, | ||
| ); | ||
| }); | ||
| }; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should fix · A failed Watchman reconnect now leaves a permanently dead watcher with one log line
Logging is the right result for a one-off
lstatfailure, but for a Watchman client error it means watching has stopped for good, and nothing recovers or says so.Failure case: Watchman goes away, so the client gets
endandWatchmanWatcher.#initreconnects once. If that attempt fails (socketconnecterror, orwatchman get-socknamecan't spawn),fb-watchmanemitserrorbut notend, so no further retry happens. Before this PR the process exited and the user restarted it. Now Metro keeps running and serves stale bundles, because no file event ever arrives again. The only signal is onewatchman watcher errorline that doesn't say changes are no longer seen. The health check that could expose this defaults toenabled: false.From reading
fb-watchman, a socketerrorwithoutendon an established connection (e.g.ECONNRESET) reaches the same state without even one reconnect attempt. I did not exercise that path.Suggested fix: handle this in
WatchmanWatcher: on a client error or failed#init, retry with backoff until a new subscription is live, since a re-subscribe can recover. If retrying is out of scope, keep Watchman connection failures fatal, or at least log clearly that file watching has stopped and Metro must be restarted. A test should cover connection loss followed by a failed reconnect.Reproduced:
repro/watchman-repro.js dead, using the realWatcherandfb-watchmanagainst a fake Watchman socket. The server closes, the reconnect fails, then the server returns. Head logswatchman watcher error ... connect ENOENT, then printsRESULT connections after Watchman came back: 0after 5s. Base exits withRESULT UNCAUGHT: connect ENOENT. · id79065045There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in e051d28. Failed Watchman reconnects now retry with exponential backoff from 1 second up to 30 seconds, resetting after a successful subscription. Coverage includes transport, watch-project, clock and subscribe failures, a later successful subscription, continued file events and cancellation on stop. The existing fresh-clock semantics are unchanged; this does not claim replay of changes during an outage. Full validation passes: 165 suites, 3,076 tests, 495 snapshots (14 skipped), plus Flow/lint/format/build.