Skip to content

Release workers and watchers after failed Bundler initialisation - #1963

Merged
robhogan merged 2 commits into
mainfrom
pr1963
Sep 28, 2026
Merged

robhogan merged 2 commits into
mainfrom
pr1963

Conversation

@robhogan

@robhogan robhogan commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #1854, which made Bundler initialisation errors reject ready().

This fixes cases where a failed transformer could lead Metro to hang in a zombie state.

Teardown still didn't account for a failed initialisation, so Metro could leak worker processes and file watchers when, for example, the transformer fails to load:

  • Transformer created its WorkerFarm - which spawns jest-worker child processes when maxWorkers > 1 - before computing the transform cache key. getTransformCacheKey is exactly where a missing transformer or Babel preset throws ([Metro 0.84.x] Silent catch in Bundler._initializedPromise causes misleading Cannot read properties of undefined (reading 'transformFile') instead of the real error聽#1808), and when it did, the constructor threw away the only reference to the farm, so nothing could kill it. Computing the cache key first means any constructor failure happens before workers start.
  • Bundler.end() awaited ready() before ending anything, so after a failed initialisation it rejected without calling DependencyGraph.end(), leaving the file map's watcher, health check interval and file processor running. It now waits for initialisation to settle, ends the transformer if one was constructed, and always ends the dependency graph.
  • DependencyGraph.end() had the same shape with respect to a failed fileMap.build(), so it now ends the file map regardless.

Initialisation errors are still surfaced through ready() and the reporter - end() now resolves either way, since teardown should complete even if startup didn't.

(Before #1854, end() failed at the same point by dereferencing an undefined _transformer, so none of this is a regression.)

Changelog:

 - **[Fix]**: Release transform workers and file watchers when Metro fails to initialise

Test plan

End to end - a project whose transformerPath points at a module that throws on load, the same way a missing Babel preset does:

const error = new Error("Cannot find module 'babel-preset-missing'");
error.code = 'MODULE_NOT_FOUND';
throw error;

with maxWorkers: 2, resolver.useWatchman: false and a no-op cache store (the transform cache key is only computed with a cache enabled). A script runs Metro from source, either through Metro.runBuild() or through Metro.runMetro(config, {watch: true}) followed by server.ready() and server.end(). It then waits 1s, counts its own child processes, lists process.getActiveResourcesInfo(), and forces an exit if it's still alive 10s later.

main (1f83bfc) Transformer change only This diff
runBuild() Rejects. 2 worker processes left running, hangs until forced exit Rejects. Nothing left running, exits Rejects. Nothing left running, exits
runMetro(), end() end() rejects. 2 worker processes and the file map's change interval left running, hangs until forced exit end() rejects. Change interval left running, hangs until forced exit end() resolves. Nothing left running, exits

So the Transformer reordering is enough for one-shot builds, and the end() changes are needed as well for a watching server.

Unit tests - new cases in Bundler-test, Transformer-test and DependencyGraph-test. The three covering the fixes each fail without the source changes and pass with them.

yarn jest packages/metro
yarn flow check
yarn lint

Summary:
Follow-up to #1854, which made `Bundler` initialisation errors reject `ready()`. Teardown still didn't account for a failed initialisation, so Metro could leak worker processes and file watchers when, for example, the transformer fails to load:

 - `Transformer` created its `WorkerFarm` - which spawns `jest-worker` child processes when `maxWorkers > 1` - *before* computing the transform cache key. `getTransformCacheKey` is exactly where a missing transformer or Babel preset throws (#1808), and when it did, the constructor threw away the only reference to the farm, so nothing could kill it. Computing the cache key first means any constructor failure happens before workers start.
 - `Bundler.end()` awaited `ready()` before ending anything, so after a failed initialisation it rejected without calling `DependencyGraph.end()`, leaving the file map's watcher, health check interval and file processor running. It now waits for initialisation to settle, ends the transformer if one was constructed, and always ends the dependency graph.
 - `DependencyGraph.end()` had the same shape with respect to a failed `fileMap.build()`, so it now ends the file map regardless.

Initialisation errors are still surfaced through `ready()` and the reporter - `end()` now resolves either way, since teardown should complete even if startup didn't.

(Before #1854, `end()` failed at the same point by dereferencing an undefined `_transformer`, so none of this is a regression.)

Changelog:
```
 - **[Fix]**: Release transform workers and file watchers when Metro fails to initialise
```

Test plan:
New tests in `Bundler-test`, `Transformer-test` and `DependencyGraph-test` cover each case, and fail without the corresponding fix.
```
yarn jest packages/metro
yarn flow check
yarn lint
```
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 23, 2026
@robhogan
robhogan marked this pull request as ready for review September 24, 2026 13:22
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Sep 24, 2026
@robhogan
robhogan requested a lite review from Copilot September 25, 2026 09:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

馃煛 Changes recommended

Ensure dependency-graph cleanup runs even when transformer teardown rejects.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Fixes Metro resource leaks when Bundler or DependencyGraph initialization fails.

Changes:

  • Defers transformer worker creation until cache-key computation succeeds.
  • Makes teardown resilient to failed initialization.
  • Adds regression tests for worker and file-map cleanup.

The Bundler teardown should still ensure DependencyGraph.end() runs if transformer cleanup rejects.

File Description
packages/鈥媘etro/鈥媠rc/鈥媙ode-haste/鈥婦ependencyGraph.js Cleans up the file map after failed builds.
packages/鈥媘etro/鈥媠rc/鈥媙ode-haste/鈥媉_tests__/鈥婦ependencyGraph-test.js Tests file-map cleanup.
packages/鈥媘etro/鈥媠rc/鈥婦eltaBundler/鈥婽ransformer.js Delays worker creation until initialization succeeds.
packages/鈥媘etro/鈥媠rc/鈥婦eltaBundler/鈥媉_tests__/鈥婽ransformer-test.js Tests worker suppression on cache-key failure.
packages/鈥媘etro/鈥媠rc/鈥婤undler.js Handles teardown after failed initialization.
packages/鈥媘etro/鈥媠rc/鈥媉_tests__/鈥婤undler-test.js Tests Bundler cleanup behavior.

馃挕 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/metro/src/Bundler.js Outdated
If `Transformer.end()` rejected - for example, when the worker farm had already ended - `Bundler.end()` never reached `DependencyGraph.end()`, leaking the file map's watcher and timers. Wrap it in `try`/`finally` so the dependency graph is always ended, while still rejecting with the Transformer's error.

Addresses review feedback on #1963.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

馃煝 Approval recommended

No unresolved issues were identified, and all reviewed changes are covered by regression tests.

Review effort: Lite
Findings: None

Resolved since last review (1)

@robhogan
robhogan requested a review from vzaidman September 25, 2026 09:31
@robhogan
robhogan merged commit 8751591 into main Sep 28, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants