ITADN

bun test --parallel hangs after all tests pass (teardown deadlock) — not reproducible without --parallel, no single culprit file

#34214Closedkurobe-glv 创建于 2026-07-15
bugneeds triage
K
kurobe-glvcommented
### What version of Bun is running? `1.3.14` ### What platform is your computer? **CI:** GitHub Actions `ubuntu-latest` (Linux x86_64) ### What steps can reproduce the bug? We run a ~4,400-test / ~450-file Next.js + React Testing Library suite via: ``` bun test --parallel --timeout=20000 <files...> ``` split into 4 shards with `--shard=N/4`. One shard (call it shard A, a fixed set of ~112 files under a deterministic `--shard` split) consistently hangs after all tests finish: every test reports `(pass)`, but the process never prints the final `Ran N tests across M files` summary and never exits. We wrap the run in a watchdog that kills the process after 120s of no new stdout — by that point every individual test has already logged pass with 0 failures, so the hang is strictly a **post-test-completion** hang, not a stuck test. ### Isolating the cause: same file set, only the run mode changes We bisected shard A's 112 files down to a 28-file slice (`q1`) that reliably reproduces the hang, then ran that exact same 28-file list three different ways: | Command | Result | Time | |---|---|---| | `bun test --parallel --timeout=20000 <q1 files>` | **Hangs** — no final summary, watchdog has to kill it | — | | `bun test --timeout=20000 <q1 files>` (single process, no `--parallel`) | Completes normally — `Ran 224 tests across 28 files`, 0 fail | 52s | | `bun test --isolate --timeout=20000 <q1 files>` (single process, per-file isolation) | Completes normally — 224 pass / 0 fail | 337s | Since the identical test files complete cleanly both without `--parallel` and with `--isolate` (single process), the hang is not caused by a leaked handle/timer/connection in our test code or its dependencies — it only appears when `--parallel` spawns/tears down worker subprocesses. ### No single culprit file — it's an emergent, count-dependent race We tried to narrow `q1` (28 files) down further to find "the" offending file: | Slice tested with `--parallel` | Result | |---|---| | `q1` — 28 files | **Hangs** | | `e1` — first 14 files of `q1` | Clean | | Suspected file (from earlier narrowing) run alone + 1 unrelated file (2 files total) | Clean | So the hang doesn't reproduce with a small number of files, but does once enough files accumulate (somewhere between 14 and 28 in this slice) — it looks like teardown of N parallel workers races once the worker pool has enough concurrent/queued work, rather than one file leaking a specific resource. This matches what we see across all 4 shards in CI: they're roughly the same file count (~110 each), but only one shard's specific *composition* of files reliably crosses whatever internal threshold triggers the race — the other 3 shards finish and exit normally in the same CI run. ### What we ruled out before landing on this - **Not `--only`/`.only`-related** — no `.only` in the suite. - **Not a leaked `setTimeout`/`setInterval` from app code** — we already wrap `setTimeout`/`setInterval` globally in a test preload to call `.unref()`, and explicitly `jsdomWindow.close()` in `afterAll`, before this investigation. Those fixes eliminated most hangs; this is what's left. - **Not `mock.module` registry leakage across files** (a separate, known Bun behavior we work around already) — the hang reproduces with a plain single-process run of the exact same files, so cross-file mock state isn't at play here; the failure mode is specifically tied to the parallel worker pool's teardown. - **Not resolved by reducing to sequential CI shards of the same size** — a 5-way shard split (vs our normal 4-way) still hung on one shard, so it's not a fixed "shard index" issue, just whichever shard's file set crosses the threshold. ### What is the expected behavior? `bun test --parallel` should exit with a summary once every test file has reported pass/fail, the same way a non-`--parallel` run does — regardless of how many files/workers are involved. ### What do you see instead? _No response_ ### Additional information - We work around this in CI with a watchdog wrapper that kills the process after N seconds of stdout silence and judges pass/fail from the already-emitted per-test output (0 fails + no "Unhandled error between tests" + at least one file ran ⇒ treat as success). This works, but it means the final coverage report (`bun test --coverage`, which writes `lcov.info` at the very end) is lost for whichever shard hits the hang that run, since the process is killed before it flushes coverage output. - `--isolate` avoids the hang entirely (runs each file in its own context within one process) but is ~6x slower for us (~5.5 min → ~5.5s per 28 files vs the numbers above scaled up would put a ~112-file shard at ~20+ min), which is not viable for our CI budget. - Happy to share the full file list / a minimal reproduction repo if useful — our current repro is entangled with an internal test preload (jsdom setup, MSW, module-mock guard), so we'd need to strip it down to a synthetic suite first. Let us know if that would help and we'll prepare one.
关闭于 2026-07-15 1 条评论