Skip to content

Commit 60c5099

Browse files
committed
domain: avoid circular memory references
Avoid circular references that the JS engine cannot see through because it involves an `async id` β‡’ `domain` link. Using weak references is not a great solution, because it increases the domain module’s dependency on internals and the added calls into C++ may affect performance, but it seems like the least bad one. PR-URL: #25993 Fixes: #23862 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Π‘ΠΊΠΎΠ²ΠΎΡ€ΠΎΠ΄Π° Никита АндрССвич <chalkerx@gmail.com> Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Refael Ackermann <refack@gmail.com>
1 parent 731c273 commit 60c5099

File tree

2 files changed

+46
-3
lines changed

2 files changed

+46
-3
lines changed

β€Žlib/domain.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ const {
3535
} = require('internal/errors').codes;
3636
const { createHook } = require('async_hooks');
3737

38+
// TODO(addaleax): Use a non-internal solution for this.
39+
const kWeak = Symbol('kWeak');
40+
const { WeakReference } = internalBinding('util');
41+
3842
// Overwrite process.domain with a getter/setter that will allow for more
3943
// effective optimizations
4044
var _domain = [null];
@@ -53,7 +57,7 @@ const asyncHook = createHook({
5357
init(asyncId, type, triggerAsyncId, resource) {
5458
if (process.domain !== null && process.domain !== undefined) {
5559
// If this operation is created while in a domain, let's mark it
56-
pairing.set(asyncId, process.domain);
60+
pairing.set(asyncId, process.domain[kWeak]);
5761
resource.domain = process.domain;
5862
if (resource.promise !== undefined &&
5963
resource.promise instanceof Promise) {
@@ -67,13 +71,15 @@ const asyncHook = createHook({
6771
before(asyncId) {
6872
const current = pairing.get(asyncId);
6973
if (current !== undefined) { // enter domain for this cb
70-
current.enter();
74+
// We will get the domain through current.get(), because the resource
75+
// object's .domain property makes sure it is not garbage collected.
76+
current.get().enter();
7177
}
7278
},
7379
after(asyncId) {
7480
const current = pairing.get(asyncId);
7581
if (current !== undefined) { // exit domain for this cb
76-
current.exit();
82+
current.get().exit();
7783
}
7884
},
7985
destroy(asyncId) {
@@ -174,6 +180,7 @@ class Domain extends EventEmitter {
174180
super();
175181

176182
this.members = [];
183+
this[kWeak] = new WeakReference(this);
177184
asyncHook.enable();
178185

179186
this.on('removeListener', updateExceptionCapture);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Flags: --expose-gc
2+
'use strict';
3+
const common = require('../common');
4+
const onGC = require('../common/ongc');
5+
const assert = require('assert');
6+
const async_hooks = require('async_hooks');
7+
const domain = require('domain');
8+
const EventEmitter = require('events');
9+
10+
// This test makes sure that the (async id β†’ domain) map which is part of the
11+
// domain module does not get in the way of garbage collection.
12+
// See: https://github.com/nodejs/node/issues/23862
13+
14+
let d = domain.create();
15+
d.run(() => {
16+
const resource = new async_hooks.AsyncResource('TestResource');
17+
const emitter = new EventEmitter();
18+
19+
d.remove(emitter);
20+
d.add(emitter);
21+
22+
emitter.linkToResource = resource;
23+
assert.strictEqual(emitter.domain, d);
24+
assert.strictEqual(resource.domain, d);
25+
26+
// This would otherwise be a circular chain now:
27+
// emitter β†’ resource β†’ async id β‡’ domain β†’ emitter.
28+
// Make sure that all of these objects are released:
29+
30+
onGC(resource, { ongc: common.mustCall() });
31+
onGC(d, { ongc: common.mustCall() });
32+
onGC(emitter, { ongc: common.mustCall() });
33+
});
34+
35+
d = null;
36+
global.gc();

0 commit comments

Comments
Β (0)