Skip to content

Commit 36dbd11

Browse files
SebmasterMylesBorins
authored andcommitted
async_hooks: add destroy event for gced AsyncResources
In cases where libraries create AsyncResources which may be emitting more events depending on usage, the only way to ensure that destroy is called properly is by calling it when the resource gets garbage collected. Backport-PR-URL: #18179 Fixes: #16153 PR-URL: #16998 Fixes: #16153 Reviewed-By: Andreas Madsen <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 331b175 commit 36dbd11

10 files changed

+196
-7
lines changed

benchmark/async_hooks/gc-tracking.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
const common = require('../common.js');
3+
const { AsyncResource } = require('async_hooks');
4+
5+
const bench = common.createBenchmark(main, {
6+
n: [1e6],
7+
method: [
8+
'trackingEnabled',
9+
'trackingDisabled',
10+
]
11+
}, {
12+
flags: ['--expose-gc']
13+
});
14+
15+
function endAfterGC(n) {
16+
setImmediate(() => {
17+
global.gc();
18+
setImmediate(() => {
19+
bench.end(n);
20+
});
21+
});
22+
}
23+
24+
function main(conf) {
25+
const n = +conf.n;
26+
27+
switch (conf.method) {
28+
case 'trackingEnabled':
29+
bench.start();
30+
for (let i = 0; i < n; i++) {
31+
new AsyncResource('foobar');
32+
}
33+
endAfterGC(n);
34+
break;
35+
case 'trackingDisabled':
36+
bench.start();
37+
for (let i = 0; i < n; i++) {
38+
new AsyncResource('foobar', { requireManualDestroy: true });
39+
}
40+
endAfterGC(n);
41+
break;
42+
default:
43+
throw new Error('Unsupported method');
44+
}
45+
}

doc/api/async_hooks.md

+13-5
Original file line numberDiff line numberDiff line change
@@ -545,12 +545,14 @@ will occur and the process will abort.
545545
The following is an overview of the `AsyncResource` API.
546546

547547
```js
548-
const { AsyncResource } = require('async_hooks');
548+
const { AsyncResource, executionAsyncId } = require('async_hooks');
549549

550550
// AsyncResource() is meant to be extended. Instantiating a
551551
// new AsyncResource() also triggers init. If triggerAsyncId is omitted then
552552
// async_hook.executionAsyncId() is used.
553-
const asyncResource = new AsyncResource(type, triggerAsyncId);
553+
const asyncResource = new AsyncResource(
554+
type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false }
555+
);
554556

555557
// Call AsyncHooks before callbacks.
556558
asyncResource.emitBefore();
@@ -568,11 +570,17 @@ asyncResource.asyncId();
568570
asyncResource.triggerAsyncId();
569571
```
570572

571-
#### `AsyncResource(type[, triggerAsyncId])`
573+
#### `AsyncResource(type[, options])`
572574

573575
* `type` {string} The type of async event.
574-
* `triggerAsyncId` {number} The ID of the execution context that created this
575-
async event.
576+
* `options` {Object}
577+
* `triggerAsyncId` {number} The ID of the execution context that created this
578+
async event. **Default:** `executionAsyncId()`
579+
* `requireManualDestroy` {boolean} Disables automatic `emitDestroy` when the
580+
object is garbage collected. This usually does not need to be set (even if
581+
`emitDestroy` is called manually), unless the resource's asyncId is retrieved
582+
and the sensitive API's `emitDestroy` is called with it.
583+
**Default:** `false`
576584

577585
Example usage:
578586

lib/async_hooks.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ const { async_hook_fields, async_id_fields } = async_wrap;
3030
const { pushAsyncIds, popAsyncIds } = async_wrap;
3131
// For performance reasons, only track Proimses when a hook is enabled.
3232
const { enablePromiseHook, disablePromiseHook } = async_wrap;
33+
// For userland AsyncResources, make sure to emit a destroy event when the
34+
// resource gets gced.
35+
const { registerDestroyHook } = async_wrap;
3336
// Properties in active_hooks are used to keep track of the set of hooks being
3437
// executed in case another hook is enabled/disabled. The new set of hooks is
3538
// then restored once the active set of hooks is finished executing.
@@ -260,13 +263,22 @@ function validateAsyncId(asyncId, type) {
260263

261264
// Embedder API //
262265

266+
const destroyedSymbol = Symbol('destroyed');
267+
263268
class AsyncResource {
264-
constructor(type, triggerAsyncId = initTriggerId()) {
269+
constructor(type, opts = {}) {
265270
if (typeof type !== 'string')
266271
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'type', 'string');
267272

273+
if (typeof opts === 'number') {
274+
opts = { triggerAsyncId: opts, requireManualDestroy: false };
275+
} else if (opts.triggerAsyncId === undefined) {
276+
opts.triggerAsyncId = initTriggerId();
277+
}
278+
268279
// Unlike emitInitScript, AsyncResource doesn't supports null as the
269280
// triggerAsyncId.
281+
const triggerAsyncId = opts.triggerAsyncId;
270282
if (!Number.isSafeInteger(triggerAsyncId) || triggerAsyncId < -1) {
271283
throw new errors.RangeError('ERR_INVALID_ASYNC_ID',
272284
'triggerAsyncId',
@@ -275,10 +287,16 @@ class AsyncResource {
275287

276288
this[async_id_symbol] = ++async_id_fields[kAsyncIdCounter];
277289
this[trigger_async_id_symbol] = triggerAsyncId;
290+
// this prop name (destroyed) has to be synchronized with C++
291+
this[destroyedSymbol] = { destroyed: false };
278292

279293
emitInitScript(
280294
this[async_id_symbol], type, this[trigger_async_id_symbol], this
281295
);
296+
297+
if (!opts.requireManualDestroy) {
298+
registerDestroyHook(this, this[async_id_symbol], this[destroyedSymbol]);
299+
}
282300
}
283301

284302
emitBefore() {
@@ -292,6 +310,7 @@ class AsyncResource {
292310
}
293311

294312
emitDestroy() {
313+
this[destroyedSymbol].destroyed = true;
295314
emitDestroyScript(this[async_id_symbol]);
296315
return this;
297316
}

src/async-wrap.cc

+41
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,46 @@ static void DisablePromiseHook(const FunctionCallbackInfo<Value>& args) {
428428
}
429429

430430

431+
class DestroyParam {
432+
public:
433+
double asyncId;
434+
v8::Persistent<Object> target;
435+
v8::Persistent<Object> propBag;
436+
};
437+
438+
439+
void AsyncWrap::WeakCallback(const v8::WeakCallbackInfo<DestroyParam>& info) {
440+
HandleScope scope(info.GetIsolate());
441+
442+
Environment* env = Environment::GetCurrent(info.GetIsolate());
443+
DestroyParam* p = info.GetParameter();
444+
Local<Object> prop_bag = PersistentToLocal(info.GetIsolate(), p->propBag);
445+
446+
Local<Value> val = prop_bag->Get(env->destroyed_string());
447+
if (val->IsFalse()) {
448+
AsyncWrap::EmitDestroy(env, p->asyncId);
449+
}
450+
p->target.Reset();
451+
p->propBag.Reset();
452+
delete p;
453+
}
454+
455+
456+
static void RegisterDestroyHook(const FunctionCallbackInfo<Value>& args) {
457+
CHECK(args[0]->IsObject());
458+
CHECK(args[1]->IsNumber());
459+
CHECK(args[2]->IsObject());
460+
461+
Isolate* isolate = args.GetIsolate();
462+
DestroyParam* p = new DestroyParam();
463+
p->asyncId = args[1].As<Number>()->Value();
464+
p->target.Reset(isolate, args[0].As<Object>());
465+
p->propBag.Reset(isolate, args[2].As<Object>());
466+
p->target.SetWeak(
467+
p, AsyncWrap::WeakCallback, v8::WeakCallbackType::kParameter);
468+
}
469+
470+
431471
void AsyncWrap::GetAsyncId(const FunctionCallbackInfo<Value>& args) {
432472
AsyncWrap* wrap;
433473
args.GetReturnValue().Set(-1);
@@ -503,6 +543,7 @@ void AsyncWrap::Initialize(Local<Object> target,
503543
env->SetMethod(target, "queueDestroyAsyncId", QueueDestroyAsyncId);
504544
env->SetMethod(target, "enablePromiseHook", EnablePromiseHook);
505545
env->SetMethod(target, "disablePromiseHook", DisablePromiseHook);
546+
env->SetMethod(target, "registerDestroyHook", RegisterDestroyHook);
506547

507548
v8::PropertyAttribute ReadOnlyDontDelete =
508549
static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete);

src/async-wrap.h

+3
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ namespace node {
8686
NODE_ASYNC_INSPECTOR_PROVIDER_TYPES(V)
8787

8888
class Environment;
89+
class DestroyParam;
8990

9091
class AsyncWrap : public BaseObject {
9192
public:
@@ -165,6 +166,8 @@ class AsyncWrap : public BaseObject {
165166

166167
virtual size_t self_size() const = 0;
167168

169+
static void WeakCallback(const v8::WeakCallbackInfo<DestroyParam> &info);
170+
168171
private:
169172
friend class PromiseWrap;
170173

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class ModuleWrap;
120120
V(cwd_string, "cwd") \
121121
V(dest_string, "dest") \
122122
V(destroy_string, "destroy") \
123+
V(destroyed_string, "destroyed") \
123124
V(detached_string, "detached") \
124125
V(disposed_string, "_disposed") \
125126
V(dns_a_string, "A") \

test/async-hooks/test-embedder.api.async-resource.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ common.expectsError(
1818
type: TypeError,
1919
});
2020
assert.throws(() => {
21-
new AsyncResource('invalid_trigger_id', null);
21+
new AsyncResource('invalid_trigger_id', { triggerAsyncId: null });
2222
}, common.expectsError({
2323
code: 'ERR_INVALID_ASYNC_ID',
2424
type: RangeError,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use strict';
2+
// Flags: --expose_gc
3+
4+
// This test ensures that userland-only AsyncResources cause a destroy event to
5+
// be emitted when they get gced.
6+
7+
const common = require('../common');
8+
const assert = require('assert');
9+
const async_hooks = require('async_hooks');
10+
11+
const destroyedIds = new Set();
12+
async_hooks.createHook({
13+
destroy: common.mustCallAtLeast((asyncId) => {
14+
destroyedIds.add(asyncId);
15+
}, 1)
16+
}).enable();
17+
18+
let asyncId = null;
19+
{
20+
const res = new async_hooks.AsyncResource('foobar');
21+
asyncId = res.asyncId();
22+
}
23+
24+
setImmediate(() => {
25+
global.gc();
26+
setImmediate(() => assert.ok(destroyedIds.has(asyncId)));
27+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict';
2+
// Flags: --expose_gc
3+
4+
// This test ensures that userland-only AsyncResources cause a destroy event to
5+
// be emitted when they get gced.
6+
7+
const common = require('../common');
8+
const async_hooks = require('async_hooks');
9+
10+
const hook = async_hooks.createHook({
11+
destroy: common.mustCall(1) // only 1 immediate is destroyed
12+
}).enable();
13+
14+
new async_hooks.AsyncResource('foobar', { requireManualDestroy: true });
15+
16+
setImmediate(() => {
17+
global.gc();
18+
setImmediate(() => {
19+
hook.disable();
20+
});
21+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict';
2+
// Flags: --expose_gc
3+
4+
// This test ensures that userland-only AsyncResources cause a destroy event to
5+
// be emitted when they get gced.
6+
7+
const common = require('../common');
8+
const async_hooks = require('async_hooks');
9+
10+
const hook = async_hooks.createHook({
11+
destroy: common.mustCall(2) // 1 immediate + manual destroy
12+
}).enable();
13+
14+
{
15+
const res = new async_hooks.AsyncResource('foobar');
16+
res.emitDestroy();
17+
}
18+
19+
setImmediate(() => {
20+
global.gc();
21+
setImmediate(() => {
22+
hook.disable();
23+
});
24+
});

0 commit comments

Comments
 (0)