Repository navigation
lib/src: exit on gc unhandled promise #15126
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
Changes from 1 commit
711d699
af11384
19c7eb3
69bfdd6
5d2c83e
8fd484e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,15 +2,13 @@ | |
|
|
||
| const { safeToString } = process.binding('util'); | ||
|
|
||
| const promiseRejectEvent = process._promiseRejectEvent; | ||
| const hasBeenNotifiedProperty = new WeakMap(); | ||
| const pendingUnhandledRejections = []; | ||
|
|
||
| exports.setup = setupPromises; | ||
|
|
||
| function setupPromises(scheduleMicrotasks) { | ||
| let deprecationWarned = false; | ||
| exports.setup = function setup(scheduleMicrotasks) { | ||
| const promiseRejectEvent = process._promiseRejectEvent; | ||
| const hasBeenNotifiedProperty = new Map(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain why a regular 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?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more.
Not sure why a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| const promiseToGuidProperty = new Map(); | ||
| const pendingUnhandledRejections = []; | ||
| const promiseInternals = {}; | ||
| let lastPromiseId = 1; | ||
|
|
||
| process._setupPromises(function(event, promise, reason) { | ||
| if (event === promiseRejectEvent.unhandled) | ||
|
|
@@ -20,32 +18,48 @@ function setupPromises(scheduleMicrotasks) { | |
| else | ||
| require('assert').fail(null, null, 'unexpected PromiseRejectEvent'); | ||
| }, function getPromiseReason(data) { | ||
| return data[data.indexOf('[[PromiseValue]]') + 1]; | ||
| // TODO: Remove this function as it should not be necessary. | ||
| return data[3]; | ||
| }, promiseInternals); | ||
|
|
||
| function unhandledRejection(promise, reason) { | ||
| hasBeenNotifiedProperty.set(promise, false); | ||
| promiseToGuidProperty.set(promise, lastPromiseId++); | ||
| addPendingUnhandledRejection(promise, reason); | ||
| } | ||
|
|
||
| function rejectionHandled(promise) { | ||
| const hasBeenNotified = hasBeenNotifiedProperty.get(promise); | ||
| if (hasBeenNotified !== undefined) { | ||
| hasBeenNotifiedProperty.delete(promise); | ||
| if (hasBeenNotified === true) { | ||
| promiseInternals.untrackPromise(promise); | ||
| process.nextTick(function() { | ||
| process.emit('rejectionHandled', promise); | ||
| }); | ||
| hasBeenNotifiedProperty.delete(promise); | ||
| if (hasBeenNotified) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| const uid = promiseToGuidProperty.get(promise); | ||
| promiseToGuidProperty.delete(promise); | ||
| let warning = null; | ||
| if (!process.listenerCount('rejectionHandled')) { | ||
| // Generate the warning object early to get a good stack trace. | ||
| warning = new Error('Promise rejection was handled ' + | ||
| `asynchronously (rejection id: ${uid})`); | ||
| } | ||
|
|
||
| promiseInternals.untrackPromise(promise); | ||
| process.nextTick(function() { | ||
| if (!process.emit('rejectionHandled', promise)) { | ||
| if (warning === null) | ||
| warning = new Error('Promise rejection was handled ' + | ||
| `asynchronously (rejection id: ${uid})`); | ||
| warning.name = 'PromiseRejectionHandledWarning'; | ||
| warning.id = uid; | ||
| process.emitWarning(warning); | ||
| } | ||
| }); | ||
| } else { | ||
| promiseToGuidProperty.delete(promise); | ||
| } | ||
| } | ||
|
|
||
| function emitWarning(uid, reason) { | ||
| const warning = new Error( | ||
| `Unhandled promise rejection (rejection id: ${uid}): ` + | ||
| safeToString(reason)); | ||
| function emitWarning(promise, reason) { | ||
| const uid = promiseToGuidProperty.get(promise); | ||
| const warning = new Error('Unhandled promise rejection ' + | ||
| `(rejection id: ${uid}): ${safeToString(reason)}`); | ||
| warning.name = 'UnhandledPromiseRejectionWarning'; | ||
| warning.id = uid; | ||
| try { | ||
|
|
@@ -56,14 +70,6 @@ function setupPromises(scheduleMicrotasks) { | |
| // ignored | ||
| } | ||
| process.emitWarning(warning); | ||
| if (!deprecationWarned) { | ||
| deprecationWarned = true; | ||
| process.emitWarning( | ||
| '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.', | ||
| 'DeprecationWarning', 'DEP0018'); | ||
| } | ||
| } | ||
|
|
||
| function emitPendingUnhandledRejections() { | ||
|
|
@@ -75,6 +81,7 @@ function setupPromises(scheduleMicrotasks) { | |
| hasBeenNotifiedProperty.set(promise, true); | ||
| if (!process.emit('unhandledRejection', reason, promise)) { | ||
| promiseInternals.trackPromise(promise); | ||
| emitWarning(promise, reason); | ||
| } else { | ||
| hadListeners = true; | ||
| } | ||
|
|
@@ -89,4 +96,4 @@ function setupPromises(scheduleMicrotasks) { | |
| } | ||
|
|
||
| return emitPendingUnhandledRejections; | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| 'use strict'; | ||
| const common = require('../common'); | ||
| const assert = require('assert'); | ||
|
|
||
| const p1 = new Promise((res, rej) => { | ||
| consol.log('One'); // eslint-disable-line no-undef | ||
|
|
@@ -20,7 +21,10 @@ new Promise((res, rej) => { | |
| }), 1); | ||
| }); | ||
|
|
||
| process.on('unhandledRejection', () => {}); // Ignore | ||
| process.on('rejectionHandled', () => {}); // Ignore | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The default behavior for
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| process.on('uncaughtException', (err) => | ||
| common.fail('Should not trigger uncaught exception')); | ||
| assert.fail('Should not trigger uncaught exception')); | ||
|
|
||
| process.on('exit', () => process._rawDebug('exit event emitted')); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,8 @@ | |
|
|
||
| // We should always have the stacktrace of the oldest rejection. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this is behavior we can rely on - if V8 decides to GC
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I somewhat agree. It seems like we can rely on it for now though. Would you still want this to be changed? |
||
|
|
||
| const common = require('../common'); | ||
| require('../common'); | ||
| const assert = require('assert'); | ||
|
|
||
| new Promise(function(res, rej) { | ||
| consol.log('One'); // eslint-disable-line no-undef | ||
|
|
@@ -12,7 +13,9 @@ new Promise(function(res, rej) { | |
| consol.log('Two'); // eslint-disable-line no-undef | ||
| }); | ||
|
|
||
| process.on('unhandledRejection', () => {}); // Ignore | ||
|
|
||
| process.on('uncaughtException', (err) => | ||
| common.fail('Should not trigger uncaught exception')); | ||
| assert.fail('Should not trigger uncaught exception')); | ||
|
|
||
| process.on('exit', () => process._rawDebug('exit event emitted')); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,24 @@ | ||
| // Flags: --expose-gc | ||
|
|
||
| 'use strict'; | ||
| const common = require('../common'); | ||
|
|
||
| // Flags: --expose-gc | ||
| const common = require('../common'); | ||
| const assert = require('assert'); | ||
|
|
||
| Promise.reject(new Error('oops')); | ||
|
|
||
| // Manually call GC due to possible memory contraints with attempting to | ||
| // Manually call GC due to possible memory constraints with attempting to | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this inside a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. probably was arbitrary |
||
| // trigger it "naturally". | ||
| setTimeout(common.mustCall(() => { | ||
| /* eslint-disable no-undef */ | ||
| gc(); | ||
| gc(); | ||
| gc(); | ||
| /* eslint-enable no-undef */ | ||
| }, 1), 2); | ||
| global.gc(); | ||
| global.gc(); | ||
| global.gc(); | ||
| }), 2); | ||
|
|
||
| process.on('beforeExit', () => | ||
| common.fail('beforeExit should not be reached')); | ||
| process.on('unhandledRejection', () => {}); // Ignore | ||
|
|
||
| process.on('uncaughtException', (err) => { | ||
| // XXX(Fishrock123): This test is currently broken... | ||
| console.log(err.stack); | ||
| common.fail('Should not trigger uncaught exception'); | ||
| assert.fail('Should not trigger uncaught exception'); | ||
| }); | ||
|
|
||
| process.on('exit', () => process._rawDebug('exit event emitted')); | ||
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