Skip to content

Commit e004d42

Browse files
addaleaxtargos
authored andcommitted
worker: use special message as MessagePort close command
When a `MessagePort` connected to another `MessagePort` closes, the latter `MessagePort` will be closed as well. Until now, this is done by testing whether the ports are still entangled after processing messages. This leaves open a race condition window in which messages sent just before the closure can be lost when timing is unfortunate. (A description of the timing is in the test file.) This can be addressed by using a special message instead, which is the last message received by a `MessagePort`. This way, all previously sent messages are processed first. Fixes: #22762 PR-URL: #27705 Reviewed-By: Rich Trott <[email protected]> Reviewed-By: Colin Ihrig <[email protected]>
1 parent b7ed4d7 commit e004d42

File tree

3 files changed

+71
-36
lines changed

3 files changed

+71
-36
lines changed

src/node_messaging.cc

+26-27
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ namespace worker {
4040
Message::Message(MallocedBuffer<char>&& buffer)
4141
: main_message_buf_(std::move(buffer)) {}
4242

43+
bool Message::IsCloseMessage() const {
44+
return main_message_buf_.data == nullptr;
45+
}
46+
4347
namespace {
4448

4549
// This is used to tell V8 how to read transferred host objects, like other
@@ -91,6 +95,8 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
9195

9296
MaybeLocal<Value> Message::Deserialize(Environment* env,
9397
Local<Context> context) {
98+
CHECK(!IsCloseMessage());
99+
94100
EscapableHandleScope handle_scope(env->isolate());
95101
Context::Scope context_scope(context);
96102

@@ -395,6 +401,7 @@ Maybe<bool> Message::Serialize(Environment* env,
395401

396402
// The serializer gave us a buffer allocated using `malloc()`.
397403
std::pair<uint8_t*, size_t> data = serializer.Release();
404+
CHECK_NOT_NULL(data.first);
398405
main_message_buf_ =
399406
MallocedBuffer<char>(reinterpret_cast<char*>(data.first), data.second);
400407
return Just(true);
@@ -430,11 +437,6 @@ void MessagePortData::AddToIncomingQueue(Message&& message) {
430437
}
431438
}
432439

433-
bool MessagePortData::IsSiblingClosed() const {
434-
Mutex::ScopedLock lock(*sibling_mutex_);
435-
return sibling_ == nullptr;
436-
}
437-
438440
void MessagePortData::Entangle(MessagePortData* a, MessagePortData* b) {
439441
CHECK_NULL(a->sibling_);
440442
CHECK_NULL(b->sibling_);
@@ -443,12 +445,6 @@ void MessagePortData::Entangle(MessagePortData* a, MessagePortData* b) {
443445
a->sibling_mutex_ = b->sibling_mutex_;
444446
}
445447

446-
void MessagePortData::PingOwnerAfterDisentanglement() {
447-
Mutex::ScopedLock lock(mutex_);
448-
if (owner_ != nullptr)
449-
owner_->TriggerAsync();
450-
}
451-
452448
void MessagePortData::Disentangle() {
453449
// Grab a copy of the sibling mutex, then replace it so that each sibling
454450
// has its own sibling_mutex_ now.
@@ -462,11 +458,12 @@ void MessagePortData::Disentangle() {
462458
sibling_ = nullptr;
463459
}
464460

465-
// We close MessagePorts after disentanglement, so we trigger the
466-
// corresponding uv_async_t to let them know that this happened.
467-
PingOwnerAfterDisentanglement();
461+
// We close MessagePorts after disentanglement, so we enqueue a corresponding
462+
// message and trigger the corresponding uv_async_t to let them know that
463+
// this happened.
464+
AddToIncomingQueue(Message());
468465
if (sibling != nullptr) {
469-
sibling->PingOwnerAfterDisentanglement();
466+
sibling->AddToIncomingQueue(Message());
470467
}
471468
}
472469

@@ -590,14 +587,25 @@ void MessagePort::OnMessage() {
590587
Debug(this, "MessagePort has message, receiving = %d",
591588
static_cast<int>(receiving_messages_));
592589

593-
if (!receiving_messages_)
594-
break;
595-
if (data_->incoming_messages_.empty())
590+
// We have nothing to do if:
591+
// - There are no pending messages
592+
// - We are not intending to receive messages, and the message we would
593+
// receive is not the final "close" message.
594+
if (data_->incoming_messages_.empty() ||
595+
(!receiving_messages_ &&
596+
!data_->incoming_messages_.front().IsCloseMessage())) {
596597
break;
598+
}
599+
597600
received = std::move(data_->incoming_messages_.front());
598601
data_->incoming_messages_.pop_front();
599602
}
600603

604+
if (received.IsCloseMessage()) {
605+
Close();
606+
return;
607+
}
608+
601609
if (!env()->can_call_into_js()) {
602610
Debug(this, "MessagePort drains queue because !can_call_into_js()");
603611
// In this case there is nothing to do but to drain the current queue.
@@ -628,15 +636,6 @@ void MessagePort::OnMessage() {
628636
}
629637
}
630638
}
631-
632-
if (data_ && data_->IsSiblingClosed()) {
633-
Close();
634-
}
635-
}
636-
637-
bool MessagePort::IsSiblingClosed() const {
638-
CHECK(data_);
639-
return data_->IsSiblingClosed();
640639
}
641640

642641
void MessagePort::OnClose() {

src/node_messaging.h

+7-9
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,20 @@ class MessagePort;
1717
// Represents a single communication message.
1818
class Message : public MemoryRetainer {
1919
public:
20+
// Create a Message with a specific underlying payload, in the format of the
21+
// V8 ValueSerializer API. If `payload` is empty, this message indicates
22+
// that the receiving message port should close itself.
2023
explicit Message(MallocedBuffer<char>&& payload = MallocedBuffer<char>());
2124

2225
Message(Message&& other) = default;
2326
Message& operator=(Message&& other) = default;
2427
Message& operator=(const Message&) = delete;
2528
Message(const Message&) = delete;
2629

30+
// Whether this is a message indicating that the port is to be closed.
31+
// This is the last message to be received by a MessagePort.
32+
bool IsCloseMessage() const;
33+
2734
// Deserialize the contained JS value. May only be called once, and only
2835
// after Serialize() has been called (e.g. by another thread).
2936
v8::MaybeLocal<v8::Value> Deserialize(Environment* env,
@@ -89,10 +96,6 @@ class MessagePortData : public MemoryRetainer {
8996
// This may be called from any thread.
9097
void AddToIncomingQueue(Message&& message);
9198

92-
// Returns true if and only this MessagePort is currently not entangled
93-
// with another message port.
94-
bool IsSiblingClosed() const;
95-
9699
// Turns `a` and `b` into siblings, i.e. connects the sending side of one
97100
// to the receiving side of the other. This is not thread-safe.
98101
static void Entangle(MessagePortData* a, MessagePortData* b);
@@ -109,10 +112,6 @@ class MessagePortData : public MemoryRetainer {
109112
SET_SELF_SIZE(MessagePortData)
110113

111114
private:
112-
// After disentangling this message port, the owner handle (if any)
113-
// is asynchronously triggered, so that it can close down naturally.
114-
void PingOwnerAfterDisentanglement();
115-
116115
// This mutex protects all fields below it, with the exception of
117116
// sibling_.
118117
mutable Mutex mutex_;
@@ -178,7 +177,6 @@ class MessagePort : public HandleWrap {
178177
// messages.
179178
std::unique_ptr<MessagePortData> Detach();
180179

181-
bool IsSiblingClosed() const;
182180
void Close(
183181
v8::Local<v8::Value> close_callback = v8::Local<v8::Value>()) override;
184182

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const { once } = require('events');
5+
const { Worker, MessageChannel } = require('worker_threads');
6+
7+
// This is a regression test for the race condition underlying
8+
// https://github.com/nodejs/node/issues/22762.
9+
// It ensures that all messages send before a MessagePort#close() call are
10+
// received. Previously, what could happen was a race condition like this:
11+
// - Thread 1 sends message A
12+
// - Thread 2 begins receiving/emitting message A
13+
// - Thread 1 sends message B
14+
// - Thread 1 closes its side of the channel
15+
// - Thread 2 finishes receiving/emitting message A
16+
// - Thread 2 sees that the port should be closed
17+
// - Thread 2 closes the port, discarding message B in the process.
18+
19+
async function test() {
20+
const worker = new Worker(`
21+
require('worker_threads').parentPort.on('message', ({ port }) => {
22+
port.postMessage('firstMessage');
23+
port.postMessage('lastMessage');
24+
port.close();
25+
});
26+
`, { eval: true });
27+
28+
for (let i = 0; i < 10000; i++) {
29+
const { port1, port2 } = new MessageChannel();
30+
worker.postMessage({ port: port2 }, [ port2 ]);
31+
await once(port1, 'message'); // 'complexObject'
32+
assert.deepStrictEqual(await once(port1, 'message'), ['lastMessage']);
33+
}
34+
35+
worker.terminate();
36+
}
37+
38+
test().then(common.mustCall());

0 commit comments

Comments
 (0)