next.js
fb3535d4 - [turbopack] Fix a potential deadlock in scope_and_block (#95695)

Commit
1 day ago
[turbopack] Fix a potential deadlock in scope_and_block (#95695) ### What? Fix a potential deadlock in `scope_and_block` (the CPU fan-out primitive in `turbopack/crates/turbo-tasks/src/scope.rs`) by routing every job through a single shared work queue, so completion never depends on a spawned worker being scheduled. ### Why? `scope_and_block` runs a batch of jobs across the tokio runtime's worker threads while the calling thread blocks. Previously, jobs at indices `1..=WORKER_TASKS` were handed *exclusively* to freshly `handle.spawn`ed worker tasks and never placed on the shared queue — but the calling thread only drains the queue, so it could not run those jobs itself. Each spawned worker runs synchronous code and parks on a `parking_lot::Condvar` (no `.await`, no `block_in_place`), so once scheduled it holds its runtime core for the whole scope. When the runtime has fewer worker threads than host CPUs, or they are already occupied, those workers may never get a core. Their exclusively-assigned jobs then never run, `remaining_tasks` never reaches 0, and the caller blocks forever. ### How? - **Every job goes on one shared queue.** Spawned helpers are now a pure optimization that pull from the same queue; they are never assigned a dedicated job. The calling thread drains the whole queue itself in `end_and_help_complete`, so liveness never depends on a helper being scheduled. - **Runtime-accurate helper cap.** The helper count is `num_workers().min(number_of_tasks) - 1` (per-scope, from `Handle::current().metrics()`) instead of a process-global host-CPU constant. - **Close via a flag, not a sentinel.** The queue carries a `closed` bit guarded by the same lock as the jobs. `end_and_help_complete` sets it and `notify_all`s once; a drainer that finds the queue empty exits when closed or parks otherwise. This replaces the previous `End` token that had to be ping-ponged across drainers. - **Wakeup correctness/perf.** `pick_job_from_work_queue` hands off a surplus `notify_one` when work remains (parking_lot notifications are not latched), and the enqueue-time `notify_one` re-wakes a parked helper — the bootstrap wakeup the hand-off cannot provide from a fully-parked state, which matters most on thread-limited runtimes. - The `VecDeque` is presized to the job count so `push_back` never reallocates under the queue lock. Added a deterministic regression test (`test_scope_worker_threads_occupied`): it pins every runtime worker thread with a synchronous sleep, runs the scope on `spawn_blocking`, and asserts it completes well before the sleep releases. This fails (cleanly, via timeout) before the fix and passes after. Closes NEXT-
Author
Parents
Loading