From 9ea644c03aa061eaa87d9043da1f77130d206001 Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Thu, 27 Aug 2026 11:32:58 +0530 Subject: [PATCH 1/3] worker: emit worker exit notifications on BroadcastChannel Expose worker termination notifications through BroadcastChannel so consumers can observe when a worker exits andinspect its thread ID and exit code. Fixes: https://github.com/nodejs/node/issues/59053 Signed-off-by: SudhansuBandha --- doc/api/worker_threads.md | 19 ++++ lib/internal/worker/io.js | 6 ++ src/node_messaging.cc | 89 ++++++++++++++++++- src/node_messaging.h | 16 ++++ test/parallel/test-worker-broadcastchannel.js | 27 ++++++ 5 files changed, 156 insertions(+), 1 deletion(-) diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index e7b6b19b355..9e96840b13b 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -987,6 +987,25 @@ added: v15.4.0 * Type: {Function} Invoked with a received message cannot be deserialized. +### `broadcastChannel.onworkerexited` + +* Type: {Function} Invoked when worker associated with the + `BroadcastChannel` terminates. + +The callback receives an object with the following properties: + +* `threadId` {number} The ID of the worker thread that terminated. +* `exitCode` {number} The exit code with which the worker terminated. + +The `exitCode` is the value passed to `process.exit()` when the worker +explicitly exits. If the worker terminates without explicitly specifying +an exit code, the corresponding exit code is reported. + +The `workerexited` event is emitted only when the worker's execution +environment is stopping. Closing a `BroadcastChannel` or its underlying +`MessagePort` does not by itself indicate that a worker has exited and +does not emit this event. + ### `broadcastChannel.postMessage(message)` + * Type: {Function} Invoked when worker associated with the `BroadcastChannel` terminates. diff --git a/src/node_messaging.cc b/src/node_messaging.cc index dd6ed8b07c1..8189b6dc16a 100644 --- a/src/node_messaging.cc +++ b/src/node_messaging.cc @@ -651,10 +651,9 @@ void MessagePortData::AddToIncomingQueue(std::shared_ptr message) { void MessagePortData::AddWorkerExitNotification(uint64_t thread_id, ExitCode exit_code) { Mutex::ScopedLock lock(mutex_); - worker_exit_notifications_.emplace_back(WorkerExitNotification{ - thread_id, - exit_code, - }); + auto message = std::make_shared( + WorkerExitNotification{thread_id, exit_code}); + AddToIncomingQueue(std::move(message)); if (owner_ != nullptr) { Debug(owner_, "Adding worker-exit notification"); @@ -662,17 +661,6 @@ void MessagePortData::AddWorkerExitNotification(uint64_t thread_id, } } -bool MessagePortData::GetWorkerExitNotification( - WorkerExitNotification* notification) { - Mutex::ScopedLock lock(mutex_); - - if (worker_exit_notifications_.empty()) return false; - - *notification = worker_exit_notifications_.front(); - worker_exit_notifications_.pop_front(); - - return true; -} void MessagePortData::Entangle(MessagePortData* a, MessagePortData* b) { auto group = std::make_shared(); @@ -802,7 +790,8 @@ MessagePort* MessagePort::New( MaybeLocal MessagePort::ReceiveMessage(Local context, MessageProcessingMode mode, - Local* port_list) { + Local* port_list, + std::optional* worker_exit) { std::shared_ptr received; { // Get the head of the message queue. @@ -827,6 +816,13 @@ MaybeLocal MessagePort::ReceiveMessage(Local context, data_->incoming_messages_.pop_front(); } + if ( received->IsWorkerExitMessage() ) { + if ( worker_exit != nullptr ) { + *worker_exit = received->worker_exit_notification(); + return env()->no_message_symbol(); + } + } + if (received->IsCloseMessage()) { Close(); return env()->no_message_symbol(); @@ -845,12 +841,9 @@ void MessagePort::OnMessage(MessageProcessingMode mode) { // context, it will call the constructor and trigger the async handle empty. // Because all data was sent from the previous context. if (IsDetached()) return; - HandleScope handle_scope(env()->isolate()); Local context = object(env()->isolate())->GetCreationContextChecked(); - Local emit_message = PersistentToLocal::Strong(emit_message_fn_); - size_t processing_limit; if (mode == MessageProcessingMode::kNormalOperation) { Mutex::ScopedLock lock(data_->mutex_); @@ -877,60 +870,28 @@ void MessagePort::OnMessage(MessageProcessingMode mode) { return; } - MessagePortData::WorkerExitNotification worker_exit; - - if (data_->GetWorkerExitNotification(&worker_exit)) { - Debug(this, - "Worker exited: thread_id=%" PRIu64 ", exit_code=%d", - worker_exit.thread_id, - static_cast(worker_exit.exit_code)); - - Local exit_info = Object::New(env()->isolate()); - - exit_info - ->Set(context, - FIXED_ONE_BYTE_STRING(env()->isolate(), "threadId"), - v8::Number::New(env()->isolate(), worker_exit.thread_id)) - .Check(); - - exit_info - ->Set(context, - FIXED_ONE_BYTE_STRING(env()->isolate(), "exitCode"), - v8::Integer::New(env()->isolate(), - static_cast(worker_exit.exit_code))) - .Check(); - - Local argv[3]; - argv[0] = exit_info; - argv[1] = Undefined(env()->isolate()); - argv[2] = FIXED_ONE_BYTE_STRING(env()->isolate(), "workerexited"); - - if (MakeCallback(emit_message, arraysize(argv), argv).IsEmpty()) { - if (data_) TriggerAsync(); - return; - } - continue; - } - - HandleScope handle_scope(env()->isolate()); Context::Scope context_scope(context); + Local emit_message = PersistentToLocal::Strong(emit_message_fn_); Local payload; Local port_list = Undefined(env()->isolate()); Local message_error; Local argv[3]; + std::optional worker_exit; { // Catch any exceptions from parsing the message itself (not from // emitting it) as 'messageeror' events. TryCatchScope try_catch(env()); - if (!ReceiveMessage(context, mode, &port_list).ToLocal(&payload)) { + if (!ReceiveMessage(context, mode, &port_list, + &worker_exit).ToLocal(&payload)) { if (try_catch.HasCaught() && !try_catch.HasTerminated()) message_error = try_catch.Exception(); goto reschedule; } } - if (payload == env()->no_message_symbol()) break; + if (payload == env()->no_message_symbol() + && !worker_exit.has_value()) break; if (!env()->can_call_into_js()) { Debug(this, "MessagePort drains queue because !can_call_into_js()"); @@ -938,9 +899,38 @@ void MessagePort::OnMessage(MessageProcessingMode mode) { continue; } - argv[0] = payload; - argv[1] = port_list; - argv[2] = env()->message_string(); + if ( worker_exit.has_value() ) { + const WorkerExitNotification& notification = *worker_exit; + + Debug(this, + "Worker exited: thread_id=%d, exit_code=%d", + static_cast(notification.thread_id), + static_cast(notification.exit_code)); + Local exit_info = Object::New(env()->isolate()); + + exit_info + ->Set(context, + FIXED_ONE_BYTE_STRING(env()->isolate(), "threadId"), + v8::Number::New( + env()->isolate(), + static_cast(notification.thread_id))) + .Check(); + + exit_info + ->Set(context, + FIXED_ONE_BYTE_STRING(env()->isolate(), "exitCode"), + v8::Integer::New( + env()->isolate(), + static_cast(notification.exit_code))) + .Check(); + argv[0] = exit_info; + argv[1] = Undefined(env()->isolate()); + argv[2] = FIXED_ONE_BYTE_STRING(env()->isolate(), "workerexited"); + } else { + argv[0] = payload; + argv[1] = port_list; + argv[2] = env()->message_string(); + } if (MakeCallback(emit_message, arraysize(argv), argv).IsEmpty()) { reschedule: @@ -968,8 +958,8 @@ void MessagePort::OnClose() { const ExitCode exit_code = environment->exit_code(ExitCode::kNoFailure); Debug(this, - "Worker exiting: thread_id=%" PRIu64 ", exit_code=%d", - thread_id, + "Worker exiting: thread_id=%d, exit_code=%d", + static_cast(thread_id), static_cast(exit_code)); if (data_->group_) { diff --git a/src/node_messaging.h b/src/node_messaging.h index 0555ba77f36..1b16010d1ad 100644 --- a/src/node_messaging.h +++ b/src/node_messaging.h @@ -17,6 +17,11 @@ namespace worker { class MessagePortData; class MessagePort; +struct WorkerExitNotification { + uint64_t thread_id; + ExitCode exit_code; +}; + typedef MaybeStackBuffer TransferList; // Used to represent the in-flight structure of an object that is being @@ -49,6 +54,10 @@ class Message : public MemoryRetainer { // V8 ValueSerializer API. If `payload` is empty, this message indicates // that the receiving message port should close itself. explicit Message(MallocedBuffer&& payload = MallocedBuffer()); + + explicit Message(WorkerExitNotification notification) + : worker_exit_notification_(std::move(notification)) {} + ~Message() = default; Message(Message&& other) = default; @@ -60,6 +69,14 @@ class Message : public MemoryRetainer { // This is the last message to be received by a MessagePort. bool IsCloseMessage() const; + bool IsWorkerExitMessage() const { + return worker_exit_notification_.has_value(); + } + + const WorkerExitNotification& worker_exit_notification() const { + return worker_exit_notification_.value(); + } + // Deserialize the contained JS value. May only be called once, and only // after Serialize() has been called (e.g. by another thread). v8::MaybeLocal Deserialize( @@ -118,6 +135,7 @@ class Message : public MemoryRetainer { std::vector> transferables_; std::vector wasm_modules_; std::optional shared_value_conveyor_; + std::optional worker_exit_notification_; friend class MessagePort; }; @@ -189,7 +207,6 @@ class MessagePortData : public TransferData { v8::Maybe Dispatch( std::shared_ptr message, std::string* error = nullptr); - // Internal worker-exit notification. void AddWorkerExitNotification(uint64_t thread_id, ExitCode exit_code); @@ -220,15 +237,8 @@ class MessagePortData : public TransferData { // once that is available with C++17, because std::shared_ptr comes with // overhead that is only necessary for BroadcastChannel. std::deque> incoming_messages_; - struct WorkerExitNotification { - uint64_t thread_id; - ExitCode exit_code; - }; - bool GetWorkerExitNotification(WorkerExitNotification* notification); - std::deque worker_exit_notifications_; - MessagePort* owner_ = nullptr; std::shared_ptr group_; friend class MessagePort; @@ -324,7 +334,8 @@ class MessagePort : public HandleWrap { v8::MaybeLocal ReceiveMessage( v8::Local context, MessageProcessingMode mode, - v8::Local* port_list = nullptr); + v8::Local* port_list = nullptr, + std::optional* worker_exit = nullptr); std::unique_ptr data_ = nullptr; bool receiving_messages_ = false; From b1c9fb9fc43bc4d0f3771865c0ed29beb0c75793 Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Thu, 3 Sep 2026 13:01:06 +0530 Subject: [PATCH 3/3] worker: resolved clang lint issues --- src/node_messaging.cc | 52 +++++++++++++++++++++---------------------- src/node_messaging.h | 12 +++++----- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/src/node_messaging.cc b/src/node_messaging.cc index 8189b6dc16a..3c13653d1e3 100644 --- a/src/node_messaging.cc +++ b/src/node_messaging.cc @@ -651,8 +651,8 @@ void MessagePortData::AddToIncomingQueue(std::shared_ptr message) { void MessagePortData::AddWorkerExitNotification(uint64_t thread_id, ExitCode exit_code) { Mutex::ScopedLock lock(mutex_); - auto message = std::make_shared( - WorkerExitNotification{thread_id, exit_code}); + auto message = + std::make_shared(WorkerExitNotification{thread_id, exit_code}); AddToIncomingQueue(std::move(message)); if (owner_ != nullptr) { @@ -661,7 +661,6 @@ void MessagePortData::AddWorkerExitNotification(uint64_t thread_id, } } - void MessagePortData::Entangle(MessagePortData* a, MessagePortData* b) { auto group = std::make_shared(); group->Entangle({a, b}); @@ -788,10 +787,11 @@ MessagePort* MessagePort::New( return port; } -MaybeLocal MessagePort::ReceiveMessage(Local context, - MessageProcessingMode mode, - Local* port_list, - std::optional* worker_exit) { +MaybeLocal MessagePort::ReceiveMessage( + Local context, + MessageProcessingMode mode, + Local* port_list, + std::optional* worker_exit) { std::shared_ptr received; { // Get the head of the message queue. @@ -816,10 +816,10 @@ MaybeLocal MessagePort::ReceiveMessage(Local context, data_->incoming_messages_.pop_front(); } - if ( received->IsWorkerExitMessage() ) { - if ( worker_exit != nullptr ) { + if (received->IsWorkerExitMessage()) { + if (worker_exit != nullptr) { *worker_exit = received->worker_exit_notification(); - return env()->no_message_symbol(); + return env()->no_message_symbol(); } } @@ -883,15 +883,15 @@ void MessagePort::OnMessage(MessageProcessingMode mode) { // Catch any exceptions from parsing the message itself (not from // emitting it) as 'messageeror' events. TryCatchScope try_catch(env()); - if (!ReceiveMessage(context, mode, &port_list, - &worker_exit).ToLocal(&payload)) { + if (!ReceiveMessage(context, mode, &port_list, &worker_exit) + .ToLocal(&payload)) { if (try_catch.HasCaught() && !try_catch.HasTerminated()) message_error = try_catch.Exception(); goto reschedule; } } - if (payload == env()->no_message_symbol() - && !worker_exit.has_value()) break; + if (payload == env()->no_message_symbol() && !worker_exit.has_value()) + break; if (!env()->can_call_into_js()) { Debug(this, "MessagePort drains queue because !can_call_into_js()"); @@ -899,7 +899,7 @@ void MessagePort::OnMessage(MessageProcessingMode mode) { continue; } - if ( worker_exit.has_value() ) { + if (worker_exit.has_value()) { const WorkerExitNotification& notification = *worker_exit; Debug(this, @@ -911,25 +911,23 @@ void MessagePort::OnMessage(MessageProcessingMode mode) { exit_info ->Set(context, FIXED_ONE_BYTE_STRING(env()->isolate(), "threadId"), - v8::Number::New( - env()->isolate(), - static_cast(notification.thread_id))) + v8::Number::New(env()->isolate(), + static_cast(notification.thread_id))) .Check(); exit_info ->Set(context, FIXED_ONE_BYTE_STRING(env()->isolate(), "exitCode"), - v8::Integer::New( - env()->isolate(), - static_cast(notification.exit_code))) + v8::Integer::New(env()->isolate(), + static_cast(notification.exit_code))) .Check(); - argv[0] = exit_info; - argv[1] = Undefined(env()->isolate()); - argv[2] = FIXED_ONE_BYTE_STRING(env()->isolate(), "workerexited"); + argv[0] = exit_info; + argv[1] = Undefined(env()->isolate()); + argv[2] = FIXED_ONE_BYTE_STRING(env()->isolate(), "workerexited"); } else { - argv[0] = payload; - argv[1] = port_list; - argv[2] = env()->message_string(); + argv[0] = payload; + argv[1] = port_list; + argv[2] = env()->message_string(); } if (MakeCallback(emit_message, arraysize(argv), argv).IsEmpty()) { diff --git a/src/node_messaging.h b/src/node_messaging.h index 1b16010d1ad..073bb2574fa 100644 --- a/src/node_messaging.h +++ b/src/node_messaging.h @@ -18,8 +18,8 @@ class MessagePortData; class MessagePort; struct WorkerExitNotification { - uint64_t thread_id; - ExitCode exit_code; + uint64_t thread_id; + ExitCode exit_code; }; typedef MaybeStackBuffer TransferList; @@ -56,7 +56,7 @@ class Message : public MemoryRetainer { explicit Message(MallocedBuffer&& payload = MallocedBuffer()); explicit Message(WorkerExitNotification notification) - : worker_exit_notification_(std::move(notification)) {} + : worker_exit_notification_(std::move(notification)) {} ~Message() = default; @@ -168,8 +168,8 @@ class SiblingGroup final : public std::enable_shared_from_this { void Disentangle(MessagePortData* data); void NotifyWorkerExit(MessagePortData* exiting_port, - uint64_t thread_id, - ExitCode exit_code); + uint64_t thread_id, + ExitCode exit_code); const std::string& name() const { return name_; } @@ -335,7 +335,7 @@ class MessagePort : public HandleWrap { v8::Local context, MessageProcessingMode mode, v8::Local* port_list = nullptr, - std::optional* worker_exit = nullptr); + std::optional* worker_exit = nullptr); std::unique_ptr data_ = nullptr; bool receiving_messages_ = false;