Repository: nodejs/node
Pull request #57910: src: improve thread safety of `TaskQueue`
Author: login: codebytere
Labels: name: c++
description: Issues and PRs that require attention from people who are familiar with C++.
Created: 2025-04-17T12:47:23Z | Merged: 2025-04-22T08:42:42Z

Closes https://github.com/nodejs/node/issues/56236.

Improve thread safety of TaskQueue by making locking of TaskQueue explicit and thus hopefully preventing a crash due to `uv_async_send` being called with `nullptr`.

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->



Diff:
diff --git a/src/node_platform.cc b/src/node_platform.cc
index 3d1cc522db380b..3aab5196a2bccc 100644
--- a/src/node_platform.cc
+++ b/src/node_platform.cc
@@ -40,9 +40,10 @@ static void PlatformWorkerThread(void* data) {
     worker_data->platform_workers_ready->Signal(lock);
   }
 
-  while (std::unique_ptr<Task> task = pending_worker_tasks->BlockingPop()) {
+  while (std::unique_ptr<Task> task =
+             pending_worker_tasks->Lock().BlockingPop()) {
     task->Run();
-    pending_worker_tasks->NotifyOfCompletion();
+    pending_worker_tasks->Lock().NotifyOfCompletion();
   }
 }
 
@@ -73,13 +74,15 @@ class WorkerThreadsTaskRunner::DelayedTaskScheduler {
   }
 
   void PostDelayedTask(std::unique_ptr<Task> task, double delay_in_seconds) {
-    tasks_.Push(std::make_unique<ScheduleTask>(this, std::move(task),
-                                               delay_in_seconds));
+    auto locked = tasks_.Lock();
+    locked.Push(std::make_unique<ScheduleTask>(
+        this, std::move(task), delay_in_seconds));
     uv_async_send(&flush_tasks_);
   }
 
   void Stop() {
-    tasks_.Push(std::make_unique<StopTask>(this));
+    auto locked = tasks_.Lock();
+    locked.Push(std::make_unique<StopTask>(this));
     uv_async_send(&flush_tasks_);
   }
 
@@ -100,8 +103,14 @@ class WorkerThreadsTaskRunner::DelayedTaskScheduler {
   static void FlushTasks(uv_async_t* flush_tasks) {
     DelayedTaskScheduler* scheduler =
         ContainerOf(&DelayedTaskScheduler::loop_, flush_tasks->loop);
-    while (std::unique_ptr<Task> task = scheduler->tasks_.Pop())
+
+    std::queue<std::unique_ptr<Task>> tasks_to_run =
+        scheduler->tasks_.Lock().PopAll();
+    while (!tasks_to_run.empty()) {
+      std::unique_ptr<Task> task = std::move(tasks_to_run.front());
+      tasks_to_run.pop();
       task->Run();
+    }
   }
 
   class StopTask : public Task {
@@ -149,7 +158,8 @@ class WorkerThreadsTaskRunner::DelayedTaskScheduler {
   static void RunTask(uv_timer_t* timer) {
     DelayedTaskScheduler* scheduler =
         ContainerOf(&DelayedTaskScheduler::loop_, timer->loop);
-    scheduler->pending_worker_tasks_->Push(scheduler->TakeTimerTask(timer));
+    scheduler->pending_worker_tasks_->Lock().Push(
+        scheduler->TakeTimerTask(timer));
   }
 
   std::unique_ptr<Task> TakeTimerTask(uv_timer_t* timer) {
@@ -203,7 +213,7 @@ WorkerThreadsTaskRunner::WorkerThreadsTaskRunner(int thread_pool_size) {
 }
 
 void WorkerThreadsTaskRunner::PostTask(std::unique_ptr<Task> task) {
-  pending_worker_tasks_.Push(std::move(task));
+  pending_worker_tasks_.Lock().Push(std::move(task));
 }
 
 void WorkerThreadsTaskRunner::PostDelayedTask(std::unique_ptr<Task> task,
@@ -212,11 +222,11 @@ void WorkerThreadsTaskRunner::PostDelayedTask(std::unique_ptr<Task> task,
 }
 
 void WorkerThreadsTaskRunner::BlockingDrain() {
-  pending_worker_tasks_.BlockingDrain();
+  pending_worker_tasks_.Lock().BlockingDrain();
 }
 
 void WorkerThreadsTaskRunner::Shutdown() {
-  pending_worker_tasks_.Stop();
+  pending_worker_tasks_.Lock().Stop();
   delayed_task_scheduler_->Stop();
   for (size_t i = 0; i < threads_.size(); i++) {
     CHECK_EQ(0, uv_thread_join(threads_[i].get()));
@@ -253,12 +263,13 @@ void PerIsolatePlatformData::PostIdleTaskImpl(
 
 void PerIsolatePlatformData::PostTaskImpl(std::unique_ptr<Task> task,
                                           const v8::SourceLocation& location) {
-  if (flush_tasks_ == nullptr) {
-    // V8 may post tasks during Isolate disposal. In that case, the only
-    // sensible path forward is to discard the task.
-    return;
-  }
-  foreground_tasks_.Push(std::move(task));
+  // The task can be posted from any V8 background worker thread, even when
+  // the foreground task runner is being cleaned up by Shutdown(). In that
+  // case, make sure we wait until the shutdown is completed (which leads
+  // to flush_tasks_ == nullptr, and the task will be discarded).
+  auto locked = foreground_tasks_.Lock();
+  if (flush_tasks_ == nullptr) return;
+  locked.Push(std::move(task));
   uv_async_send(flush_tasks_);
 }
 
@@ -266,16 +277,13 @@ void PerIsolatePlatformData::PostDelayedTaskImpl(
     std::unique_ptr<Task> task,
     double delay_in_seconds,
     const v8::SourceLocation& location) {
-  if (flush_tasks_ == nullptr) {
-    // V8 may post tasks during Isolate disposal. In that case, the only
-    // sensible path forward is to discard the task.
-    return;
-  }
+  auto locked = foreground_delayed_tasks_.Lock();
+  if (flush_tasks_ == nullptr) return;
   std::unique_ptr<DelayedTask> delayed(new DelayedTask());
   delayed->task = std::move(task);
   delayed->platform_data = shared_from_this();
   delayed->timeout = delay_in_seconds;
-  foreground_delayed_tasks_.Push(std::move(delayed));
+  locked.Push(std::move(delayed));
   uv_async_send(flush_tasks_);
 }
 
@@ -301,32 +309,30 @@ void PerIsolatePlatformData::AddShutdownCallback(void (*callback)(void*),
 }
 
 void PerIsolatePlatformData::Shutdown() {
-  if (flush_tasks_ == nullptr)
-    return;
+  auto foreground_tasks_locked = foreground_tasks_.Lock();
+  auto foreground_delayed_tasks_locked = foreground_delayed_tasks_.Lock();
 
-  // While there should be no V8 tasks in the queues at this point, it is
-  // possible that Node.js-internal tasks from e.g. the inspector are still
-  // lying around. We clear these queues and ignore the return value,
-  // effectively deleting the tasks instead of running them.
-  foreground_delayed_tasks_.PopAll();
-  foreground_tasks_.PopAll();
+  foreground_delayed_tasks_locked.PopAll();
+  foreground_tasks_locked.PopAll();
   scheduled_delayed_tasks_.clear();
 
-  // Both destroying the scheduled_delayed_tasks_ lists and closing
-  // flush_tasks_ handle add tasks to the event loop. We keep a count of all
-  // non-closed handles, and when that reaches zero, we inform any shutdown
-  // callbacks that the platform is done as far as this Isolate is concerned.
-  self_reference_ = shared_from_this();
-  uv_close(reinterpret_cast<uv_handle_t*>(flush_tasks_),
-           [](uv_handle_t* handle) {
-    std::unique_ptr<uv_async_t> flush_tasks {
-        reinterpret_cast<uv_async_t*>(handle) };
-    PerIsolatePlatformData* platform_data =
-        static_cast<PerIsolatePlatformData*>(flush_tasks->data);
-    platform_data->DecreaseHandleCount();
-    platform_data->self_reference_.reset();
-  });
-  flush_tasks_ = nullptr;
+  if (flush_tasks_ != nullptr) {
+    // Both destroying the scheduled_delayed_tasks_ lists and closing
+    // flush_tasks_ handle add tasks to the event loop. We keep a count of all
+    // non-closed handles, and when that reaches zero, we inform any shutdown
+    // callbacks that the platform is done as far as this Isolate is concerned.
+    self_reference_ = shared_from_this();
+    uv_close(reinterpret_cast<uv_handle_t*>(flush_tasks_),
+             [](uv_handle_t* handle) {
+               std::unique_ptr<uv_async_t> flush_tasks{
+                   reinterpret_cast<uv_async_t*>(handle)};
+               PerIsolatePlatformData* platform_data =
+                   static_cast<PerIsolatePlatformData*>(flush_tasks->data);
+               platform_data->DecreaseHandleCount();
+               platform_data->self_reference_.reset();
+             });
+    flush_tasks_ = nullptr;
+  }
 }
 
 void PerIsolatePlatformData::DecreaseHandleCount() {
@@ -472,39 +478,48 @@ void NodePlatform::DrainTasks(Isolate* isolate) {
 bool PerIsolatePlatformData::FlushForegroundTasksInternal() {
   bool did_work = false;
 
-  while (std::unique_ptr<DelayedTask> delayed =
-      foreground_delayed_tasks_.Pop()) {
+  std::queue<std::unique_ptr<DelayedTask>> delayed_tasks_to_schedule =
+      foreground_delayed_tasks_.Lock().PopAll();
+  while (!delayed_tasks_to_schedule.empty()) {
+    std::unique_ptr<DelayedTask> delayed =
+        std::move(delayed_tasks_to_schedule.front());
+    delayed_tasks_to_schedule.pop();
+
     did_work = true;
     uint64_t delay_millis = llround(delayed->timeout * 1000);
 
     delayed->timer.data = static_cast<void*>(delayed.get());
     uv_timer_init(loop_, &delayed->timer);
-    // Timers may not guarantee queue ordering of events with the same delay if
-    // the delay is non-zero. This should not be a problem in practice.
+    // Timers may not guarantee queue ordering of events with the same delay
+    // if the delay is non-zero. This should not be a problem in practice.
     uv_timer_start(&delayed->timer, RunForegroundTask, delay_millis, 0);
     uv_unref(reinterpret_cast<uv_handle_t*>(&delayed->timer));
     uv_handle_count_++;
 
-    scheduled_delayed_tasks_.emplace_back(delayed.release(),
-                                          [](DelayedTask* delayed) {
-      uv_close(reinterpret_cast<uv_handle_t*>(&delayed->timer),
-               [](uv_handle_t* handle) {
-        std::unique_ptr<DelayedTask> task {
-            static_cast<DelayedTask*>(handle->data) };
-        task->platform_data->DecreaseHandleCount();
-      });
-    });
+    scheduled_delayed_tasks_.emplace_back(
+        delayed.release(), [](DelayedTask* delayed) {
+          uv_close(reinterpret_cast<uv_handle_t*>(&delayed->timer),
+                   [](uv_handle_t* handle) {
+                     std::unique_ptr<DelayedTask> task{
+                         static_cast<DelayedTask*>(handle->data)};
+                     task->platform_data->DecreaseHandleCount();
+                   });
+        });
+  }
+
+  std::queue<std::unique_ptr<Task>> tasks;
+  {
+    auto locked = foreground_tasks_.Lock();
+    tasks = locked.PopAll();
   }
-  // Move all foreground tasks into a separate queue and flush that queue.
-  // This way tasks that are posted while flushing the queue will be run on the
-  // next call of FlushForegroundTasksInternal.
-  std::queue<std::unique_ptr<Task>> tasks = foreground_tasks_.PopAll();
+
   while (!tasks.empty()) {
     std::unique_ptr<Task> task = std::move(tasks.front());
     tasks.pop();
     did_work = true;
     RunForegroundTask(std::move(task));
   }
+
   return did_work;
 }
 
@@ -594,66 +609,63 @@ TaskQueue<T>::TaskQueue()
       outstanding_tasks_(0), stopped_(false), task_queue_() { }
 
 template <class T>
-void TaskQueue<T>::Push(std::unique_ptr<T> task) {
-  Mutex::ScopedLock scoped_lock(lock_);
-  outstanding_tasks_++;
-  task_queue_.push(std::move(task));
-  tasks_available_.Signal(scoped_lock);
+TaskQueue<T>::Locked::Locked(TaskQueue* queue)
+    : queue_(queue), lock_(queue->lock_) {}
+
+template <class T>
+void TaskQueue<T>::Locked::Push(std::unique_ptr<T> task) {
+  queue_->outstanding_tasks_++;
+  queue_->task_queue_.push(std::move(task));
+  queue_->tasks_available_.Signal(lock_);
 }
 
 template <class T>
-std::unique_ptr<T> TaskQueue<T>::Pop() {
-  Mutex::ScopedLock scoped_lock(lock_);
-  if (task_queue_.empty()) {
+std::unique_ptr<T> TaskQueue<T>::Locked::Pop() {
+  if (queue_->task_queue_.empty()) {
     return std::unique_ptr<T>(nullptr);
   }
-  std::unique_ptr<T> result = std::move(task_queue_.front());
-  task_queue_.pop();
+  std::unique_ptr<T> result = std::move(queue_->task_queue_.front());
+  queue_->task_queue_.pop();
   return result;
 }
 
 template <class T>
-std::unique_ptr<T> TaskQueue<T>::BlockingPop() {
-  Mutex::ScopedLock scoped_lock(lock_);
-  while (task_queue_.empty() && !stopped_) {
-    tasks_available_.Wait(scoped_lock);
+std::unique_ptr<T> TaskQueue<T>::Locked::BlockingPop() {
+  while (queue_->task_queue_.empty() && !queue_->stopped_) {
+    queue_->tasks_available_.Wait(lock_);
   }
-  if (stopped_) {
+  if (queue_->stopped_) {
     return std::unique_ptr<T>(nullptr);
   }
-  std::unique_ptr<T> result = std::move(task_queue_.front());
-  task_queue_.pop();
+  std::unique_ptr<T> result = std::move(queue_->task_queue_.front());
+  queue_->task_queue_.pop();
   return result;
 }
 
 template <class T>
-void TaskQueue<T>::NotifyOfCompletion() {
-  Mutex::ScopedLock scoped_lock(lock_);
-  if (--outstanding_tasks_ == 0) {
-    tasks_drained_.Broadcast(scoped_lock);
+void TaskQueue<T>::Locked::NotifyOfCompletion() {
+  if (--queue_->outstanding_tasks_ == 0) {
+    queue_->tasks_drained_.Broadcast(lock_);
   }
 }
 
 template <class T>
-void TaskQueue<T>::BlockingDrain() {
-  Mutex::ScopedLock scoped_lock(lock_);
-  while (outstanding_tasks_ > 0) {
-    tasks_drained_.Wait(scoped_lock);
+void TaskQueue<T>::Locked::BlockingDrain() {
+  while (queue_->outstanding_tasks_ > 0) {
+    queue_->tasks_drained_.Wait(lock_);
   }
 }
 
 template <class T>
-void TaskQueue<T>::Stop() {
-  Mutex::ScopedLock scoped_lock(lock_);
-  stopped_ = true;
-  tasks_available_.Broadcast(scoped_lock);
+void TaskQueue<T>::Locked::Stop() {
+  queue_->stopped_ = true;
+  queue_->tasks_available_.Broadcast(lock_);
 }
 
 template <class T>
-std::queue<std::unique_ptr<T>> TaskQueue<T>::PopAll() {
-  Mutex::ScopedLock scoped_lock(lock_);
+std::queue<std::unique_ptr<T>> TaskQueue<T>::Locked::PopAll() {
   std::queue<std::unique_ptr<T>> result;
-  result.swap(task_queue_);
+  result.swap(queue_->task_queue_);
   return result;
 }
 
diff --git a/src/node_platform.h b/src/node_platform.h
index d6f445ce9cd93d..6462f06f6983b2 100644
--- a/src/node_platform.h
+++ b/src/node_platform.h
@@ -22,16 +22,28 @@ class PerIsolatePlatformData;
 template <class T>
 class TaskQueue {
  public:
+  class Locked {
+   public:
+    void Push(std::unique_ptr<T> task);
+    std::unique_ptr<T> Pop();
+    std::unique_ptr<T> BlockingPop();
+    void NotifyOfCompletion();
+    void BlockingDrain();
+    void Stop();
+    std::queue<std::unique_ptr<T>> PopAll();
+
+   private:
+    friend class TaskQueue;
+    explicit Locked(TaskQueue* queue);
+
+    TaskQueue* queue_;
+    Mutex::ScopedLock lock_;
+  };
+
   TaskQueue();
   ~TaskQueue() = default;
 
-  void Push(std::unique_ptr<T> task);
-  std::unique_ptr<T> Pop();
-  std::unique_ptr<T> BlockingPop();
-  std::queue<std::unique_ptr<T>> PopAll();
-  void NotifyOfCompletion();
-  void BlockingDrain();
-  void Stop();
+  Locked Lock() { return Locked(this); }
 
  private:
   Mutex lock_;
@@ -98,6 +110,8 @@ class PerIsolatePlatformData
   void RunForegroundTask(std::unique_ptr<v8::Task> task);
   static void RunForegroundTask(uv_timer_t* timer);
 
+  uv_async_t* flush_tasks_ = nullptr;
+
   struct ShutdownCallback {
     void (*cb)(void*);
     void* data;
@@ -110,7 +124,9 @@ class PerIsolatePlatformData
 
   v8::Isolate* const isolate_;
   uv_loop_t* const loop_;
-  uv_async_t* flush_tasks_ = nullptr;
+
+  // When acquiring locks for both task queues, lock foreground_tasks_
+  // first then foreground_delayed_tasks_ to avoid deadlocks.
   TaskQueue<v8::Task> foreground_tasks_;
   TaskQueue<DelayedTask> foreground_delayed_tasks_;
 


Reviews:
user: login: bnoordhuis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T16:27:58Z
user: login: bnoordhuis
body: LGTM with some suggestions.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-18T20:55:12Z
user: login: mcollina
body: RSLGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-19T13:49:36Z
user: login: jasnell
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-19T17:05:16Z
user: login: joyeecheung
body: LGTM (as in, my tiny head can't find potential issues out of the new locks), though I think it can use a bit more comments (I am fairly certain that the race can only happen at shutdown, the platform is initialized too early for any tasks to be posted during that process)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-21T17:34:29Z
user: login: joyeecheung
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-21T20:46:24Z

Review comments:
diff_hunk: @@ -22,16 +22,69 @@ class PerIsolatePlatformData;
 template <class T>
 class TaskQueue {
  public:
+  class Locked {
+   public:
+    explicit Locked(TaskQueue* queue) : queue_(queue), lock_(queue->lock_) {}
+
+    void Push(std::unique_ptr<T> task) {
+      queue_->outstanding_tasks_++;
+      queue_->task_queue_.push(std::move(task));
+      queue_->tasks_available_.Signal(lock_);
+    }
path: src/node_platform.h
user: login: bnoordhuis
body: The convention in node is to move method bodies into either the corresponding .cc or a -inl.h file (with the exception of really simple getters and setters, the kind you write on a single line.)
created_at: 2025-04-17T16:17:25Z
author_association: MEMBER
diff_hunk: @@ -22,16 +22,69 @@ class PerIsolatePlatformData;
 template <class T>
 class TaskQueue {
  public:
+  class Locked {
+   public:
+    explicit Locked(TaskQueue* queue) : queue_(queue), lock_(queue->lock_) {}
path: src/node_platform.h
user: login: bnoordhuis
body: Maybe make this private so Lock() is the only entry point?
created_at: 2025-04-17T16:19:21Z
author_association: MEMBER
diff_hunk: @@ -100,8 +103,8 @@ class WorkerThreadsTaskRunner::DelayedTaskScheduler {
   static void FlushTasks(uv_async_t* flush_tasks) {
     DelayedTaskScheduler* scheduler =
         ContainerOf(&DelayedTaskScheduler::loop_, flush_tasks->loop);
-    while (std::unique_ptr<Task> task = scheduler->tasks_.Pop())
-      task->Run();
+    auto locked = scheduler->tasks_.Lock();
+    while (std::unique_ptr<Task> task = locked.Pop()) task->Run();
path: src/node_platform.cc
user: login: bnoordhuis
body: Change in behavior: Run() methods now run with the mutex held and that's probably not what you want.
created_at: 2025-04-17T16:23:13Z
author_association: MEMBER
diff_hunk: @@ -253,20 +257,22 @@ void PerIsolatePlatformData::PostIdleTaskImpl(
 
 void PerIsolatePlatformData::PostTaskImpl(std::unique_ptr<Task> task,
                                           const v8::SourceLocation& location) {
-  if (flush_tasks_ == nullptr) {
+  // Check if flush_tasks is valid before using it
+  if (!flush_tasks_valid_.load()) {
     // V8 may post tasks during Isolate disposal. In that case, the only
     // sensible path forward is to discard the task.
     return;
   }
-  foreground_tasks_.Push(std::move(task));
+  foreground_tasks_.Lock().Push(std::move(task));
   uv_async_send(flush_tasks_);
path: src/node_platform.cc
user: login: bnoordhuis
body: I think what you want to do here is take out the lock, then check and push; then you don't need that atomic bool either. I can't post it as a suggestion but it'd look like this:
```c++
auto locked = foreground_tasks_.Lock();
if (flush_tasks == nullptr) return;
locked.Push(std::move(task));
uv_async_send(flush_tasks_);
```
The other thread must take out the lock when freeing `flush_tasks_`.
created_at: 2025-04-17T16:26:40Z
author_association: MEMBER
diff_hunk: @@ -100,8 +103,19 @@ class WorkerThreadsTaskRunner::DelayedTaskScheduler {
   static void FlushTasks(uv_async_t* flush_tasks) {
     DelayedTaskScheduler* scheduler =
         ContainerOf(&DelayedTaskScheduler::loop_, flush_tasks->loop);
-    while (std::unique_ptr<Task> task = scheduler->tasks_.Pop())
+
+    std::vector<std::unique_ptr<Task>> tasks_to_run;
+    {
+      auto locked = scheduler->tasks_.Lock();
+      std::unique_ptr<Task> task;
+      while ((task = locked.Pop())) {
+        tasks_to_run.push_back(std::move(task));
+      }
+    }
path: src/node_platform.cc
user: login: bnoordhuis
body: ```suggestion
    auto tasks_to_run = scheduler->tasks_.Lock().PopAll();
```
That's equivalent, right?
created_at: 2025-04-18T20:46:54Z
author_association: MEMBER
diff_hunk: @@ -472,39 +479,51 @@ void NodePlatform::DrainTasks(Isolate* isolate) {
 bool PerIsolatePlatformData::FlushForegroundTasksInternal() {
   bool did_work = false;
 
-  while (std::unique_ptr<DelayedTask> delayed =
-      foreground_delayed_tasks_.Pop()) {
-    did_work = true;
+  std::vector<std::unique_ptr<DelayedTask>> delayed_tasks_to_schedule;
+  {
+    auto locked_tasks = foreground_delayed_tasks_.Lock();
+    std::unique_ptr<DelayedTask> delayed;
+    while ((delayed = locked_tasks.Pop())) {
+      did_work = true;
+      delayed_tasks_to_schedule.push_back(std::move(delayed));
+    }
+  }
+
+  for (auto& delayed : delayed_tasks_to_schedule) {
path: src/node_platform.cc
user: login: bnoordhuis
body: Same as above:
```suggestion
  auto delayed_tasks_to_schedule = foreground_delayed_tasks_.Lock().PopAll();

  for (auto& delayed : delayed_tasks_to_schedule) {
    did_work = true;
```
created_at: 2025-04-18T20:51:04Z
author_association: MEMBER
diff_hunk: @@ -301,32 +310,30 @@ void PerIsolatePlatformData::AddShutdownCallback(void (*callback)(void*),
 }
 
 void PerIsolatePlatformData::Shutdown() {
-  if (flush_tasks_ == nullptr)
-    return;
+  auto foreground_tasks_locked = foreground_tasks_.Lock();
+  auto foreground_delayed_tasks_locked = foreground_delayed_tasks_.Lock();
path: src/node_platform.cc
user: login: bnoordhuis
body: Can you add a comment to their definitions in node_platform.h explaining the locks should always be acquired in this order?
created_at: 2025-04-18T20:54:57Z
author_association: MEMBER
diff_hunk: @@ -253,29 +263,23 @@ void PerIsolatePlatformData::PostIdleTaskImpl(
 
 void PerIsolatePlatformData::PostTaskImpl(std::unique_ptr<Task> task,
                                           const v8::SourceLocation& location) {
-  if (flush_tasks_ == nullptr) {
-    // V8 may post tasks during Isolate disposal. In that case, the only
-    // sensible path forward is to discard the task.
-    return;
-  }
-  foreground_tasks_.Push(std::move(task));
+  auto locked = foreground_tasks_.Lock();
path: src/node_platform.cc
user: login: joyeecheung
body: ```suggestion
  // The task can be posted from any V8 background worker thread, even when
  // the foreground task runner is being cleaned up by Shutdown(). In that
  // case, make sure we wait until the shutdown is completed (which leads
  // to flush_tasks_ == nullptr, and the task will be discarded).
  auto locked = foreground_tasks_.Lock();
```
created_at: 2025-04-21T17:32:29Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-04-17T13:40:02Z
body: ## [Codecov](https://app.codecov.io/gh/nodejs/node/pull/57910?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs) Report
Attention: Patch coverage is `97.56098%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 90.26%. Comparing base [(`5077ea4`)](https://app.codecov.io/gh/nodejs/node/commit/5077ea41491d555b44675f86502d4a91850dcbfd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs) to head [(`d71a542`)](https://app.codecov.io/gh/nodejs/node/commit/d71a54229590c9d51bbce349998834749f8061b8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs).
> Report is 425 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/nodejs/node/pull/57910?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs) | Patch % | Lines |
|---|---|---|
| [src/node\_platform.cc](https://app.codecov.io/gh/nodejs/node/pull/57910?src=pr&el=tree&filepath=src%2Fnode_platform.cc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs#diff-c3JjL25vZGVfcGxhdGZvcm0uY2M=) | 97.53% | [0 Missing and 2 partials :warning: ](https://app.codecov.io/gh/nodejs/node/pull/57910?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff            @@
##             main   #57910    +/-   ##
========================================
  Coverage   90.25%   90.26%            
========================================
  Files         630      630            
  Lines      185691   186170   +479     
  Branches    36407    36480    +73     
========================================
+ Hits       167589   168039   +450     
+ Misses      10994    10975    -19     
- Partials     7108     7156    +48     
```

| [Files with missing lines](https://app.codecov.io/gh/nodejs/node/pull/57910?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs) | Coverage Δ | |
|---|---|---|
| [src/node\_platform.h](https://app.codecov.io/gh/nodejs/node/pull/57910?src=pr&el=tree&filepath=src%2Fnode_platform.h&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs#diff-c3JjL25vZGVfcGxhdGZvcm0uaA==) | `80.00% <100.00%> (+5.00%)` | :arrow_up: |
| [src/node\_platform.cc](https://app.codecov.io/gh/nodejs/node/pull/57910?src=pr&el=tree&filepath=src%2Fnode_platform.cc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs#diff-c3JjL25vZGVfcGxhdGZvcm0uY2M=) | `88.70% <97.53%> (+0.83%)` | :arrow_up: |

... and [65 files with indirect coverage changes](https://app.codecov.io/gh/nodejs/node/pull/57910/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs)

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: mcollina
created_at: 2025-04-18T07:39:42Z
body: @codebytere I wonder if this could also help with https://github.com/nodejs/node/issues/54918.
author_association: MEMBER
user: login: nodejs-github-bot
created_at: 2025-04-19T13:54:06Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66364/
author_association: COLLABORATOR
user: login: lpinca
created_at: 2025-04-19T14:42:35Z
body: > @codebytere I wonder if this could also help with https://github.com/nodejs/node/issues/54918

I applied this patch and unfortunately it does not.
author_association: MEMBER
user: login: mcollina
created_at: 2025-04-19T15:00:27Z
body: Thanks for checking!
author_association: MEMBER
user: login: nodejs-github-bot
created_at: 2025-04-21T07:44:08Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66399/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-04-21T20:50:34Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66408/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-04-22T08:42:44Z
body: Landed in d12ee6e9ca5fe776746486130e156478254059ce
author_association: COLLABORATOR
user: login: marco-ippolito
created_at: 2025-06-05T09:17:16Z
body: This commit requires a manual backport to land on v20.x
author_association: MEMBER