-
Notifications
You must be signed in to change notification settings - Fork 30k
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
lib/src: exit on gc unhandled promise #15126
Conversation
I think explicitly silencing by adding a no-op unhandled rejection handler should be fine :) |
By the way, the idea of adding a flag to Node that would restore the current behaviour was floated somewhere, and since then I have come to think that would be a really good thing to have. |
Yeah. I've been working on this a bit this week in between other things. The strategy I've been working on is to use flags to enable a couple of possible behaviors with the current behavior set as the default. My goal has been to have a pull request sometime next week. |
Is that not somewhat independent from this PR? |
@BridgeAR Yes, but landing this PR and then introducing the flag would create a period where the current behaviour would not be available, and with respect to timing I’m a bit worried that might hit the Node 9 timeframe perfectly :| That’s all, I’d just like to make sure it’s at least considered. |
@@ -1,3 +1,6 @@ | |||
(node:30929) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): ReferenceError: consol is not defined | |||
(node:30929) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): ReferenceError: consol is not defined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PID should be replaced with an asterisk for wildcard matching.
doc/api/deprecations.md
Outdated
Unhandled promise rejections are deprecated. In the future, promise rejections | ||
that are not handled will terminate the Node.js process with a non-zero exit | ||
code. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't it be marked EOL instead of being removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. It should be marked End-of-life
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, thanks a lot for picking this up!
Left some comments, just to make sure we agree on the semantics we eventually want:
- Not handling a promise within the "after-nextTick" should still produce a warning but not a deprecation warning. (TODO(Benjamingr) re-read our research from last time on the impact on async/await patterns)
- Not handling a promise that performed GC should exit the process and print its stack trace like a regular exception.
const common = require('../common'); | ||
|
||
const p = new Promise((res, rej) => { | ||
consol.log('oops'); // eslint-disable-line no-undef |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No reason to cause an implicit reference error here - this can just be throw new ReferenceError(...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not want to change to much on the code and left the tests pretty much as they were. I will change it though.
const common = require('../common'); | ||
|
||
const p = new Promise((res, rej) => { | ||
consol.log('oops'); // eslint-disable-line no-undef |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No reason to cause an implicit reference error here - this can just be throw new ReferenceError(...)
// Manually call GC due to possible memory constraints with attempting to | ||
// trigger it "naturally". | ||
setTimeout(common.mustCall(() => { | ||
global.gc(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really flakey, why 3 times? Why these delays in the setTimeout?
Are we testing that V8 doesn't drop a live reference in GC here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have been forced to check gc-based behaviour in setTimeout
s in other cases in recent V8 versions. I don’t think anybody of has really gotten to the ground of it so far.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see, then a comment would be nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not certain how that comment should look like. Do you have a example?
|
||
Promise.reject(new Error('oops')); | ||
|
||
// Manually call GC due to possible memory constraints with attempting to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this inside a setTimeout
with 2 delay? Can't the handlers be added prior and then the global.gc calls made synchronously? (Also, why 3 times)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above for the 3 times. I did not write these tests originally and it is somewhat difficult for me to know why it was exactly written the way it is. I think the two milliseconds delay is arbitrary. I am also not sure about your second question. As far as I see it the setTimeout is meant to make sure the rejected promise is fully recognized to be unhandled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably was arbitrary
test/message/promise_fast_reject.js
Outdated
const assert = require('assert'); | ||
|
||
new Promise(function(res, rej) { | ||
consol.log('One'); // eslint-disable-line no-undef |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto about explicit errors and throw
ing directly rather than consol
consol.log('Three'); // eslint-disable-line no-undef | ||
}); | ||
|
||
new Promise((res, rej) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what this tests, new Promise
runs synchronously
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. It should probably be a nextTick instead.
}), 1); | ||
}); | ||
|
||
process.on('rejectionHandled', () => {}); // Ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default behavior for rejectionHandled
is ignore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will print a warning otherwise and I wanted to have the output as clean as possible.
@@ -6,6 +6,8 @@ const assert = require('assert'); | |||
|
|||
let expected_result, promise; | |||
|
|||
process.on('unhandledRejection', () => {}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we fix the test instead of ignoring the errors?
If we can't it might indicate a design problem
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@addaleax recommended to ignore this. I did not find the cause of the issue here especially as the output is really little.
let deprecationWarned = false; | ||
exports.setup = function setup(scheduleMicrotasks) { | ||
const promiseRejectEvent = process._promiseRejectEvent; | ||
const hasBeenNotifiedProperty = new Map(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain why a regular Map
is sufficient here?
A WeakMap was used for a very specific reason in order to not interfere with GC.
On a related note, is there any way we can test the unhandled rejection tracking code does not leak memory?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I realize it doesn't do the same thing exactly and tracking happens in C++, but still)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WeakMap
s can't be enumerated, but no enumeration is done here; when rejectionHandled
is called there is still a strong reference to the promise (the promise
argument) so it should still exist in the WeakMap
🤔
Not sure why a Map
is needed either; it just creates the need for .delete
calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason why I changed it is that I can not see a reason to use WeakMap
. All entries are either going to be handled and in that case explicitly deleted or they result in a unhandled rejection that will now terminate the process. Using the WeakMap has a performance overhead and the GC has to do more work.
if (hasBeenNotified !== undefined) { | ||
hasBeenNotifiedProperty.delete(promise); | ||
hasBeenNotifiedProperty.delete(promise); | ||
if (hasBeenNotified) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the .delete
can be inside the if
I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should always be deleted, otherwise the reference would be kept. This function is only called once per handled rejection and it can not be freed otherwise.
The current behavior can be restored by adding an |
I only looked at this briefly but it has the same issue as the old PR, doesn't it? It adds a lot of overhead per promise. Weakly persistent handles aren't cheap. |
@bnoordhuis yes, this isn't here yet - both behavior and overhead |
src: use std::map for the promise reject map Refs: nodejs#5292 Refs: nodejs/promises#26 Refs: nodejs#6355 Refs: nodejs#6375
0452cde
to
8fd484e
Compare
I addressed the comments and rebased due to conflicts. |
Ping |
@BridgeAR not 100% sure by reading through the comments so far. Is this now in a state where its ready to land ? Just asking because of the earlier comments about not being there in terms of behavior and overhead. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just pointing out an obvious bit of my WIP
there may be other spots too
|
||
// Make some sort of list size check so as to not leak memory. | ||
if (env->promise_tracker_.Size() > 10000) { | ||
// XXX(Fishrock123): Do some intelligent logic here? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for hopefully obvious reasons this shouldn't be merged in this state
Local<Value> err = GetPromiseReason(&env, orp); | ||
Local<Message> message = Exception::CreateMessage(isolate, err); | ||
|
||
// XXX(Fishrock123): Should this just call ReportException and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
example 2
I am in favour of the approach. I would love to see this landed in Node 9. |
@BridgeAR status? |
It looks like once this is merged, all code currently causing Where can I go to find the discussion in which this decision was reached, so I can learn more about the reasoning etc.? I think one could argue that asynchronously handling exceptions is not an invalid use of Promises. I also think you may be surprised by how many codebases will be affected - unless telemetry data is available, of course. Edit: apologies, I now see that this will only occur when the object is GC'd - which will mean catching the Promise is impossible anyway. Might be worth linking some more context/background in the PR description. |
@BridgeAR do you still want to pursue this? I would like to see it land early enough for Node 10. |
In general: yes. I am not sure if I can work on it next week though. The week after is probably possible. But it is also fine for me if someone else wants to work on it right now. |
Just a heads up: I am currently looking into this again and try to have a update about mid next week. |
Closing in favor of #20097 |
This is a follow up on #12010. Is is not complete and mainly a rebase with a few minor fixes.
All contributions go to @Fishrock123, @addaleax, @matthewloring and @ofrobots.
I have no idea about C++ and it is difficult for me to continue any further, therefore I would ask someone to step foward to continue the work here 😄
One N-API test is failing and I do not know how to properly fix that one.Edit: I fixed the test but it seems like I have a regression somewhere in the error message printed. I am looking into that soon.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
lib, src, doc