Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

worker_threads parentPort extends EventTarget in Node 14 but extends EventEmitter in Node 12 #35835

Closed
snowinferno opened this issue Oct 27, 2020 · 14 comments
Labels
worker Issues and PRs related to Worker support.

Comments

@snowinferno
Copy link

  • Version: v14.15.0
  • Platform: Darwin SuperSecretHostname 19.6.0 Darwin Kernel Version 19.6.0: Mon Aug 31 22:12:52 PDT 2020; root:xnu-6153.141.2~1/RELEASE_X86_64 x86_64
  • Subsystem: worker_threads

What steps will reproduce the bug?

Run this script under node v14.15.0

const { isMainThread, parentPort, Worker } = require('worker_threads');

if (isMainThread) {
    const worker = new Worker(__filename);
    worker.on('error', (message) => console.dir(message));
} else {
    parentPort.emit('error', "This is my error message.");
}

How often does it reproduce? Is there a required condition?

Every time the script is run.

Required condition: Node v14.15.0

What is the expected behavior?

The error should reflect Error [ERR_UNHANDLED_ERROR]: Unhandled error. ('This is my error message.') with a stack trace.

What do you see instead?

The error reflects TypeError [Error]: parentPort.emit is not a function with a stack trace.
parentPort seemingly does not extend EventEmitter as documentation indicates and behavior under node v12.19.0 shows.

Additional information

This appears to be related to commit 0aa3809b6be on the v14.x branch

This would be a breaking change in a stable API since this same script behaves as expected under Node v12.19.0.

Inspecting parentPort I find that contrary to expectation and documentation, it extends EventTarget instead of EventEmitter.

MessagePort [EventTarget] {
  [Symbol(kEvents)]: Map(2) {
    'newListener' => { size: 1, next: [Listener] },
    'removeListener' => { size: 1, next: [Listener] }
  },
  [Symbol(kMaxListeners)]: 10,
  [Symbol(kMaxListenersWarned)]: false,
  [Symbol(kNewListener)]: [Function (anonymous)],
  [Symbol(kRemoveListener)]: [Function (anonymous)]
}
@indutny
Copy link
Member

indutny commented Oct 27, 2020

cc @addaleax @jasnell

@addaleax addaleax added the worker Issues and PRs related to Worker support. label Oct 27, 2020
@addaleax
Copy link
Member

So … I’m conflicted – on the one hand, this is working as expected. MessagePort instances are supposed to be EventTargets, not EventEmitters, we just didn’t have EventTarget when we introduced it in the first place.

That being said, the fact that .emit() was being dropped was not something I had considered when I made the switch, and I’m surprised to realize that it’s missing from NodeEventTarget. I’m not sure if we want to implement that or not, given that it’s not exactly a straightforward thing to do once Web-style listeners come into play (e.g. what subclass of Event should a .addEventListener() listener receive when somebody uses .emit('error', error)?) /cc @benjamingr @jasnell

(Either way, I’ll have to go update my slides for nodeconf.remote next week … 😄)

@addaleax
Copy link
Member

Also: Yes, this is definitely missing documentation updates. I’ll get onto that.

@snowinferno Is this causing any real-world trouble? I’m surprised that parentPort is being used to emit custom events in any case…

@snowinferno
Copy link
Author

It's not causing real world trouble yet.

The company I work for is mainly on Node 10 and 12 while we prepare for and test Node 14.

I happened to be using node 14 when I ran a test with worker_thread code to perform some tasks. In the event an error happens during processing, we're using parentPort.emit('error', error) to bubble it back to the main thread through worker.on('error',...).
This seemed like the most fitting way to get the error back to the main thread instead of sending a message like

{
  "message": "some error message",
  "isWarning": true|false,
  "isError": true|flase,
  "isFatal": true|false,
  "error": { Error object }
}

@snowinferno
Copy link
Author

The module I'm working on is in a state where it is being picked up by application builds but the method of triggering it is not yet user-friendly.

If the decision is to leave the worker_thread implementation as-is, we will have to prioritize changing how errors get bubbled up before we make Node 14 available for production applications.

@benjamingr
Copy link
Member

. I’m not sure if we want to implement that or not, given that it’s not exactly a straightforward thing to do once Web-style listeners come into play.

I think it's fine to shim this (emit) for convenience.

(e.g. what subclass of Event should a .addEventListener() listener receive when somebody uses .emit('error', error)?)

Probably CustomEvent with Error in its detail (like browser unhandledrejection events) though we don't have that yet I guess?

(Either way, I’ll have to go update my slides for nodeconf.remote next week … 😄)

Good luck with your talk!

@addaleax
Copy link
Member

In the event an error happens during processing, we're using parentPort.emit('error', error) to bubble it back to the main thread through worker.on('error',...).
This seemed like the most fitting way to get the error back to the main thread instead of sending a message like

Ok, but just so we’re on the same page – parentPort.emit('error', error) does not lead to worker.on('error') being emitted. worker.on('error') is only emitted for uncaught exceptions inside the Worker. What happens is that emitting an 'error' on any EventEmitter that does not have an 'error' listener will be turned into an exception. So what you’re doing is essentially equivalent to just writing throw error;.

nodejs-github-bot pushed a commit that referenced this issue Oct 28, 2020
Refs: #34057
Refs: #35835

PR-URL: #35839
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
@benjamingr
Copy link
Member

benjamingr commented Oct 28, 2020

@addaleax

I think it's fine to shim this (emit) for convenience.

BTW Would it help (you) at all if I worked this? (made a PR that adds emit to NodeEventTarget and wraps a CustomEvent?)

(I am happy to do it if it's helpful)

@addaleax
Copy link
Member

@benjamingr I mean, I’d get to it at some point, but feel free to pick this up if you like :)

@benjamingr
Copy link
Member

Hmm, one possible difference is support for multiple args - I'll do what I can and I'll make a PR to dicusss the "product" of how "emit" can look.

@benjamingr
Copy link
Member

@addaleax how would you feel about a NodeCustomEvent type (that would play the role of MessageEvents except higher up?

It would be the default for kCreateEvent that MessagePort overrides.

Also - we currently use data on MessageEvents but the DOM uses .detail on CustomEvent, which would be better as a property name?

@addaleax
Copy link
Member

@addaleax how would you feel about a NodeCustomEvent type (that would play the role of MessageEvents except higher up?

The name sounds good to me :)

It would be the default for kCreateEvent that MessagePort overrides.

That’s fine, although we should make sure that the kCreateEvent in MessagePort calls that base class function if it doesn’t recognize type (i.e. type is not in message, messageerror).

Also - we currently use data on MessageEvents but the DOM uses .detail on CustomEvent, which would be better as a property name?

I think detail is just fine if we stick with CustomEvent.data is just what we use because it’s what MessageEvents are supposed to have :)

@benjamingr
Copy link
Member

Opened a PR in #35851 with a bunch of questions

@snowinferno
Copy link
Author

Ok, but just so we’re on the same page – parentPort.emit('error', error) does not lead to worker.on('error') being emitted. worker.on('error') is only emitted for uncaught exceptions inside the Worker. What happens is that emitting an 'error' on any EventEmitter that does not have an 'error' listener will be turned into an exception. So what you’re doing is essentially equivalent to just writing throw error;.

@addaleax Ahh, I see. So we are actually creating a side-effect that just happened to bubble up through worker.on('error'...).
In light of that, I'll schedule some work to remove this side-effect logic and handle bubbling the errors up in a more appropriate way.

targos pushed a commit that referenced this issue Nov 3, 2020
Refs: #34057
Refs: #35835

PR-URL: #35839
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
BethGriggs pushed a commit that referenced this issue Dec 8, 2020
Refs: #34057
Refs: #35835

PR-URL: #35839
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
BethGriggs pushed a commit that referenced this issue Dec 10, 2020
Refs: #34057
Refs: #35835

PR-URL: #35839
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
BethGriggs pushed a commit that referenced this issue Dec 15, 2020
Refs: #34057
Refs: #35835

PR-URL: #35839
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
worker Issues and PRs related to Worker support.
Projects
None yet
Development

No branches or pull requests

5 participants