Files
node/src/node_platform.h
T
Shelley Vohr 32d23ec931 src: fix task queue deadlock when built as C++23
Since #65353, FlushForegroundTasksInternal() and the
DelayedTaskScheduler drain their queues with
`for (auto& task : queue.Lock().PopAll())`. In C++20 the Locked
temporary is destroyed at the end of the range initializer, before the
loop body runs. C++23 (P2718R0) extends the lifetime of every temporary
in a range-for initializer to the end of the loop, so the queue's mutex
stays held while the tasks run, and the first task that posts to the
same queue - any V8 foreground task that schedules another one -
deadlocks on the non-recursive mutex. Node.js itself builds with
-std=gnu++20, but an embedder that compiles it as C++23 hangs in the
first foreground task flush.

Store the drained tasks in a local before iterating so the lock is
released independent of the language version.

Refs: https://github.com/nodejs/node/pull/65353
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66066
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
2026-09-20 16:35:38 +00:00

293 lines
9.8 KiB
C++

#ifndef SRC_NODE_PLATFORM_H_
#define SRC_NODE_PLATFORM_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include <concepts>
#include <functional>
#include <queue>
#include <type_traits>
#include <unordered_map>
#include <vector>
#include "libplatform/libplatform.h"
#include "node.h"
#include "node_mutex.h"
#include "uv.h"
namespace node {
class NodePlatform;
class IsolateData;
class PerIsolatePlatformData;
template <typename T>
concept has_priority = requires(T t) { t.priority; };
template <class T>
class TaskQueue {
public:
class Locked {
public:
void Push(std::unique_ptr<T> task, bool outstanding = false);
std::unique_ptr<T> Pop();
std::unique_ptr<T> BlockingPop();
void NotifyOfOutstandingCompletion();
void BlockingDrain();
void Stop();
// All queued tasks, in the order Pop() would have returned them. Store the
// result before iterating it: used directly as a range-for initializer,
// `Lock().PopAll()` keeps the lock held for the whole loop from C++23 on.
std::vector<std::unique_ptr<T>> PopAll();
private:
friend class TaskQueue;
explicit Locked(TaskQueue* queue);
TaskQueue* queue_;
Mutex::ScopedLock lock_;
};
TaskQueue();
~TaskQueue() = default;
Locked Lock() { return Locked(this); }
private:
struct Item {
std::unique_ptr<T> task;
uint64_t sequence;
};
// Higher priority first if the entry type has one; posting order otherwise
// and among equal priorities (a sequence number breaks the tie).
struct ItemCompare {
bool operator()(const Item& a, const Item& b) const {
if constexpr (has_priority<T>) {
if (a.task->priority != b.task->priority) {
return a.task->priority < b.task->priority;
}
}
return a.sequence > b.sequence;
}
};
using PriorityQueue =
std::priority_queue<Item, std::vector<Item>, ItemCompare>;
std::unique_ptr<T> PopTask();
Mutex lock_;
ConditionVariable tasks_available_;
ConditionVariable outstanding_tasks_drained_;
int outstanding_tasks_;
bool stopped_;
uint64_t next_sequence_ = 0;
PriorityQueue task_queue_;
};
struct TaskQueueEntry {
std::unique_ptr<v8::Task> task;
v8::TaskPriority priority;
TaskQueueEntry(std::unique_ptr<v8::Task> t, v8::TaskPriority p)
: task(std::move(t)), priority(p) {}
inline bool is_outstanding() const {
return priority == v8::TaskPriority::kUserBlocking;
}
};
struct DelayedTask {
std::unique_ptr<v8::Task> task;
v8::TaskPriority priority;
uv_timer_t timer;
double timeout;
std::shared_ptr<PerIsolatePlatformData> platform_data;
};
enum class PlatformDebugLogLevel {
kNone = 0,
kMinimal = 1,
kVerbose = 2,
};
// This acts as the foreground task runner for a given Isolate.
class PerIsolatePlatformData
: public IsolatePlatformDelegate,
public v8::TaskRunner,
public std::enable_shared_from_this<PerIsolatePlatformData> {
public:
PerIsolatePlatformData(
v8::Isolate* isolate,
uv_loop_t* loop,
PlatformDebugLogLevel debug_log_level = PlatformDebugLogLevel::kNone);
~PerIsolatePlatformData() override;
std::shared_ptr<v8::TaskRunner> GetForegroundTaskRunner() override;
bool IdleTasksEnabled() override { return false; }
// Non-nestable tasks are treated like regular tasks.
bool NonNestableTasksEnabled() const override { return true; }
bool NonNestableDelayedTasksEnabled() const override { return true; }
void AddShutdownCallback(void (*callback)(void*), void* data);
void Shutdown();
// Returns true if work was dispatched or executed. New tasks that are
// posted during flushing of the queue are postponed until the next
// flushing.
bool FlushForegroundTasksInternal();
const uv_loop_t* event_loop() const { return loop_; }
private:
// v8::TaskRunner implementation.
void PostTaskImpl(std::unique_ptr<v8::Task> task,
const v8::SourceLocation& location) override;
void PostDelayedTaskImpl(std::unique_ptr<v8::Task> task,
double delay_in_seconds,
const v8::SourceLocation& location) override;
void PostIdleTaskImpl(std::unique_ptr<v8::IdleTask> task,
const v8::SourceLocation& location) override;
void PostNonNestableTaskImpl(std::unique_ptr<v8::Task> task,
const v8::SourceLocation& location) override;
void PostNonNestableDelayedTaskImpl(
std::unique_ptr<v8::Task> task,
double delay_in_seconds,
const v8::SourceLocation& location) override;
void DeleteFromScheduledTasks(DelayedTask* task);
void DecreaseHandleCount();
static void FlushTasks(uv_async_t* handle);
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;
};
typedef std::vector<ShutdownCallback> ShutdownCbList;
ShutdownCbList shutdown_callbacks_;
// shared_ptr to self to keep this object alive during shutdown.
std::shared_ptr<PerIsolatePlatformData> self_reference_;
uint32_t uv_handle_count_ = 1; // 1 = flush_tasks_
v8::Isolate* const isolate_;
uv_loop_t* const loop_;
// When acquiring locks for both task queues, lock foreground_tasks_
// first then foreground_delayed_tasks_ to avoid deadlocks.
TaskQueue<TaskQueueEntry> foreground_tasks_;
TaskQueue<DelayedTask> foreground_delayed_tasks_;
// Use a custom deleter because libuv needs to close the handle first.
typedef std::unique_ptr<DelayedTask, void (*)(DelayedTask*)>
DelayedTaskPointer;
std::vector<DelayedTaskPointer> scheduled_delayed_tasks_;
PlatformDebugLogLevel debug_log_level_ = PlatformDebugLogLevel::kNone;
};
// This acts as the single worker thread task runner for all Isolates.
class WorkerThreadsTaskRunner {
public:
explicit WorkerThreadsTaskRunner(int thread_pool_size,
PlatformDebugLogLevel debug_log_level);
void PostTask(v8::TaskPriority priority,
std::unique_ptr<v8::Task> task,
const v8::SourceLocation& location);
void PostDelayedTask(v8::TaskPriority priority,
std::unique_ptr<v8::Task> task,
const v8::SourceLocation& location,
double delay_in_seconds);
void BlockingDrain();
void Shutdown();
int NumberOfWorkerThreads() const;
private:
// A queue shared by all threads. The consumers are the worker threads which
// take tasks from it to run in PlatformWorkerThread(). The producers can be
// any thread. Both the foreground thread and the worker threads can push
// tasks into the queue via v8::Platform::PostTaskOnWorkerThread() which
// eventually calls PostTask() on this class. When any thread calls
// v8::Platform::PostDelayedTaskOnWorkerThread(), the DelayedTaskScheduler
// thread will schedule a timer that pushes the delayed tasks back into this
// queue when the timer expires.
TaskQueue<TaskQueueEntry> pending_worker_tasks_;
class DelayedTaskScheduler;
std::unique_ptr<DelayedTaskScheduler> delayed_task_scheduler_;
std::vector<std::unique_ptr<uv_thread_t>> threads_;
PlatformDebugLogLevel debug_log_level_ = PlatformDebugLogLevel::kNone;
};
class NodePlatform : public MultiIsolatePlatform {
public:
NodePlatform(int thread_pool_size,
v8::TracingController* tracing_controller,
v8::PageAllocator* page_allocator = nullptr);
~NodePlatform() override;
void DrainTasks(v8::Isolate* isolate) override;
void Shutdown();
// v8::Platform implementation.
int NumberOfWorkerThreads() override;
void PostTaskOnWorkerThreadImpl(v8::TaskPriority priority,
std::unique_ptr<v8::Task> task,
const v8::SourceLocation& location) override;
void PostDelayedTaskOnWorkerThreadImpl(
v8::TaskPriority priority,
std::unique_ptr<v8::Task> task,
double delay_in_seconds,
const v8::SourceLocation& location) override;
bool IdleTasksEnabled(v8::Isolate* isolate) override;
double MonotonicallyIncreasingTime() override;
double CurrentClockTimeMillis() override;
v8::TracingController* GetTracingController() override;
bool FlushForegroundTasks(v8::Isolate* isolate) override;
std::unique_ptr<v8::JobHandle> CreateJobImpl(
v8::TaskPriority priority,
std::unique_ptr<v8::JobTask> job_task,
const v8::SourceLocation& location) override;
void RegisterIsolate(v8::Isolate* isolate, uv_loop_t* loop) override;
void RegisterIsolate(v8::Isolate* isolate,
IsolatePlatformDelegate* delegate) override;
void UnregisterIsolate(v8::Isolate* isolate) override;
void AddIsolateFinishedCallback(v8::Isolate* isolate,
void (*callback)(void*),
void* data) override;
std::shared_ptr<v8::TaskRunner> GetForegroundTaskRunner(
v8::Isolate* isolate, v8::TaskPriority priority) override;
Platform::StackTracePrinter GetStackTracePrinter() override;
v8::PageAllocator* GetPageAllocator() override;
private:
IsolatePlatformDelegate* ForIsolate(v8::Isolate* isolate);
std::shared_ptr<PerIsolatePlatformData> ForNodeIsolate(v8::Isolate* isolate);
Mutex per_isolate_mutex_;
using DelegatePair = std::pair<IsolatePlatformDelegate*,
std::shared_ptr<PerIsolatePlatformData>>;
std::unordered_map<v8::Isolate*, DelegatePair> per_isolate_;
v8::TracingController* tracing_controller_;
v8::PageAllocator* page_allocator_;
std::shared_ptr<WorkerThreadsTaskRunner> worker_thread_task_runner_;
bool has_shut_down_ = false;
PlatformDebugLogLevel debug_log_level_ = PlatformDebugLogLevel::kNone;
};
} // namespace node
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_NODE_PLATFORM_H_