Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions doc/api/worker_threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,25 @@ added: v15.4.0
* Type: {Function} Invoked with a received message cannot be
deserialized.

### `broadcastChannel.onworkerexited`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a YAML tag for version history tracking


* 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)`

<!-- YAML
Expand Down
6 changes: 6 additions & 0 deletions lib/internal/worker/io.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const kIncrementsPortRef = Symbol('kIncrementsPortRef');
const kName = Symbol('kName');
const kOnMessage = Symbol('kOnMessage');
const kOnMessageError = Symbol('kOnMessageError');
const kOnWorkerExited = Symbol('kOnWorkerExited');
const kPort = Symbol('kPort');
const kWaitingStreams = Symbol('kWaitingStreams');
const kWritableCallback = Symbol('kWritableCallback');
Expand Down Expand Up @@ -367,8 +368,10 @@ class BroadcastChannel extends EventTarget {
this[kOnMessage] = FunctionPrototypeBind(onMessageEvent, this, 'message');
this[kOnMessageError] =
FunctionPrototypeBind(onMessageEvent, this, 'messageerror');
this[kOnWorkerExited] = FunctionPrototypeBind(onMessageEvent, this, 'workerexited');
this[kHandle].on('message', this[kOnMessage]);
this[kHandle].on('messageerror', this[kOnMessageError]);
this[kHandle].on('workerexited', this[kOnWorkerExited]);
}

[inspect.custom](depth, options) {
Expand Down Expand Up @@ -407,8 +410,10 @@ class BroadcastChannel extends EventTarget {
return;
this[kHandle].off('message', this[kOnMessage]);
this[kHandle].off('messageerror', this[kOnMessageError]);
this[kHandle].off('workerexited', this[kOnWorkerExited]);
this[kOnMessage] = undefined;
this[kOnMessageError] = undefined;
this[kOnWorkerExited] = undefined;
this[kHandle].close();
this[kHandle] = undefined;
}
Expand Down Expand Up @@ -468,6 +473,7 @@ ObjectDefineProperties(BroadcastChannel.prototype, {

defineEventHandler(BroadcastChannel.prototype, 'message');
defineEventHandler(BroadcastChannel.prototype, 'messageerror');
defineEventHandler(BroadcastChannel.prototype, 'workerexited');

function markAsUncloneable(obj) {
if ((typeof obj !== 'object' && typeof obj !== 'function') || obj === null) {
Expand Down
89 changes: 88 additions & 1 deletion src/node_messaging.cc
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,32 @@ void MessagePortData::AddToIncomingQueue(std::shared_ptr<Message> 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,
});

if (owner_ != nullptr) {
Debug(owner_, "Adding worker-exit notification");
owner_->TriggerAsync();
}
}

bool MessagePortData::GetWorkerExitNotification(
WorkerExitNotification* notification) {
Mutex::ScopedLock lock(mutex_);

if (worker_exit_notifications_.empty()) return false;

*notification = worker_exit_notifications_.front();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer std::optional<> instead of assigning to out parameters (esp. if they have non-trivial types)

worker_exit_notifications_.pop_front();

return true;
}

void MessagePortData::Entangle(MessagePortData* a, MessagePortData* b) {
auto group = std::make_shared<SiblingGroup>();
group->Entangle({a, b});
Expand Down Expand Up @@ -823,6 +849,7 @@ void MessagePort::OnMessage(MessageProcessingMode mode) {
HandleScope handle_scope(env()->isolate());
Local<Context> context =
object(env()->isolate())->GetCreationContextChecked();
Local<Function> emit_message = PersistentToLocal::Strong(emit_message_fn_);

size_t processing_limit;
if (mode == MessageProcessingMode::kNormalOperation) {
Expand Down Expand Up @@ -850,9 +877,43 @@ 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<int>(worker_exit.exit_code));

Local<Object> 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<int>(worker_exit.exit_code)))
.Check();

Local<Value> 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;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicating a significant amount of logic – is there a reason that this needs to be a new message type, and cannot be something that would be conveyed through normal messages in the queue?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to implement a separate pipeline since this notification is related to worker lifecycle instead of inter thread messages of Broadcast Channel. If this is not something to be done then I will update the PR to use existing pipeline for emitting these lifecycle events.


HandleScope handle_scope(env()->isolate());
Context::Scope context_scope(context);
Local<Function> emit_message = PersistentToLocal::Strong(emit_message_fn_);

Local<Value> payload;
Local<Value> port_list = Undefined(env()->isolate());
Expand Down Expand Up @@ -901,6 +962,20 @@ void MessagePort::OnMessage(MessageProcessingMode mode) {
void MessagePort::OnClose() {
Debug(this, "MessagePort::OnClose()");
if (data_) {
Environment* environment = env();
if (environment->is_stopping()) {
const uint64_t thread_id = environment->thread_id();
const ExitCode exit_code = environment->exit_code(ExitCode::kNoFailure);

Debug(this,
"Worker exiting: thread_id=%" PRIu64 ", exit_code=%d",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Worker exiting: thread_id=%" PRIu64 ", exit_code=%d",
"Worker exiting: thread_id=%d, exit_code=%d",

Debug() doesn't care about the actual code anyway

thread_id,
static_cast<int>(exit_code));

if (data_->group_) {
data_->group_->NotifyWorkerExit(data_.get(), thread_id, exit_code);
}
}
// Detach() returns move(data_).
Detach()->Disentangle();
}
Expand Down Expand Up @@ -1587,6 +1662,18 @@ void SiblingGroup::Disentangle(MessagePortData* data) {
(*(ports_.begin()))->AddToIncomingQueue(std::make_shared<Message>());
}

void SiblingGroup::NotifyWorkerExit(MessagePortData* exiting_port,
uint64_t thread_id,
ExitCode exit_code) {
RwLock::ScopedReadLock lock(group_mutex_);

for (MessagePortData* port : ports_) {
if (port == exiting_port) continue;

port->AddWorkerExitNotification(thread_id, exit_code);
}
}

SiblingGroup::Map SiblingGroup::groups_;
Mutex SiblingGroup::groups_mutex_;

Expand Down
16 changes: 16 additions & 0 deletions src/node_messaging.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ class SiblingGroup final : public std::enable_shared_from_this<SiblingGroup> {
void Entangle(std::initializer_list<MessagePortData*> data);
void Disentangle(MessagePortData* data);

void NotifyWorkerExit(MessagePortData* exiting_port,
uint64_t thread_id,
ExitCode exit_code);

const std::string& name() const { return name_; }

size_t size() const { return ports_.size(); }
Expand Down Expand Up @@ -185,6 +189,9 @@ class MessagePortData : public TransferData {
v8::Maybe<bool> Dispatch(
std::shared_ptr<Message> message,
std::string* error = nullptr);

// Internal worker-exit notification.
void AddWorkerExitNotification(uint64_t thread_id, ExitCode exit_code);

// Turns `a` and `b` into siblings, i.e. connects the sending side of one
// to the receiving side of the other. This is not thread-safe.
Expand Down Expand Up @@ -213,6 +220,15 @@ 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<std::shared_ptr<Message>> incoming_messages_;
struct WorkerExitNotification {
uint64_t thread_id;
ExitCode exit_code;
};

bool GetWorkerExitNotification(WorkerExitNotification* notification);

std::deque<WorkerExitNotification> worker_exit_notifications_;

MessagePort* owner_ = nullptr;
std::shared_ptr<SiblingGroup> group_;
friend class MessagePort;
Expand Down
27 changes: 27 additions & 0 deletions test/parallel/test-worker-broadcastchannel.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,30 @@
"BroadcastChannel { name: 'channel5', active: false }"
);
}

{
const bc = new BroadcastChannel('channel6');

const worker = new Worker(`
const { BroadcastChannel } = require('worker_threads');

const bc = new BroadcastChannel('channel6');

// Keep the BroadcastChannel alive long enough for the exit
// notification to be observed by the parent.
setImmediate(() => {
process.exit(42);
});
`, { eval: true });

bc.onworkerexited = common.mustCall((event) => {
assert.strictEqual(event.data.threadId, worker.threadId);

Check failure on line 203 in test/parallel/test-worker-broadcastchannel.js

View workflow job for this annotation

GitHub Actions / aarch64-linux: with shared boringssl-0.20260803.0 / build

--- stderr --- node:internal/event_target:1131 process.nextTick(() => { throw err; }); ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 2 !== -1 at BroadcastChannel.<anonymous> (/home/runner/work/_temp/node-v27.0.0-nightly2026-08-272a91ec9458-slim/test/parallel/test-worker-broadcastchannel.js:203:12) at BroadcastChannel.<anonymous> (/home/runner/work/_temp/node-v27.0.0-nightly2026-08-272a91ec9458-slim/test/common/index.js:511:15) at BroadcastChannel.eventHandler (node:internal/event_target:1141:12) at [nodejs.internal.kHybridDispatch] (node:internal/event_target:851:20) at BroadcastChannel.dispatchEvent (node:internal/event_target:792:26) at BroadcastChannel.onMessageEvent (node:internal/worker/io:350:8) at [nodejs.internal.kHybridDispatch] (node:internal/event_target:851:20) at MessagePort.<anonymous> (node:internal/per_context/messageport:23:28) { generatedMessage: true, code: 'ERR_ASSERTION', actual: 2, expected: -1, operator: 'strictEqual', diff: 'simple' } Node.js v27.0.0-pre Command: out/Release/node /home/runner/work/_temp/node-v27.0.0-nightly2026-08-272a91ec9458-slim/test/parallel/test-worker-broadcastchannel.js
assert.strictEqual(event.data.exitCode, 42);

bc.close();
});

worker.on('exit', common.mustCall((exitCode) => {
assert.strictEqual(exitCode, 42);
}));
}
Loading