Skip to content

Commit 134cbe1

Browse files
Julien GilliTrott
Julien Gilli
andcommitted
domain: allow concurrent user-land impl
Currently, only one domain-lke implementation (the core domain one) can be used to handle uncaught exceptions or unhandled error events. This PR aims at making it possible for different domain-like user-land implementations to be used concurrently (including with the core domain impl) so that the state of the core domain module (doc deprecated) does not prevent users of domains from having a well-maintained domain-like facility. Ref: #23348 Co-authored-by: Rich Trott <[email protected]>
1 parent 87aa3f1 commit 134cbe1

7 files changed

+91
-121
lines changed

doc/api/errors.md

+25-17
Original file line numberDiff line numberDiff line change
@@ -1014,23 +1014,6 @@ ongoing asynchronous operations.
10141014

10151015
`c-ares` failed to set the DNS server.
10161016

1017-
<a id="ERR_DOMAIN_CALLBACK_NOT_AVAILABLE"></a>
1018-
### `ERR_DOMAIN_CALLBACK_NOT_AVAILABLE`
1019-
1020-
The `domain` module was not usable since it could not establish the required
1021-
error handling hooks, because
1022-
[`process.setUncaughtExceptionCaptureCallback()`][] had been called at an
1023-
earlier point in time.
1024-
1025-
<a id="ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE"></a>
1026-
### `ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`
1027-
1028-
[`process.setUncaughtExceptionCaptureCallback()`][] could not be called
1029-
because the `domain` module has been loaded at an earlier point in time.
1030-
1031-
The stack trace is extended to include the point in time at which the
1032-
`domain` module had been loaded.
1033-
10341017
<a id="ERR_ENCODING_INVALID_ENCODED_DATA"></a>
10351018
### `ERR_ENCODING_INVALID_ENCODED_DATA`
10361019

@@ -2473,6 +2456,31 @@ The UTF-16 encoding was used with [`hash.digest()`][]. While the
24732456
causing the method to return a string rather than a `Buffer`, the UTF-16
24742457
encoding (e.g. `ucs` or `utf16le`) is not supported.
24752458

2459+
<a id="ERR_DOMAIN_CALLBACK_NOT_AVAILABLE"></a>
2460+
### `ERR_DOMAIN_CALLBACK_NOT_AVAILABLE`
2461+
<!-- YAML
2462+
added: v10.0.0
2463+
removed: REPLACEME
2464+
-->
2465+
2466+
The `domain` module was not usable since it could not establish the required
2467+
error handling hooks, because
2468+
[`process.setUncaughtExceptionCaptureCallback()`][] had been called at an
2469+
earlier point in time.
2470+
2471+
<a id="ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE"></a>
2472+
### `ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`
2473+
<!-- YAML
2474+
added: v10.0.0
2475+
removed: REPLACEME
2476+
-->
2477+
2478+
[`process.setUncaughtExceptionCaptureCallback()`][] could not be called
2479+
because the `domain` module has been loaded at an earlier point in time.
2480+
2481+
The stack trace is extended to include the point in time at which the
2482+
`domain` module had been loaded.
2483+
24762484
<a id="ERR_HTTP2_FRAME_ERROR"></a>
24772485
### `ERR_HTTP2_FRAME_ERROR`
24782486
<!-- YAML

doc/api/repl.md

+11-20
Original file line numberDiff line numberDiff line change
@@ -159,25 +159,19 @@ changes:
159159
The REPL uses the [`domain`][] module to catch all uncaught exceptions for that
160160
REPL session.
161161

162-
This use of the [`domain`][] module in the REPL has these side effects:
162+
This use of the [`domain`][] module in the REPL has the side effect of making
163+
uncaught exceptions not emit the [`'uncaughtException'`][] event.
163164

164-
* Uncaught exceptions only emit the [`'uncaughtException'`][] event in the
165-
standalone REPL. Adding a listener for this event in a REPL within
166-
another Node.js program results in [`ERR_INVALID_REPL_INPUT`][].
167-
168-
```js
169-
const r = repl.start();
170-
171-
r.write('process.on("uncaughtException", () => console.log("Foobar"));\n');
172-
// Output stream includes:
173-
// TypeError [ERR_INVALID_REPL_INPUT]: Listeners for `uncaughtException`
174-
// cannot be used in the REPL
165+
```js
166+
const r = repl.start();
175167

176-
r.close();
177-
```
168+
r.write('process.on("uncaughtException", () => console.log("Foobar"));\n');
169+
// Output stream includes:
170+
// TypeError [ERR_INVALID_REPL_INPUT]: Listeners for `uncaughtException`
171+
// cannot be used in the REPL
178172

179-
* Trying to use [`process.setUncaughtExceptionCaptureCallback()`][] throws
180-
an [`ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`][] error.
173+
r.close();
174+
```
181175

182176
#### Assignment of the `_` (underscore) variable
183177
<!-- YAML
@@ -746,16 +740,13 @@ For an example of running a REPL instance over [`curl(1)`][], see:
746740

747741
[TTY keybindings]: readline.md#readline_tty_keybindings
748742
[ZSH]: https://en.wikipedia.org/wiki/Z_shell
749-
[`'uncaughtException'`]: process.md#process_event_uncaughtexception
750743
[`--experimental-repl-await`]: cli.md#cli_experimental_repl_await
751-
[`ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`]: errors.md#errors_err_domain_cannot_set_uncaught_exception_capture
752-
[`ERR_INVALID_REPL_INPUT`]: errors.md#errors_err_invalid_repl_input
753744
[`curl(1)`]: https://curl.haxx.se/docs/manpage.html
754745
[`domain`]: domain.md
755-
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#process_process_setuncaughtexceptioncapturecallback_fn
756746
[`readline.InterfaceCompleter`]: readline.md#readline_use_of_the_completer_function
757747
[`repl.ReplServer`]: #repl_class_replserver
758748
[`repl.start()`]: #repl_repl_start_options
759749
[`reverse-i-search`]: #repl_reverse_i_search
750+
[`'uncaughtException'`]: process.md#process_event_uncaughtexception
760751
[`util.inspect()`]: util.md#util_util_inspect_object_options
761752
[stream]: stream.md

lib/domain.js

+11-24
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,19 @@ const {
4242
} = primordials;
4343

4444
const EventEmitter = require('events');
45-
const {
46-
ERR_DOMAIN_CALLBACK_NOT_AVAILABLE,
47-
ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE,
48-
ERR_UNHANDLED_ERROR
49-
} = require('internal/errors').codes;
45+
const { ERR_UNHANDLED_ERROR } = require('internal/errors').codes;
5046
const { createHook } = require('async_hooks');
5147
const { useDomainTrampoline } = require('internal/async_hooks');
5248

5349
// TODO(addaleax): Use a non-internal solution for this.
5450
const kWeak = Symbol('kWeak');
5551
const { WeakReference } = internalBinding('util');
5652

53+
// Communicate with events module, but don't require that
54+
// module to have to load this one, since this module has
55+
// a few side effects.
56+
EventEmitter.usingDomains = true;
57+
5758
// Overwrite process.domain with a getter/setter that will allow for more
5859
// effective optimizations
5960
const _domain = [null];
@@ -105,23 +106,7 @@ const asyncHook = createHook({
105106
}
106107
});
107108

108-
// When domains are in use, they claim full ownership of the
109-
// uncaught exception capture callback.
110-
if (process.hasUncaughtExceptionCaptureCallback()) {
111-
throw new ERR_DOMAIN_CALLBACK_NOT_AVAILABLE();
112-
}
113-
114-
// Get the stack trace at the point where `domain` was required.
115-
// eslint-disable-next-line no-restricted-syntax
116-
const domainRequireStack = new Error('require(`domain`) at this point').stack;
117-
118109
const { setUncaughtExceptionCaptureCallback } = process;
119-
process.setUncaughtExceptionCaptureCallback = function(fn) {
120-
const err = new ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE();
121-
err.stack = err.stack + '\n' + '-'.repeat(40) + '\n' + domainRequireStack;
122-
throw err;
123-
};
124-
125110

126111
let sendMakeCallbackDeprecation = false;
127112
function emitMakeCallbackDeprecation({ target, method }) {
@@ -151,8 +136,8 @@ function topLevelDomainCallback(cb, ...args) {
151136
return ret;
152137
}
153138

154-
// It's possible to enter one domain while already inside
155-
// another one. The stack is each entered domain.
139+
// It's possible to enter one domain while already inside another one. The stack
140+
// is each entered domain.
156141
let stack = [];
157142
exports._stack = stack;
158143
useDomainTrampoline(topLevelDomainCallback);
@@ -210,6 +195,8 @@ class Domain extends EventEmitter {
210195
}
211196
}
212197

198+
Domain[EventEmitter.isDomainLike] = true;
199+
213200
exports.Domain = Domain;
214201

215202
exports.create = exports.createDomain = function createDomain() {
@@ -344,7 +331,7 @@ Domain.prototype.add = function(ee) {
344331
// d.add(e);
345332
// e.add(d);
346333
// e.emit('error', er); // RangeError, stack overflow!
347-
if (this.domain && (ee instanceof Domain)) {
334+
if (this.domain && ee[EventEmitter.isDomainLike]) {
348335
for (let d = this.domain; d; d = d.domain) {
349336
if (ee === d) return;
350337
}

lib/events.js

+44-12
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ module.exports.getEventListeners = getEventListeners;
8686
EventEmitter.EventEmitter = EventEmitter;
8787

8888
EventEmitter.usingDomains = false;
89+
EventEmitter.isDomainLike = Symbol('isDomainLike');
8990

9091
EventEmitter.captureRejectionSymbol = kRejection;
9192
ObjectDefineProperty(EventEmitter, 'captureRejections', {
@@ -112,6 +113,7 @@ ObjectDefineProperty(EventEmitter.prototype, kCapture, {
112113
enumerable: false
113114
});
114115

116+
EventEmitter.prototype.domain = undefined;
115117
EventEmitter.prototype._events = undefined;
116118
EventEmitter.prototype._eventsCount = 0;
117119
EventEmitter.prototype._maxListeners = undefined;
@@ -183,6 +185,13 @@ EventEmitter.setMaxListeners =
183185
};
184186

185187
EventEmitter.init = function(opts) {
188+
this.domain = null;
189+
// If there is an active domain, then attach to it, except if the event
190+
// emitter being constructed is itself an instance of a domain-like class.
191+
// eslint-disable-next-line max-len
192+
if (EventEmitter.usingDomains && !this.constructor[EventEmitter.isDomainLike]) {
193+
this.domain = process.domain;
194+
}
186195

187196
if (this._events === undefined ||
188197
this._events === ObjectGetPrototypeOf(this)._events) {
@@ -326,12 +335,24 @@ EventEmitter.prototype.emit = function emit(type, ...args) {
326335
} else if (!doError)
327336
return false;
328337

338+
const domain = this.domain;
339+
329340
// If there is no 'error' event listener then throw.
330341
if (doError) {
331342
let er;
332343
if (args.length > 0)
333344
er = args[0];
334-
if (er instanceof Error) {
345+
if (domain !== null && domain !== undefined) {
346+
if (!er) {
347+
er = new ERR_UNHANDLED_ERROR();
348+
}
349+
if (typeof er === 'object' && er !== null) {
350+
er.domainEmitter = this;
351+
er.domain = domain;
352+
er.domainThrown = false;
353+
}
354+
domain.emit('error', er);
355+
} else if (er instanceof Error) {
335356
try {
336357
const capture = {};
337358
ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit);
@@ -344,27 +365,35 @@ EventEmitter.prototype.emit = function emit(type, ...args) {
344365
// Note: The comments on the `throw` lines are intentional, they show
345366
// up in Node's output if this results in an unhandled exception.
346367
throw er; // Unhandled 'error' event
347-
}
368+
} else {
369+
let stringifiedEr;
370+
const { inspect } = require('internal/util/inspect');
371+
try {
372+
stringifiedEr = inspect(er);
373+
} catch {
374+
stringifiedEr = er;
375+
}
348376

349-
let stringifiedEr;
350-
const { inspect } = require('internal/util/inspect');
351-
try {
352-
stringifiedEr = inspect(er);
353-
} catch {
354-
stringifiedEr = er;
377+
// At least give some kind of context to the user
378+
const err = new ERR_UNHANDLED_ERROR(stringifiedEr);
379+
err.context = er;
380+
throw err; // Unhandled 'error' event
355381
}
356382

357-
// At least give some kind of context to the user
358-
const err = new ERR_UNHANDLED_ERROR(stringifiedEr);
359-
err.context = er;
360-
throw err; // Unhandled 'error' event
383+
return false;
361384
}
362385

363386
const handler = events[type];
364387

365388
if (handler === undefined)
366389
return false;
367390

391+
let needDomainExit = false;
392+
if (domain !== null && domain !== undefined && this !== process) {
393+
domain.enter();
394+
needDomainExit = true;
395+
}
396+
368397
if (typeof handler === 'function') {
369398
const result = ReflectApply(handler, this, args);
370399

@@ -391,6 +420,9 @@ EventEmitter.prototype.emit = function emit(type, ...args) {
391420
}
392421
}
393422

423+
if (needDomainExit)
424+
domain.exit();
425+
394426
return true;
395427
};
396428

lib/internal/errors.js

-9
Original file line numberDiff line numberDiff line change
@@ -874,15 +874,6 @@ E('ERR_DIR_CONCURRENT_OPERATION',
874874
'asynchronous operations', Error);
875875
E('ERR_DNS_SET_SERVERS_FAILED', 'c-ares failed to set servers: "%s" [%s]',
876876
Error);
877-
E('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',
878-
'A callback was registered through ' +
879-
'process.setUncaughtExceptionCaptureCallback(), which is mutually ' +
880-
'exclusive with using the `domain` module',
881-
Error);
882-
E('ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE',
883-
'The `domain` module is in use, which is mutually exclusive with calling ' +
884-
'process.setUncaughtExceptionCaptureCallback()',
885-
Error);
886877
E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) {
887878
this.errno = ret;
888879
return `The encoded data was not valid for encoding ${encoding}`;
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,6 @@
11
'use strict';
22
const common = require('../common');
3-
const assert = require('assert');
43

54
process.setUncaughtExceptionCaptureCallback(common.mustNotCall());
65

7-
assert.throws(
8-
() => require('domain'),
9-
{
10-
code: 'ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',
11-
name: 'Error',
12-
message: /^A callback was registered.*with using the `domain` module/
13-
}
14-
);
15-
16-
process.setUncaughtExceptionCaptureCallback(null);
176
require('domain'); // Should not throw.

test/parallel/test-domain-set-uncaught-exception-capture-after-load.js

-28
This file was deleted.

0 commit comments

Comments
 (0)