Skip to content

Commit 9fdb6e6

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 1c11ea4 commit 9fdb6e6

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
@@ -459,6 +459,62 @@ init for PROMISE with id 6, trigger id: 5 # the Promise returned by then()
459459
after 6
460460
```
461461

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

464520
<!-- YAML

lib/async_hooks.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525
getHookArrays,
2626
enableHooks,
2727
disableHooks,
28+
executionAsyncResource,
2829
// Internal Embedder API
2930
newAsyncId,
3031
getDefaultTriggerAsyncId,
@@ -176,7 +177,7 @@ class AsyncResource {
176177

177178
runInAsyncScope(fn, thisArg, ...args) {
178179
const asyncId = this[async_id_symbol];
179-
emitBefore(asyncId, this[trigger_async_id_symbol]);
180+
emitBefore(asyncId, this[trigger_async_id_symbol], this);
180181

181182
const ret = thisArg === undefined ?
182183
fn(...args) :
@@ -211,6 +212,7 @@ module.exports = {
211212
createHook,
212213
executionAsyncId,
213214
triggerAsyncId,
215+
executionAsyncResource,
214216
// Embedder API
215217
AsyncResource,
216218
};

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') {
@@ -330,8 +347,8 @@ function emitInitScript(asyncId, type, triggerAsyncId, resource) {
330347
}
331348

332349

333-
function emitBeforeScript(asyncId, triggerAsyncId) {
334-
pushAsyncIds(asyncId, triggerAsyncId);
350+
function emitBeforeScript(asyncId, triggerAsyncId, resource) {
351+
pushAsyncContext(asyncId, triggerAsyncId, resource);
335352

336353
if (async_hook_fields[kBefore] > 0)
337354
emitBeforeNative(asyncId);
@@ -342,7 +359,7 @@ function emitAfterScript(asyncId) {
342359
if (async_hook_fields[kAfter] > 0)
343360
emitAfterNative(asyncId);
344361

345-
popAsyncIds(asyncId);
362+
popAsyncContext(asyncId);
346363
}
347364

348365

@@ -360,6 +377,7 @@ function clearAsyncIdStack() {
360377
async_id_fields[kExecutionAsyncId] = 0;
361378
async_id_fields[kTriggerAsyncId] = 0;
362379
async_hook_fields[kStackLength] = 0;
380+
execution_async_resources.splice(0, execution_async_resources.length);
363381
}
364382

365383

@@ -369,31 +387,33 @@ function hasAsyncIdStack() {
369387

370388

371389
// This is the equivalent of the native push_async_ids() call.
372-
function pushAsyncIds(asyncId, triggerAsyncId) {
390+
function pushAsyncContext(asyncId, triggerAsyncId, resource) {
373391
const offset = async_hook_fields[kStackLength];
374392
if (offset * 2 >= async_wrap.async_ids_stack.length)
375-
return pushAsyncIds_(asyncId, triggerAsyncId);
393+
return pushAsyncContext_(asyncId, triggerAsyncId, resource);
376394
async_wrap.async_ids_stack[offset * 2] = async_id_fields[kExecutionAsyncId];
377395
async_wrap.async_ids_stack[offset * 2 + 1] = async_id_fields[kTriggerAsyncId];
396+
execution_async_resources[offset] = resource;
378397
async_hook_fields[kStackLength]++;
379398
async_id_fields[kExecutionAsyncId] = asyncId;
380399
async_id_fields[kTriggerAsyncId] = triggerAsyncId;
381400
}
382401

383402

384403
// This is the equivalent of the native pop_async_ids() call.
385-
function popAsyncIds(asyncId) {
404+
function popAsyncContext(asyncId) {
386405
const stackLength = async_hook_fields[kStackLength];
387406
if (stackLength === 0) return false;
388407

389408
if (enabledHooksExist() && async_id_fields[kExecutionAsyncId] !== asyncId) {
390409
// Do the same thing as the native code (i.e. crash hard).
391-
return popAsyncIds_(asyncId);
410+
return popAsyncContext_(asyncId);
392411
}
393412

394413
const offset = stackLength - 1;
395414
async_id_fields[kExecutionAsyncId] = async_wrap.async_ids_stack[2 * offset];
396415
async_id_fields[kTriggerAsyncId] = async_wrap.async_ids_stack[2 * offset + 1];
416+
execution_async_resources.pop();
397417
async_hook_fields[kStackLength] = offset;
398418
return offset > 0;
399419
}
@@ -426,6 +446,7 @@ module.exports = {
426446
clearDefaultTriggerAsyncId,
427447
clearAsyncIdStack,
428448
hasAsyncIdStack,
449+
executionAsyncResource,
429450
// Internal Embedder API
430451
newAsyncId,
431452
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)