Repository: vercel/next.js
Pull request: Turbopack: fix hanging dev server and builds with fs cache

### What?

Fixes a deadlock due to concurrent task locks.
Also adds a debug check to prevent this in future.

Changed files:
filename: turbopack/crates/turbo-tasks-backend/src/backend/mod.rs
patch: @@ -469,11 +469,17 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
         self.assert_not_persistent_calling_transient(reader, task_id, /* cell_id */ None);
 
         let mut ctx = self.execute_context(turbo_tasks);
-        let (mut task, reader_task) = if self.should_track_dependencies()
+        let need_reader_task = if self.should_track_dependencies()
             && !matches!(options.tracking, ReadTracking::Untracked)
+            && reader.is_some_and(|reader_id| reader_id != task_id)
             && let Some(reader_id) = reader
             && reader_id != task_id
         {
+            Some(reader_id)
+        } else {
+            None
+        };
+        let (mut task, mut reader_task) = if let Some(reader_id) = need_reader_task {
             // Having a task_pair here is not optimal, but otherwise this would lead to a race
             // condition. See below.
             // TODO(sokra): solve that in a more performant way.
@@ -534,6 +540,7 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
                     break;
                 }
                 drop(task);
+                drop(reader_task);
                 {
                     let _span = tracing::trace_span!(
                         "make root node for strongly consistent read",
@@ -549,7 +556,13 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
                         &mut ctx,
                     );
                 }
-                task = ctx.task(task_id, TaskDataCategory::All);
+                (task, reader_task) = if let Some(reader_id) = need_reader_task {
+                    // TODO(sokra): see comment above
+                    let (task, reader) = ctx.task_pair(task_id, reader_id, TaskDataCategory::All);
+                    (task, Some(reader))
+                } else {
+                    (ctx.task(task_id, TaskDataCategory::All), None)
+                }
             }
 
             let is_dirty =
@@ -697,6 +710,7 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
                         )
                     }
                 });
+                drop(reader_task);
                 drop(task);
                 if !task_ids_to_schedule.is_empty() {
                     let mut queue = AggregationUpdateQueue::new();
@@ -893,6 +907,7 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
                 ctx.get_task_description(task_id)
             );
         }
+        drop(reader_task);
 
         // Cell should exist, but data was dropped or is not serializable. We need to recompute the
         // task the get the cell content.

filename: turbopack/crates/turbo-tasks-backend/src/backend/operation/mod.rs
patch: @@ -85,6 +85,8 @@ where
     turbo_tasks: &'e dyn TurboTasksBackendApi<TurboTasksBackend<B>>,
     _operation_guard: Option<OperationGuard<'e, B>>,
     transaction: TransactionState<'e, 'tx, B>,
+    #[cfg(debug_assertions)]
+    active_task_locks: Arc<std::sync::atomic::AtomicU8>,
 }
 
 impl<'e, 'tx, B: BackingStorage> ExecuteContextImpl<'e, 'tx, B>
@@ -100,6 +102,8 @@ where
             turbo_tasks,
             _operation_guard: Some(backend.start_operation()),
             transaction: TransactionState::None,
+            #[cfg(debug_assertions)]
+            active_task_locks: Arc::new(std::sync::atomic::AtomicU8::new(0)),
         }
     }
 
@@ -113,6 +117,8 @@ where
             turbo_tasks,
             _operation_guard: Some(backend.start_operation()),
             transaction: TransactionState::Borrowed(transaction),
+            #[cfg(debug_assertions)]
+            active_task_locks: Arc::new(std::sync::atomic::AtomicU8::new(0)),
         }
     }
 
@@ -180,6 +186,14 @@ where
     }
 
     fn task(&mut self, task_id: TaskId, category: TaskDataCategory) -> Self::TaskGuardImpl {
+        #[cfg(debug_assertions)]
+        if self.active_task_locks.fetch_add(1, Ordering::AcqRel) != 0 {
+            panic!(
+                "Concurrent task lock acquisition detected. This is not allowed and indicates a \
+                 bug. It can lead to deadlocks."
+            );
+        }
+
         let mut task = self.backend.storage.access_mut(task_id);
         if !task.state().is_restored(category) {
             if task_id.is_transient() {
@@ -208,6 +222,8 @@ where
             backend: self.backend,
             #[cfg(debug_assertions)]
             category,
+            #[cfg(debug_assertions)]
+            active_task_locks: self.active_task_locks.clone(),
         }
     }
 
@@ -228,6 +244,14 @@ where
         task_id2: TaskId,
         category: TaskDataCategory,
     ) -> (Self::TaskGuardImpl, Self::TaskGuardImpl) {
+        #[cfg(debug_assertions)]
+        if self.active_task_locks.fetch_add(2, Ordering::AcqRel) != 0 {
+            panic!(
+                "Concurrent task lock acquisition detected. This is not allowed and indicates a \
+                 bug. It can lead to deadlocks."
+            );
+        }
+
         let (mut task1, mut task2) = self.backend.storage.access_pair_mut(task_id1, task_id2);
         let is_restored1 = task1.state().is_restored(category);
         let is_restored2 = task2.state().is_restored(category);
@@ -264,13 +288,17 @@ where
                 backend: self.backend,
                 #[cfg(debug_assertions)]
                 category,
+                #[cfg(debug_assertions)]
+                active_task_locks: self.active_task_locks.clone(),
             },
             TaskGuardImpl {
                 task: task2,
                 task_id: task_id2,
                 backend: self.backend,
                 #[cfg(debug_assertions)]
                 category,
+                #[cfg(debug_assertions)]
+                active_task_locks: self.active_task_locks.clone(),
             },
         )
     }
@@ -328,6 +356,8 @@ impl<'e, B: BackingStorage> ChildExecuteContext<'e> for ChildExecuteContextImpl<
             turbo_tasks: self.turbo_tasks,
             _operation_guard: None,
             transaction: TransactionState::None,
+            #[cfg(debug_assertions)]
+            active_task_locks: Arc::new(std::sync::atomic::AtomicU8::new(0)),
         }
     }
 }
@@ -376,6 +406,15 @@ pub struct TaskGuardImpl<'a, B: BackingStorage> {
     backend: &'a TurboTasksBackendInner<B>,
     #[cfg(debug_assertions)]
     category: TaskDataCategory,
+    #[cfg(debug_assertions)]
+    active_task_locks: Arc<std::sync::atomic::AtomicU8>,
+}
+
+#[cfg(debug_assertions)]
+impl<B: BackingStorage> Drop for TaskGuardImpl<'_, B> {
+    fn drop(&mut self) {
+        self.active_task_locks.fetch_sub(1, Ordering::AcqRel);
+    }
 }
 
 impl<B: BackingStorage> TaskGuardImpl<'_, B> {