-
Notifications
You must be signed in to change notification settings - Fork 30.9k
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
process: allow multiple uncaught exception capture calbacks #24279
Changes from all commits
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 |
---|---|---|
|
@@ -1807,11 +1807,12 @@ This function is only available on POSIX platforms (i.e. not Windows or | |
Android). | ||
This feature is not available in [`Worker`][] threads. | ||
|
||
## process.setUncaughtExceptionCaptureCallback(fn) | ||
## process.setUncaughtExceptionCaptureCallback(owner, fn) | ||
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. A bit concerned about backwards compat here. Is the new 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 not use a Set instead of a Map and use the functions themselves as keys 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. @devsnek's suggestion seems like a good alternative, I'll try that 👍 |
||
<!-- YAML | ||
added: v9.3.0 | ||
--> | ||
|
||
* `owner` {symbol} | ||
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.
|
||
* `fn` {Function|null} | ||
|
||
The `process.setUncaughtExceptionCaptureCallback()` function sets a function | ||
|
@@ -1824,12 +1825,7 @@ command line or set through [`v8.setFlagsFromString()`][], the process will | |
not abort. | ||
|
||
To unset the capture function, | ||
`process.setUncaughtExceptionCaptureCallback(null)` may be used. Calling this | ||
method with a non-`null` argument while another capture function is set will | ||
throw an error. | ||
|
||
Using this function is mutually exclusive with using the deprecated | ||
[`domain`][] built-in module. | ||
`process.setUncaughtExceptionCaptureCallback(owner, null)` may be used. | ||
|
||
## process.stderr | ||
|
||
|
@@ -2137,7 +2133,6 @@ cases: | |
[`Worker`]: worker_threads.html#worker_threads_class_worker | ||
[`console.error()`]: console.html#console_console_error_data_args | ||
[`console.log()`]: console.html#console_console_log_data_args | ||
[`domain`]: domain.html | ||
[`net.Server`]: net.html#net_class_net_server | ||
[`net.Socket`]: net.html#net_class_net_socket | ||
[`NODE_OPTIONS`]: cli.html#cli_node_options_options | ||
|
@@ -2150,7 +2145,7 @@ cases: | |
[`process.hrtime()`]: #process_process_hrtime_time | ||
[`process.hrtime.bigint()`]: #process_process_hrtime_bigint | ||
[`process.kill()`]: #process_process_kill_pid_signal | ||
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn | ||
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_owner_fn | ||
[`promise.catch()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch | ||
[`require()`]: globals.html#globals_require | ||
[`require.main`]: modules.html#modules_accessing_the_main_module | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -29,12 +29,12 @@ | |||||
const util = require('util'); | ||||||
const EventEmitter = require('events'); | ||||||
const { | ||||||
ERR_DOMAIN_CALLBACK_NOT_AVAILABLE, | ||||||
ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE, | ||||||
ERR_UNHANDLED_ERROR | ||||||
} = require('internal/errors').codes; | ||||||
const { createHook } = require('async_hooks'); | ||||||
|
||||||
const CAPTURE_CB_KEY = Symbol('domain'); | ||||||
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 we do something similar to what https://github.com/nodejs/node/blob/master/doc/guides/using-symbols.md suggests? e.g.
Suggested change
Having identical names for the variable and the symbol itself helps with grepping :) 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. Great suggestion, I'll do that 👍 |
||||||
|
||||||
// overwrite process.domain with a getter/setter that will allow for more | ||||||
// effective optimizations | ||||||
var _domain = [null]; | ||||||
|
@@ -74,23 +74,7 @@ const asyncHook = createHook({ | |||||
} | ||||||
}); | ||||||
|
||||||
// When domains are in use, they claim full ownership of the | ||||||
// uncaught exception capture callback. | ||||||
if (process.hasUncaughtExceptionCaptureCallback()) { | ||||||
throw new ERR_DOMAIN_CALLBACK_NOT_AVAILABLE(); | ||||||
} | ||||||
|
||||||
// Get the stack trace at the point where `domain` was required. | ||||||
// eslint-disable-next-line no-restricted-syntax | ||||||
const domainRequireStack = new Error('require(`domain`) at this point').stack; | ||||||
|
||||||
const { setUncaughtExceptionCaptureCallback } = process; | ||||||
process.setUncaughtExceptionCaptureCallback = function(fn) { | ||||||
const err = new ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE(); | ||||||
err.stack = err.stack + '\n' + '-'.repeat(40) + '\n' + domainRequireStack; | ||||||
throw err; | ||||||
}; | ||||||
|
||||||
|
||||||
let sendMakeCallbackDeprecation = false; | ||||||
function emitMakeCallbackDeprecation() { | ||||||
|
@@ -125,10 +109,10 @@ internalBinding('domain').enable(topLevelDomainCallback); | |||||
|
||||||
function updateExceptionCapture() { | ||||||
if (stack.every((domain) => domain.listenerCount('error') === 0)) { | ||||||
setUncaughtExceptionCaptureCallback(null); | ||||||
setUncaughtExceptionCaptureCallback(CAPTURE_CB_KEY, null); | ||||||
} else { | ||||||
setUncaughtExceptionCaptureCallback(null); | ||||||
setUncaughtExceptionCaptureCallback((er) => { | ||||||
setUncaughtExceptionCaptureCallback(CAPTURE_CB_KEY, null); | ||||||
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. Setting this to |
||||||
setUncaughtExceptionCaptureCallback(CAPTURE_CB_KEY, (er) => { | ||||||
return process.domain._errorHandler(er); | ||||||
}); | ||||||
} | ||||||
|
@@ -211,7 +195,7 @@ Domain.prototype._errorHandler = function(er) { | |||||
// Clear the uncaughtExceptionCaptureCallback so that we know that, even | ||||||
// if technically the top-level domain is still active, it would | ||||||
// be ok to abort on an uncaught exception at this point | ||||||
setUncaughtExceptionCaptureCallback(null); | ||||||
setUncaughtExceptionCaptureCallback(CAPTURE_CB_KEY, null); | ||||||
try { | ||||||
caught = this.emit('error', er); | ||||||
} finally { | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,7 +26,7 @@ | |
_shouldAbortOnUncaughtToggle }, | ||
{ internalBinding, NativeModule }, | ||
triggerFatalException) { | ||
const exceptionHandlerState = { captureFn: null }; | ||
const exceptionHandlerState = { captureFns: new Map() }; | ||
const isMainThread = internalBinding('worker').threadId === 0; | ||
|
||
function startup() { | ||
|
@@ -579,8 +579,10 @@ | |
// call that threw and was never cleared. So clear it now. | ||
clearDefaultTriggerAsyncId(); | ||
|
||
if (exceptionHandlerState.captureFn !== null) { | ||
exceptionHandlerState.captureFn(er); | ||
if (exceptionHandlerState.captureFns.size !== 0) { | ||
for (const fn of exceptionHandlerState.captureFns.values()) { | ||
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 branch into multiple ownership here feels... off. these kinds of handlers should be at an application level, so the possibility of having like 30 functions all handling an uncaught exception seems a bit confusing 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. 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. @misterdjules if we had some sort of stopPropagation behaviour, i think this would be doable. maybe for (const fn of exceptionHandlerState.captureFns.values()) {
if (fn() !== false) { break; } // return `false` to signal you didn't handle it, and the next function should be used
} 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 agree with @devsnek – the purpose of these hooks is to (possibly?) take ownership for the errors, otherwise we’d basically end up with a list of 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 purpose of calling all (possibly many) registered callbacks was to address the use case tested by https://github.com/misterdjules/domaine/blob/master/test/test-domaine-domain-compat.js. Basically, if one module runs a function within a user-land implemented domain-like module and that function runs another function within another domain-like instance, when an error is thrown in that second function, the goal would be to have both domain-like instances to handle that same error. The rationale for this is that we'd want those different domain-like implementations to not step on each other by handling errors for each other (think for instance of the case where those domain-like instances are created by different dependencies of the same Node app). However, this is an opinionated design decision and I think I'd be totally fine with only allowing the top-level active domain-like instance ( In that case though it seems we'd need to be able to keep a single stack of all different domain-like instances and that exposing an API to set the capture callback to user-land is not useful, as there would be a need for setting one of them internally in core. @devsnek @addaleax What are your thoughts on this? |
||
fn(er); | ||
} | ||
} else if (!process.emit('uncaughtException', er)) { | ||
// If someone handled it, then great. otherwise, die in C++ land | ||
// since that means that we'll exit the process, emit the 'exit' event. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -171,6 +171,8 @@ function setupChildProcessIpcChannel() { | |
} | ||
} | ||
|
||
process.domainsMap = new Map(); | ||
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.
|
||
|
||
module.exports = { | ||
setupStdio, | ||
setupProcessMethods, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,6 @@ const { | |
ERR_INVALID_ARG_TYPE, | ||
ERR_INVALID_OPT_VALUE, | ||
ERR_OUT_OF_RANGE, | ||
ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET, | ||
ERR_UNKNOWN_SIGNAL | ||
} | ||
} = require('internal/errors'); | ||
|
@@ -213,24 +212,27 @@ function setupUncaughtExceptionCapture(exceptionHandlerState, | |
// shouldAbortOnUncaughtToggle is a typed array for faster | ||
// communication with JS. | ||
|
||
process.setUncaughtExceptionCaptureCallback = function(fn) { | ||
process.setUncaughtExceptionCaptureCallback = function(owner, fn) { | ||
if (fn === null) { | ||
exceptionHandlerState.captureFn = fn; | ||
exceptionHandlerState.captureFns.delete(owner); | ||
shouldAbortOnUncaughtToggle[0] = 1; | ||
return; | ||
} | ||
|
||
if (typeof fn !== 'function') { | ||
throw new ERR_INVALID_ARG_TYPE('fn', ['Function', 'null'], fn); | ||
} | ||
if (exceptionHandlerState.captureFn !== null) { | ||
throw new ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET(); | ||
|
||
if (typeof owner !== 'symbol') { | ||
throw new ERR_INVALID_ARG_TYPE('owner', ['Symbol'], owner); | ||
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 guess this makes this semver-major? 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 not use a Set and just add/remove the functions themselves 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. @devsnek's suggestion seems like a good alternative, I'll try that 👍 |
||
} | ||
exceptionHandlerState.captureFn = fn; | ||
|
||
exceptionHandlerState.captureFns.set(owner, fn); | ||
shouldAbortOnUncaughtToggle[0] = 0; | ||
}; | ||
|
||
process.hasUncaughtExceptionCaptureCallback = function() { | ||
return exceptionHandlerState.captureFn !== null; | ||
return exceptionHandlerState.captureFns.size !== 0; | ||
}; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,6 @@ | ||
'use strict'; | ||
const common = require('../common'); | ||
const captureSym = Symbol('foo'); | ||
|
||
process.setUncaughtExceptionCaptureCallback(common.mustNotCall()); | ||
|
||
common.expectsError( | ||
() => require('domain'), | ||
{ | ||
code: 'ERR_DOMAIN_CALLBACK_NOT_AVAILABLE', | ||
type: Error, | ||
message: /^A callback was registered.*with using the `domain` module/ | ||
} | ||
); | ||
|
||
process.setUncaughtExceptionCaptureCallback(null); | ||
process.setUncaughtExceptionCaptureCallback(captureSym, common.mustNotCall()); | ||
require('domain'); // Should not throw. |
This file was deleted.
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 a section for errors that were once present but are no longer below in this document :)
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.
Thanks for the pointer 👍 I'll add those errors to the
Legacy Node.js Error Codes
section. Just to make sure: is it the section you were referring 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.
Yes, exactly. :)