Repository: vercel/next.js
Pull request: Fix DashMap read-write self-deadlock in task_cache causing hangs

### What?

Fixes a deadlock in the Turbopack backend's `task_cache` DashMap that causes `next` to hang during incremental builds with persistent caching.

### Why?

In `get_or_create_persistent_task` and `get_or_create_transient_task`, `self.task_cache.get(&task_type)` returns a `dashmap::Ref` that holds a **read lock** on a DashMap shard. Although the `TaskId` was copied out with `let task_id = *task_id`, the original `Ref` was not dropped until the end of the `if let` block — which includes the call to `ConnectChildOperation::run`.

`ConnectChildOperation::run` can trigger `AggregationUpdateQueue::process` → `for_each_task_all` → `prepare_tasks_with_callback`, which calls `task_cache.entry(task_type).or_insert(task_id)` — requiring a **write lock** on a `task_cache` shard. When this targets the same shard that the current thread already holds a read lock on, the thread self-deadlocks because dashmap's `RawRwLock` is not reentrant.

Once one thread self-deadlocks, all other tokio worker threads needing that shard pile up behind it, eventually causing a complete system hang where every worker is stuck in `dashmap::lock::RawRwLock::lock_exclusive_slow`.

This was observed as a hang during the 7th incremental build with persistent caching of a large application, with a macOS `sample` trace showing all tokio-runtime-worker threads blocked on `_pthread_cond_wait` inside `lock_exclusive_slow` on the `DashMap<Arc<CachedTaskType>, TaskId>`.

### How?

Use `.map(|r| *r)` to copy the `TaskId` out and drop the `Ref` (releasing the read lock) before calling `ConnectChildOperation::run`:

```rust
// Before: Ref lives until end of `if let` block
if let Some(task_id) = self.task_cache.get(&task_type) {
    let task_id = *task_id;
    ConnectChildOperation::run(...); // read lock still held!
}

// After: Ref dropped by .map(), read lock released
if let Some(task_id) = self.task_cache.get(&task_type).map(|r| *r) {
    ConnectChildOperation::run(...); // no lock held
}
```

Applied to both `get_or_create_persistent_task` and `get_or_create_transient_task`.


Changed files:
filename: turbopack/crates/turbo-tasks-backend/src/backend/mod.rs
patch: @@ -1532,8 +1532,9 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
         // Create a single ExecuteContext for both lookup and connect_child
         let mut ctx = self.execute_context(turbo_tasks);
         // First check if the task exists in the cache which only uses a read lock
-        if let Some(task_id) = self.task_cache.get(&task_type) {
-            let task_id = *task_id;
+        // .map(|r| *r) copies the TaskId and drops the DashMap Ref (releasing the read lock)
+        // before ConnectChildOperation::run, which may re-enter task_cache with a write lock.
+        if let Some(task_id) = self.task_cache.get(&task_type).map(|r| *r) {
             self.track_cache_hit(&task_type);
             operation::ConnectChildOperation::run(
                 parent_task,
@@ -1614,9 +1615,10 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
             );
         }
         let mut ctx = self.execute_context(turbo_tasks);
-        // First check if the task exists in the cache which only uses a read lock
-        if let Some(task_id) = self.task_cache.get(&task_type) {
-            let task_id = *task_id;
+        // First check if the task exists in the cache which only uses a read lock.
+        // .map(|r| *r) copies the TaskId and drops the DashMap Ref (releasing the read lock)
+        // before ConnectChildOperation::run, which may re-enter task_cache with a write lock.
+        if let Some(task_id) = self.task_cache.get(&task_type).map(|r| *r) {
             self.track_cache_hit(&task_type);
             operation::ConnectChildOperation::run(
                 parent_task,