Skip to content

worker_threads: inconsistent stop/start behavior with MessagePort.onmessage #26463

Closed
@chjj

Description

@chjj
  • Version: v11.10.1
  • Platform: Linux 4.20.11-arch1-1-ARCH deps: update openssl to 1.0.1j #1 SMP PREEMPT Wed Feb 20 21:11:12 UTC 2019 x86_64 GNU/Linux
  • Subsystem: worker_threads

These two samples of code differ in output between node.js and the browser:

Browser (Chromium 72.0.3626.119):

const {MessageChannel} = global;
const {port1, port2} = new MessageChannel();

port1.postMessage('hello');

setTimeout(() => {
  port2.onmessage = ({data}) => {
    console.log(data);

    port2.onmessage = null;
    port1.postMessage('world');

    setTimeout(() => {
      port2.onmessage = ({data}) => {
        console.log(data);
      };
    }, 100);
  };
}, 100);

Output:

hello

node.js with identical logic:

const {MessageChannel} = require('worker_threads');
const {port1, port2} = new MessageChannel();

port1.postMessage('hello');

setTimeout(() => {
  port2.onmessage = (data) => {
    console.log(data);

    port2.onmessage = null;
    port1.postMessage('world');

    setTimeout(() => {
      port2.onmessage = (data) => {
        console.log(data);
      };
    }, 100);
  };
}, 100);

Output:

hello
world

The reason for this is that node.js calls stopMessagePort() [1] [2] [3] when the onmessage listener is removed (likewise for .on('message')). This causes the port to start buffering messages again and is the reason why we can still get world once we rebind the event listener. The browser does not implement this behavior: once a MessagePort is started, it is started forever until closed.

(Another small thing to consider: port.onmessage = () => {} starts the port in the browser whereas port.addEventListener('message', () => {}) does not.)

The second strange thing that I've found, which is sort of related to the stopMessagePort() behavior involves closing MessagePorts.

Here is an example:

const {MessageChannel} = require('worker_threads');
const {port1, port2} = new MessageChannel();

port1.on('message', console.log);
port1.close(() => {
  port1.off('message', console.log);
});

Throws:

$ node port.js
internal/worker/io.js:155
      MessagePortPrototype.stop.call(port);
                                ^

Error: Cannot send data on closed MessagePort
    at MessagePort.eventEmitter.on (internal/worker/io.js:155:33)
    at MessagePort.emit (events.js:197:13)
    at MessagePort.removeListener (events.js:333:18)
    at MessagePort.port1.close (/home/chjj/port.js:6:9)
    at Object.onceWrapper (events.js:285:13)
    at MessagePort.emit (events.js:197:13)
    at MessagePort.onclose (internal/worker/io.js:105:8)

There seems to be a small window of time where removing the message listener will throw after the port is closed (putting the port1.off(...) call in a setTimeout doesn't seem to produce the error) [4] [5]. The browser never throws when removing events (partially because it doesn't implement this auto-stop behavior).

Metadata

Metadata

Assignees

Labels

workerIssues and PRs related to Worker support.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions