Skip to content

Commit 93417ac

Browse files
addaleaxdanbev
authored andcommitted
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 8679932 commit 93417ac

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,20 +57,22 @@ 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
}
5963
},
6064
before(asyncId) {
6165
const current = pairing.get(asyncId);
6266
if (current !== undefined) { // enter domain for this cb
63-
current.enter();
67+
// We will get the domain through current.get(), because the resource
68+
// object's .domain property makes sure it is not garbage collected.
69+
current.get().enter();
6470
}
6571
},
6672
after(asyncId) {
6773
const current = pairing.get(asyncId);
6874
if (current !== undefined) { // exit domain for this cb
69-
current.exit();
75+
current.get().exit();
7076
}
7177
},
7278
destroy(asyncId) {
@@ -167,6 +173,7 @@ class Domain extends EventEmitter {
167173
super();
168174

169175
this.members = [];
176+
this[kWeak] = new WeakReference(this);
170177
asyncHook.enable();
171178

172179
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)