Skip to content

Commit f7adfcc

Browse files
mcollinaQard
authored andcommitted
async_hooks: add executionAsyncResource
Remove the need for the destroy hook in the basic APM case. Co-authored-by: Stephen Belanger <[email protected]> PR-URL: #30959 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Vladimir de Turckheim <[email protected]> Reviewed-By: Chengzhong Wu <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent e5a64e5 commit f7adfcc

19 files changed

+458
-57
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
'use strict';
2+
3+
const { promisify } = require('util');
4+
const { readFile } = require('fs');
5+
const sleep = promisify(setTimeout);
6+
const read = promisify(readFile);
7+
const common = require('../common.js');
8+
const {
9+
createHook,
10+
executionAsyncResource,
11+
executionAsyncId
12+
} = require('async_hooks');
13+
const { createServer } = require('http');
14+
15+
// Configuration for the http server
16+
// there is no need for parameters in this test
17+
const connections = 500;
18+
const path = '/';
19+
20+
const bench = common.createBenchmark(main, {
21+
type: ['async-resource', 'destroy'],
22+
asyncMethod: ['callbacks', 'async'],
23+
n: [1e6]
24+
});
25+
26+
function buildCurrentResource(getServe) {
27+
const server = createServer(getServe(getCLS, setCLS));
28+
const hook = createHook({ init });
29+
const cls = Symbol('cls');
30+
hook.enable();
31+
32+
return {
33+
server,
34+
close
35+
};
36+
37+
function getCLS() {
38+
const resource = executionAsyncResource();
39+
if (resource === null || !resource[cls]) {
40+
return null;
41+
}
42+
return resource[cls].state;
43+
}
44+
45+
function setCLS(state) {
46+
const resource = executionAsyncResource();
47+
if (resource === null) {
48+
return;
49+
}
50+
if (!resource[cls]) {
51+
resource[cls] = { state };
52+
} else {
53+
resource[cls].state = state;
54+
}
55+
}
56+
57+
function init(asyncId, type, triggerAsyncId, resource) {
58+
var cr = executionAsyncResource();
59+
if (cr !== null) {
60+
resource[cls] = cr[cls];
61+
}
62+
}
63+
64+
function close() {
65+
hook.disable();
66+
server.close();
67+
}
68+
}
69+
70+
function buildDestroy(getServe) {
71+
const transactions = new Map();
72+
const server = createServer(getServe(getCLS, setCLS));
73+
const hook = createHook({ init, destroy });
74+
hook.enable();
75+
76+
return {
77+
server,
78+
close
79+
};
80+
81+
function getCLS() {
82+
const asyncId = executionAsyncId();
83+
return transactions.has(asyncId) ? transactions.get(asyncId) : null;
84+
}
85+
86+
function setCLS(value) {
87+
const asyncId = executionAsyncId();
88+
transactions.set(asyncId, value);
89+
}
90+
91+
function init(asyncId, type, triggerAsyncId, resource) {
92+
transactions.set(asyncId, getCLS());
93+
}
94+
95+
function destroy(asyncId) {
96+
transactions.delete(asyncId);
97+
}
98+
99+
function close() {
100+
hook.disable();
101+
server.close();
102+
}
103+
}
104+
105+
function getServeAwait(getCLS, setCLS) {
106+
return async function serve(req, res) {
107+
setCLS(Math.random());
108+
await sleep(10);
109+
await read(__filename);
110+
res.setHeader('content-type', 'application/json');
111+
res.end(JSON.stringify({ cls: getCLS() }));
112+
};
113+
}
114+
115+
function getServeCallbacks(getCLS, setCLS) {
116+
return function serve(req, res) {
117+
setCLS(Math.random());
118+
setTimeout(() => {
119+
readFile(__filename, () => {
120+
res.setHeader('content-type', 'application/json');
121+
res.end(JSON.stringify({ cls: getCLS() }));
122+
});
123+
}, 10);
124+
};
125+
}
126+
127+
const types = {
128+
'async-resource': buildCurrentResource,
129+
'destroy': buildDestroy
130+
};
131+
132+
const asyncMethods = {
133+
'callbacks': getServeCallbacks,
134+
'async': getServeAwait
135+
};
136+
137+
function main({ type, asyncMethod }) {
138+
const { server, close } = types[type](asyncMethods[asyncMethod]);
139+
140+
server
141+
.listen(common.PORT)
142+
.on('listening', () => {
143+
144+
bench.http({
145+
path,
146+
connections
147+
}, () => {
148+
close();
149+
});
150+
});
151+
}

doc/api/async_hooks.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,62 @@ init for PROMISE with id 6, trigger id: 5 # the Promise returned by then()
464464
after 6
465465
```
466466

467+
#### `async_hooks.executionAsyncResource()`
468+
469+
<!-- YAML
470+
added: REPLACEME
471+
-->
472+
473+
* Returns: {Object} The resource representing the current execution.
474+
Useful to store data within the resource.
475+
476+
Resource objects returned by `executionAsyncResource()` are most often internal
477+
Node.js handle objects with undocumented APIs. Using any functions or properties
478+
on the object is likely to crash your application and should be avoided.
479+
480+
Using `executionAsyncResource()` in the top-level execution context will
481+
return an empty object as there is no handle or request object to use,
482+
but having an object representing the top-level can be helpful.
483+
484+
```js
485+
const { open } = require('fs');
486+
const { executionAsyncId, executionAsyncResource } = require('async_hooks');
487+
488+
console.log(executionAsyncId(), executionAsyncResource()); // 1 {}
489+
open(__filename, 'r', (err, fd) => {
490+
console.log(executionAsyncId(), executionAsyncResource()); // 7 FSReqWrap
491+
});
492+
```
493+
494+
This can be used to implement continuation local storage without the
495+
use of a tracking `Map` to store the metadata:
496+
497+
```js
498+
const { createServer } = require('http');
499+
const {
500+
executionAsyncId,
501+
executionAsyncResource,
502+
createHook
503+
} = require('async_hooks');
504+
const sym = Symbol('state'); // Private symbol to avoid pollution
505+
506+
createHook({
507+
init(asyncId, type, triggerAsyncId, resource) {
508+
const cr = executionAsyncResource();
509+
if (cr) {
510+
resource[sym] = cr[sym];
511+
}
512+
}
513+
}).enable();
514+
515+
const server = createServer(function(req, res) {
516+
executionAsyncResource()[sym] = { state: req.url };
517+
setTimeout(function() {
518+
res.end(JSON.stringify(executionAsyncResource()[sym]));
519+
}, 100);
520+
}).listen(3000);
521+
```
522+
467523
#### `async_hooks.executionAsyncId()`
468524

469525
<!-- YAML

lib/async_hooks.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const {
2626
getHookArrays,
2727
enableHooks,
2828
disableHooks,
29+
executionAsyncResource,
2930
// Internal Embedder API
3031
newAsyncId,
3132
getDefaultTriggerAsyncId,
@@ -178,7 +179,7 @@ class AsyncResource {
178179

179180
runInAsyncScope(fn, thisArg, ...args) {
180181
const asyncId = this[async_id_symbol];
181-
emitBefore(asyncId, this[trigger_async_id_symbol]);
182+
emitBefore(asyncId, this[trigger_async_id_symbol], this);
182183

183184
try {
184185
const ret = thisArg === undefined ?
@@ -217,6 +218,7 @@ module.exports = {
217218
createHook,
218219
executionAsyncId,
219220
triggerAsyncId,
221+
executionAsyncResource,
220222
// Embedder API
221223
AsyncResource,
222224
};

lib/internal/async_hooks.js

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,26 @@ const async_wrap = internalBinding('async_wrap');
2828
* 3. executionAsyncId of the current resource.
2929
*
3030
* async_ids_stack is a Float64Array that contains part of the async ID
31-
* stack. Each pushAsyncIds() call adds two doubles to it, and each
32-
* popAsyncIds() call removes two doubles from it.
31+
* stack. Each pushAsyncContext() call adds two doubles to it, and each
32+
* popAsyncContext() call removes two doubles from it.
3333
* It has a fixed size, so if that is exceeded, calls to the native
34-
* side are used instead in pushAsyncIds() and popAsyncIds().
34+
* side are used instead in pushAsyncContext() and popAsyncContext().
3535
*/
36-
const { async_hook_fields, async_id_fields, owner_symbol } = async_wrap;
36+
const {
37+
async_hook_fields,
38+
async_id_fields,
39+
execution_async_resources,
40+
owner_symbol
41+
} = async_wrap;
3742
// Store the pair executionAsyncId and triggerAsyncId in a std::stack on
3843
// Environment::AsyncHooks::async_ids_stack_ tracks the resource responsible for
3944
// the current execution stack. This is unwound as each resource exits. In the
4045
// case of a fatal exception this stack is emptied after calling each hook's
4146
// after() callback.
42-
const { pushAsyncIds: pushAsyncIds_, popAsyncIds: popAsyncIds_ } = async_wrap;
47+
const {
48+
pushAsyncContext: pushAsyncContext_,
49+
popAsyncContext: popAsyncContext_
50+
} = async_wrap;
4351
// For performance reasons, only track Promises when a hook is enabled.
4452
const { enablePromiseHook, disablePromiseHook } = async_wrap;
4553
// Properties in active_hooks are used to keep track of the set of hooks being
@@ -92,6 +100,15 @@ const emitDestroyNative = emitHookFactory(destroy_symbol, 'emitDestroyNative');
92100
const emitPromiseResolveNative =
93101
emitHookFactory(promise_resolve_symbol, 'emitPromiseResolveNative');
94102

103+
const topLevelResource = {};
104+
105+
function executionAsyncResource() {
106+
const index = async_hook_fields[kStackLength] - 1;
107+
if (index === -1) return topLevelResource;
108+
const resource = execution_async_resources[index];
109+
return resource;
110+
}
111+
95112
// Used to fatally abort the process if a callback throws.
96113
function fatalError(e) {
97114
if (typeof e.stack === 'string') {
@@ -334,8 +351,8 @@ function emitInitScript(asyncId, type, triggerAsyncId, resource) {
334351
}
335352

336353

337-
function emitBeforeScript(asyncId, triggerAsyncId) {
338-
pushAsyncIds(asyncId, triggerAsyncId);
354+
function emitBeforeScript(asyncId, triggerAsyncId, resource) {
355+
pushAsyncContext(asyncId, triggerAsyncId, resource);
339356

340357
if (hasHooks(kBefore))
341358
emitBeforeNative(asyncId);
@@ -346,7 +363,7 @@ function emitAfterScript(asyncId) {
346363
if (hasHooks(kAfter))
347364
emitAfterNative(asyncId);
348365

349-
popAsyncIds(asyncId);
366+
popAsyncContext(asyncId);
350367
}
351368

352369

@@ -364,6 +381,7 @@ function clearAsyncIdStack() {
364381
async_id_fields[kExecutionAsyncId] = 0;
365382
async_id_fields[kTriggerAsyncId] = 0;
366383
async_hook_fields[kStackLength] = 0;
384+
execution_async_resources.splice(0, execution_async_resources.length);
367385
}
368386

369387

@@ -373,31 +391,33 @@ function hasAsyncIdStack() {
373391

374392

375393
// This is the equivalent of the native push_async_ids() call.
376-
function pushAsyncIds(asyncId, triggerAsyncId) {
394+
function pushAsyncContext(asyncId, triggerAsyncId, resource) {
377395
const offset = async_hook_fields[kStackLength];
378396
if (offset * 2 >= async_wrap.async_ids_stack.length)
379-
return pushAsyncIds_(asyncId, triggerAsyncId);
397+
return pushAsyncContext_(asyncId, triggerAsyncId, resource);
380398
async_wrap.async_ids_stack[offset * 2] = async_id_fields[kExecutionAsyncId];
381399
async_wrap.async_ids_stack[offset * 2 + 1] = async_id_fields[kTriggerAsyncId];
400+
execution_async_resources[offset] = resource;
382401
async_hook_fields[kStackLength]++;
383402
async_id_fields[kExecutionAsyncId] = asyncId;
384403
async_id_fields[kTriggerAsyncId] = triggerAsyncId;
385404
}
386405

387406

388407
// This is the equivalent of the native pop_async_ids() call.
389-
function popAsyncIds(asyncId) {
408+
function popAsyncContext(asyncId) {
390409
const stackLength = async_hook_fields[kStackLength];
391410
if (stackLength === 0) return false;
392411

393412
if (enabledHooksExist() && async_id_fields[kExecutionAsyncId] !== asyncId) {
394413
// Do the same thing as the native code (i.e. crash hard).
395-
return popAsyncIds_(asyncId);
414+
return popAsyncContext_(asyncId);
396415
}
397416

398417
const offset = stackLength - 1;
399418
async_id_fields[kExecutionAsyncId] = async_wrap.async_ids_stack[2 * offset];
400419
async_id_fields[kTriggerAsyncId] = async_wrap.async_ids_stack[2 * offset + 1];
420+
execution_async_resources.pop();
401421
async_hook_fields[kStackLength] = offset;
402422
return offset > 0;
403423
}
@@ -430,6 +450,7 @@ module.exports = {
430450
clearDefaultTriggerAsyncId,
431451
clearAsyncIdStack,
432452
hasAsyncIdStack,
453+
executionAsyncResource,
433454
// Internal Embedder API
434455
newAsyncId,
435456
getOrSetAsyncId,

lib/internal/process/task_queues.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function processTicksAndRejections() {
7171
do {
7272
while (tock = queue.shift()) {
7373
const asyncId = tock[async_id_symbol];
74-
emitBefore(asyncId, tock[trigger_async_id_symbol]);
74+
emitBefore(asyncId, tock[trigger_async_id_symbol], tock);
7575

7676
try {
7777
const callback = tock.callback;

lib/internal/timers.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const {
9696
emitInit,
9797
emitBefore,
9898
emitAfter,
99-
emitDestroy
99+
emitDestroy,
100100
} = require('internal/async_hooks');
101101

102102
// Symbols for storing async id state.
@@ -448,7 +448,7 @@ function getTimerCallbacks(runNextTicks) {
448448
prevImmediate = immediate;
449449

450450
const asyncId = immediate[async_id_symbol];
451-
emitBefore(asyncId, immediate[trigger_async_id_symbol]);
451+
emitBefore(asyncId, immediate[trigger_async_id_symbol], immediate);
452452

453453
try {
454454
const argv = immediate._argv;
@@ -537,7 +537,7 @@ function getTimerCallbacks(runNextTicks) {
537537
continue;
538538
}
539539

540-
emitBefore(asyncId, timer[trigger_async_id_symbol]);
540+
emitBefore(asyncId, timer[trigger_async_id_symbol], timer);
541541

542542
let start;
543543
if (timer._repeat)

0 commit comments

Comments
 (0)