Skip to content

Harden Metro bundle retry against file-watcher races (#56530)#56530

Closed
rubennorte wants to merge 1 commit into
react:mainfrom
rubennorte:export-D101791796
Closed

Harden Metro bundle retry against file-watcher races (#56530)#56530
rubennorte wants to merge 1 commit into
react:mainfrom
rubennorte:export-D101791796

Conversation

@rubennorte

@rubennorte rubennorte commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary:

Each Fantom test writes a unique \-<test>.js entrypoint into
.out/js-builds/ and then asks Metro to bundle it. Metro's file watcher
(metro-file-map's FallbackWatcher on Linux, debounced 100 ms) does not
always observe the new entrypoint by the time the HTTP request arrives,
especially when multiple workers are writing entrypoints concurrently.
The previous retry logic was too narrow:

  • Only HTTP 404 was treated as retryable. Metro returns 404 only when the
    entry file path itself can't be resolved; an unresolved transitive
    dep (e.g. setUpDefaultReactNativeEnvironment) returns HTTP 500 with
    {type: 'UnableToResolveError'} — we'd throw immediately on that.
  • Only 3 attempts with a flat 500 ms wait (~1 s total), which is not
    enough on a busy host with 8 workers writing entrypoints at once.

This results in ~30 spurious "Failed to request bundle from Metro: Unable
to resolve module ..." failures per run.

Refactor createBundle into a focused fetchBundleWithRetry helper that:

  • Parses Metro's JSON error envelope ({type, message, ...} from
    formatBundlingError) once per response and uses type to decide
    whether to retry. Retries on HTTP 404, on HTTP 500 with
    UnableToResolveError or ResourceNotFoundError, and on transient
    fetch network errors. All other failures (transform errors, syntax
    errors, real config issues) throw immediately so we don't waste seconds
    on them.
  • Uses exponential backoff (100 ms → 200 → 400 → 800 → 1.6 s, capped at
    2 s) with up to 10 attempts (~11 s total worst case).
  • Surfaces a clean error message (parsed from the JSON envelope) when
    retries are exhausted.

Changelog: [Internal]

Reviewed By: andrewdacenko

Differential Revision: D101791796

@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 Apr 21, 2026
@meta-codesync

meta-codesync Bot commented Apr 21, 2026

Copy link
Copy Markdown

@rubennorte has exported this pull request. If you are a Meta employee, you can view the originating Diff in D101791796.

@meta-codesync meta-codesync Bot changed the title Harden Metro bundle retry against file-watcher races Harden Metro bundle retry against file-watcher races (#56530) Apr 21, 2026
rubennorte added a commit to rubennorte/react-native that referenced this pull request Apr 21, 2026
Summary:

Each Fantom test writes a unique `\-<test>.js` entrypoint into
`.out/js-builds/` and then asks Metro to bundle it. Metro's file watcher
(metro-file-map's `FallbackWatcher` on Linux, debounced 100 ms) does not
always observe the new entrypoint by the time the HTTP request arrives,
especially when multiple workers are writing entrypoints concurrently.
The previous retry logic was too narrow:

- Only HTTP 404 was treated as retryable. Metro returns 404 only when the
  entry file path itself can't be resolved; an unresolved transitive
  dep (e.g. `setUpDefaultReactNativeEnvironment`) returns HTTP 500 with
  `{type: 'UnableToResolveError'}` — we'd throw immediately on that.
- Only 3 attempts with a flat 500 ms wait (~1 s total), which is not
  enough on a busy host with 8 workers writing entrypoints at once.

This results in ~30 spurious "Failed to request bundle from Metro: Unable
to resolve module ..." failures per run.

Refactor `createBundle` into a focused `fetchBundleWithRetry` helper that:

- Parses Metro's JSON error envelope (`{type, message, ...}` from
  `formatBundlingError`) once per response and uses `type` to decide
  whether to retry. Retries on HTTP 404, on HTTP 500 with
  `UnableToResolveError` or `ResourceNotFoundError`, and on transient
  `fetch` network errors. All other failures (transform errors, syntax
  errors, real config issues) throw immediately so we don't waste seconds
  on them.
- Uses exponential backoff (100 ms → 200 → 400 → 800 → 1.6 s, capped at
  2 s) with up to 10 attempts (~11 s total worst case).
- Surfaces a clean error message (parsed from the JSON envelope) when
  retries are exhausted.

Changelog: [Internal]

Differential Revision: D101791796
Summary:
Pull Request resolved: react#56530

Each Fantom test writes a unique `\-<test>.js` entrypoint into
`.out/js-builds/` and then asks Metro to bundle it. Metro's file watcher
(metro-file-map's `FallbackWatcher` on Linux, debounced 100 ms) does not
always observe the new entrypoint by the time the HTTP request arrives,
especially when multiple workers are writing entrypoints concurrently.
The previous retry logic was too narrow:

- Only HTTP 404 was treated as retryable. Metro returns 404 only when the
  entry file path itself can't be resolved; an unresolved transitive
  dep (e.g. `setUpDefaultReactNativeEnvironment`) returns HTTP 500 with
  `{type: 'UnableToResolveError'}` — we'd throw immediately on that.
- Only 3 attempts with a flat 500 ms wait (~1 s total), which is not
  enough on a busy host with 8 workers writing entrypoints at once.

This results in ~30 spurious "Failed to request bundle from Metro: Unable
to resolve module ..." failures per run.

Refactor `createBundle` into a focused `fetchBundleWithRetry` helper that:

- Parses Metro's JSON error envelope (`{type, message, ...}` from
  `formatBundlingError`) once per response and uses `type` to decide
  whether to retry. Retries on HTTP 404, on HTTP 500 with
  `UnableToResolveError` or `ResourceNotFoundError`, and on transient
  `fetch` network errors. All other failures (transform errors, syntax
  errors, real config issues) throw immediately so we don't waste seconds
  on them.
- Uses exponential backoff (100 ms → 200 → 400 → 800 → 1.6 s, capped at
  2 s) with up to 10 attempts (~11 s total worst case).
- Surfaces a clean error message (parsed from the JSON envelope) when
  retries are exhausted.

Changelog: [Internal]

Reviewed By: andrewdacenko

Differential Revision: D101791796
@meta-codesync meta-codesync Bot closed this in c1ffe9f Apr 21, 2026
@facebook-github-tools facebook-github-tools Bot added the Merged This PR has been merged. label Apr 21, 2026
@meta-codesync

meta-codesync Bot commented Apr 21, 2026

Copy link
Copy Markdown

This pull request has been merged in c1ffe9f.

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. fb-exported Merged This PR has been merged. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant