-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
Changes from all commits
711d699
af11384
19c7eb3
69bfdd6
5d2c83e
8fd484e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,21 +2,13 @@ | |
|
||
const { safeToString } = process.binding('util'); | ||
|
||
const promiseRejectEvent = process._promiseRejectEvent; | ||
const hasBeenNotifiedProperty = new WeakMap(); | ||
const promiseToGuidProperty = new WeakMap(); | ||
const pendingUnhandledRejections = []; | ||
let lastPromiseId = 1; | ||
|
||
exports.setup = setupPromises; | ||
|
||
function getAsynchronousRejectionWarningObject(uid) { | ||
return new Error('Promise rejection was handled ' + | ||
`asynchronously (rejection id: ${uid})`); | ||
} | ||
|
||
function setupPromises(scheduleMicrotasks) { | ||
let deprecationWarned = false; | ||
exports.setup = function setup(scheduleMicrotasks) { | ||
const promiseRejectEvent = process._promiseRejectEvent; | ||
const hasBeenNotifiedProperty = new Map(); | ||
const promiseToGuidProperty = new Map(); | ||
const pendingUnhandledRejections = []; | ||
const promiseInternals = {}; | ||
let lastPromiseId = 1; | ||
|
||
process._setupPromises(function(event, promise, reason) { | ||
if (event === promiseRejectEvent.unhandled) | ||
|
@@ -25,7 +17,7 @@ function setupPromises(scheduleMicrotasks) { | |
rejectionHandled(promise); | ||
else | ||
require('assert').fail(null, null, 'unexpected PromiseRejectEvent'); | ||
}); | ||
}, promiseInternals); | ||
|
||
function unhandledRejection(promise, reason) { | ||
hasBeenNotifiedProperty.set(promise, false); | ||
|
@@ -35,34 +27,36 @@ function setupPromises(scheduleMicrotasks) { | |
|
||
function rejectionHandled(promise) { | ||
const hasBeenNotified = hasBeenNotifiedProperty.get(promise); | ||
if (hasBeenNotified !== undefined) { | ||
hasBeenNotifiedProperty.delete(promise); | ||
hasBeenNotifiedProperty.delete(promise); | ||
if (hasBeenNotified) { | ||
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 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); | ||
if (hasBeenNotified === true) { | ||
let warning = null; | ||
if (!process.listenerCount('rejectionHandled')) { | ||
// Generate the warning object early to get a good stack trace. | ||
warning = getAsynchronousRejectionWarningObject(uid); | ||
} | ||
process.nextTick(function() { | ||
if (!process.emit('rejectionHandled', promise)) { | ||
if (warning === null) | ||
warning = getAsynchronousRejectionWarningObject(uid); | ||
warning.name = 'PromiseRejectionHandledWarning'; | ||
warning.id = uid; | ||
process.emitWarning(warning); | ||
} | ||
}); | ||
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 { | ||
|
@@ -73,14 +67,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() { | ||
|
@@ -90,9 +76,9 @@ function setupPromises(scheduleMicrotasks) { | |
const reason = pendingUnhandledRejections.shift(); | ||
if (hasBeenNotifiedProperty.get(promise) === false) { | ||
hasBeenNotifiedProperty.set(promise, true); | ||
const uid = promiseToGuidProperty.get(promise); | ||
if (!process.emit('unhandledRejection', reason, promise)) { | ||
emitWarning(uid, reason); | ||
promiseInternals.trackPromise(promise); | ||
emitWarning(promise, reason); | ||
} else { | ||
hadListeners = true; | ||
} | ||
|
@@ -107,4 +93,4 @@ function setupPromises(scheduleMicrotasks) { | |
} | ||
|
||
return emitPendingUnhandledRejections; | ||
} | ||
}; |
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; whenrejectionHandled
is called there is still a strong reference to the promise (thepromise
argument) so it should still exist in theWeakMap
🤔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.