Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

async_hooks: add executionAsyncResource #30959

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dd0d929
async_hooks: add currentResource
mcollina May 4, 2018
6ab9a58
rename to ExecutionAsyncResource
mcollina Aug 21, 2018
1c4abc1
benchmark fix
mcollina Aug 21, 2018
2fb5a50
store resource on AsyncHooks object
Qard Dec 15, 2019
63fcaa5
Update docs to use executionAsyncResource
Qard Dec 16, 2019
e1a4e88
Convert execution async resource to use a stack
Qard Dec 19, 2019
b66098d
Add doc note about use of handle APIs being unsafe
Qard Dec 19, 2019
b95b514
Clean up some unnecessary bits
Qard Dec 19, 2019
a388de9
Give top-level an empty object as a resource
Qard Dec 19, 2019
b3ecef0
Minor docs improvements
Qard Dec 19, 2019
0fd3be8
Use stack emplace rather than push
Qard Dec 19, 2019
1eb13c5
Do not push promise resource on init
Qard Dec 21, 2019
9381824
Rename test files
Qard Jan 6, 2020
851e478
Interact with resource stack directly
Qard Jan 16, 2020
30f79b2
Cleanup some things
Qard Jan 16, 2020
621d089
Handle object reuse properly
Qard Feb 3, 2020
c485b39
Add test for object reuse fix
Qard Feb 3, 2020
36bcdfa
Make reused resource storage weak
Qard Feb 4, 2020
60b33d2
Stored resource needs to be strong reference
Qard Feb 7, 2020
48920e3
Reset resource after destroy and add note about strong reference safety
Qard Feb 7, 2020
1d68999
Better resource stack clearing
Qard Feb 9, 2020
a8f00fc
Make stronger statement about unsafety of using resource properties o…
Qard Feb 10, 2020
1a7243c
fixup: make benchmark tests pass
addaleax Feb 10, 2020
3402fac
fixup: docs review nit
addaleax Feb 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
151 changes: 151 additions & 0 deletions benchmark/async_hooks/async-resource-vs-destroy.js
@@ -0,0 +1,151 @@
'use strict';

const { promisify } = require('util');
const { readFile } = require('fs');
const sleep = promisify(setTimeout);
const read = promisify(readFile);
const common = require('../common.js');
const {
createHook,
executionAsyncResource,
executionAsyncId
} = require('async_hooks');
const { createServer } = require('http');

// Configuration for the http server
// there is no need for parameters in this test
const connections = 500;
const path = '/';

const bench = common.createBenchmark(main, {
type: ['async-resource', 'destroy'],
asyncMethod: ['callbacks', 'async'],
n: [1e6]
});

function buildCurrentResource(getServe) {
const server = createServer(getServe(getCLS, setCLS));
const hook = createHook({ init });
const cls = Symbol('cls');
hook.enable();

return {
server,
close
};

function getCLS() {
const resource = executionAsyncResource();
if (resource === null || !resource[cls]) {
return null;
}
return resource[cls].state;
}

function setCLS(state) {
const resource = executionAsyncResource();
if (resource === null) {
return;
}
if (!resource[cls]) {
resource[cls] = { state };
} else {
resource[cls].state = state;
}
}

function init(asyncId, type, triggerAsyncId, resource) {
var cr = executionAsyncResource();
if (cr !== null) {
resource[cls] = cr[cls];
}
}

function close() {
hook.disable();
server.close();
}
}

function buildDestroy(getServe) {
const transactions = new Map();
const server = createServer(getServe(getCLS, setCLS));
const hook = createHook({ init, destroy });
hook.enable();

return {
server,
close
};

function getCLS() {
const asyncId = executionAsyncId();
return transactions.has(asyncId) ? transactions.get(asyncId) : null;
}

function setCLS(value) {
const asyncId = executionAsyncId();
transactions.set(asyncId, value);
}

function init(asyncId, type, triggerAsyncId, resource) {
transactions.set(asyncId, getCLS());
}

function destroy(asyncId) {
transactions.delete(asyncId);
}

function close() {
hook.disable();
server.close();
}
}

function getServeAwait(getCLS, setCLS) {
return async function serve(req, res) {
setCLS(Math.random());
await sleep(10);
await read(__filename);
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({ cls: getCLS() }));
};
}

function getServeCallbacks(getCLS, setCLS) {
return function serve(req, res) {
setCLS(Math.random());
setTimeout(() => {
readFile(__filename, () => {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({ cls: getCLS() }));
});
}, 10);
};
}

const types = {
'async-resource': buildCurrentResource,
'destroy': buildDestroy
};

const asyncMethods = {
'callbacks': getServeCallbacks,
'async': getServeAwait
};

function main({ type, asyncMethod }) {
const { server, close } = types[type](asyncMethods[asyncMethod]);

server
.listen(common.PORT)
.on('listening', () => {

bench.http({
path,
connections
}, () => {
close();
});
});
}
56 changes: 56 additions & 0 deletions doc/api/async_hooks.md
Expand Up @@ -459,6 +459,62 @@ init for PROMISE with id 6, trigger id: 5 # the Promise returned by then()
after 6
```

#### `async_hooks.executionAsyncResource()`

<!-- YAML
added: REPLACEME
-->

* Returns: {Object} The resource representing the current execution.
Useful to store data within the resource.
Qard marked this conversation as resolved.
Show resolved Hide resolved

Resource objects returned by `executionAsyncResource()` are most often internal
Node.js handle objects with undocumented APIs. Using any functions or properties
on the object is likely to crash your application and should be avoided.

Using `executionAsyncResource()` in the top-level execution context will
return an empty object as there is no handle or request object to use,
but having an object representing the top-level can be helpful.

```js
const { open } = require('fs');
const { executionAsyncId, executionAsyncResource } = require('async_hooks');

console.log(executionAsyncId(), executionAsyncResource()); // 1 {}
open(__filename, 'r', (err, fd) => {
console.log(executionAsyncId(), executionAsyncResource()); // 7 FSReqWrap
});
```

This can be used to implement continuation local storage without the
using of a tracking `Map` to store the metadata:
addaleax marked this conversation as resolved.
Show resolved Hide resolved

```js
const { createServer } = require('http');
const {
executionAsyncId,
executionAsyncResource,
createHook
} = require('async_hooks');
const sym = Symbol('state'); // Private symbol to avoid pollution

createHook({
init(asyncId, type, triggerAsyncId, resource) {
const cr = executionAsyncResource();
if (cr) {
resource[sym] = cr[sym];
}
}
}).enable();

const server = createServer(function(req, res) {
executionAsyncResource()[sym] = { state: req.url };
setTimeout(function() {
res.end(JSON.stringify(executionAsyncResource()[sym]));
}, 100);
}).listen(3000);
```

#### `async_hooks.executionAsyncId()`

<!-- YAML
Expand Down
4 changes: 3 additions & 1 deletion lib/async_hooks.js
Expand Up @@ -25,6 +25,7 @@ const {
getHookArrays,
enableHooks,
disableHooks,
executionAsyncResource,
// Internal Embedder API
newAsyncId,
getDefaultTriggerAsyncId,
Expand Down Expand Up @@ -176,7 +177,7 @@ class AsyncResource {

runInAsyncScope(fn, thisArg, ...args) {
const asyncId = this[async_id_symbol];
emitBefore(asyncId, this[trigger_async_id_symbol]);
Qard marked this conversation as resolved.
Show resolved Hide resolved
emitBefore(asyncId, this[trigger_async_id_symbol], this);

const ret = thisArg === undefined ?
fn(...args) :
Expand Down Expand Up @@ -211,6 +212,7 @@ module.exports = {
createHook,
executionAsyncId,
triggerAsyncId,
executionAsyncResource,
// Embedder API
AsyncResource,
};
45 changes: 33 additions & 12 deletions lib/internal/async_hooks.js
Expand Up @@ -28,18 +28,26 @@ const async_wrap = internalBinding('async_wrap');
* 3. executionAsyncId of the current resource.
*
* async_ids_stack is a Float64Array that contains part of the async ID
* stack. Each pushAsyncIds() call adds two doubles to it, and each
* popAsyncIds() call removes two doubles from it.
* stack. Each pushAsyncContext() call adds two doubles to it, and each
* popAsyncContext() call removes two doubles from it.
* It has a fixed size, so if that is exceeded, calls to the native
* side are used instead in pushAsyncIds() and popAsyncIds().
* side are used instead in pushAsyncContext() and popAsyncContext().
*/
const { async_hook_fields, async_id_fields, owner_symbol } = async_wrap;
const {
async_hook_fields,
async_id_fields,
execution_async_resources,
owner_symbol
} = async_wrap;
// Store the pair executionAsyncId and triggerAsyncId in a std::stack on
// Environment::AsyncHooks::async_ids_stack_ tracks the resource responsible for
// the current execution stack. This is unwound as each resource exits. In the
// case of a fatal exception this stack is emptied after calling each hook's
// after() callback.
const { pushAsyncIds: pushAsyncIds_, popAsyncIds: popAsyncIds_ } = async_wrap;
const {
pushAsyncContext: pushAsyncContext_,
popAsyncContext: popAsyncContext_
} = async_wrap;
// For performance reasons, only track Promises when a hook is enabled.
const { enablePromiseHook, disablePromiseHook } = async_wrap;
// Properties in active_hooks are used to keep track of the set of hooks being
Expand Down Expand Up @@ -92,6 +100,15 @@ const emitDestroyNative = emitHookFactory(destroy_symbol, 'emitDestroyNative');
const emitPromiseResolveNative =
emitHookFactory(promise_resolve_symbol, 'emitPromiseResolveNative');

const topLevelResource = {};

function executionAsyncResource() {
const index = async_hook_fields[kStackLength] - 1;
if (index === -1) return topLevelResource;
const resource = execution_async_resources[index];
return resource;
}

// Used to fatally abort the process if a callback throws.
function fatalError(e) {
if (typeof e.stack === 'string') {
Expand Down Expand Up @@ -330,8 +347,8 @@ function emitInitScript(asyncId, type, triggerAsyncId, resource) {
}


function emitBeforeScript(asyncId, triggerAsyncId) {
pushAsyncIds(asyncId, triggerAsyncId);
function emitBeforeScript(asyncId, triggerAsyncId, resource) {
pushAsyncContext(asyncId, triggerAsyncId, resource);

if (async_hook_fields[kBefore] > 0)
emitBeforeNative(asyncId);
Expand All @@ -342,7 +359,7 @@ function emitAfterScript(asyncId) {
if (async_hook_fields[kAfter] > 0)
emitAfterNative(asyncId);

popAsyncIds(asyncId);
popAsyncContext(asyncId);
}


Expand All @@ -360,6 +377,7 @@ function clearAsyncIdStack() {
async_id_fields[kExecutionAsyncId] = 0;
async_id_fields[kTriggerAsyncId] = 0;
async_hook_fields[kStackLength] = 0;
execution_async_resources.splice(0, execution_async_resources.length);
}


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


// This is the equivalent of the native push_async_ids() call.
function pushAsyncIds(asyncId, triggerAsyncId) {
function pushAsyncContext(asyncId, triggerAsyncId, resource) {
const offset = async_hook_fields[kStackLength];
if (offset * 2 >= async_wrap.async_ids_stack.length)
return pushAsyncIds_(asyncId, triggerAsyncId);
return pushAsyncContext_(asyncId, triggerAsyncId, resource);
async_wrap.async_ids_stack[offset * 2] = async_id_fields[kExecutionAsyncId];
async_wrap.async_ids_stack[offset * 2 + 1] = async_id_fields[kTriggerAsyncId];
execution_async_resources[offset] = resource;
async_hook_fields[kStackLength]++;
async_id_fields[kExecutionAsyncId] = asyncId;
async_id_fields[kTriggerAsyncId] = triggerAsyncId;
}


// This is the equivalent of the native pop_async_ids() call.
function popAsyncIds(asyncId) {
function popAsyncContext(asyncId) {
const stackLength = async_hook_fields[kStackLength];
if (stackLength === 0) return false;

if (enabledHooksExist() && async_id_fields[kExecutionAsyncId] !== asyncId) {
// Do the same thing as the native code (i.e. crash hard).
return popAsyncIds_(asyncId);
return popAsyncContext_(asyncId);
}

const offset = stackLength - 1;
async_id_fields[kExecutionAsyncId] = async_wrap.async_ids_stack[2 * offset];
async_id_fields[kTriggerAsyncId] = async_wrap.async_ids_stack[2 * offset + 1];
execution_async_resources.pop();
async_hook_fields[kStackLength] = offset;
return offset > 0;
}
Expand Down Expand Up @@ -426,6 +446,7 @@ module.exports = {
clearDefaultTriggerAsyncId,
clearAsyncIdStack,
hasAsyncIdStack,
executionAsyncResource,
// Internal Embedder API
newAsyncId,
getOrSetAsyncId,
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/process/task_queues.js
Expand Up @@ -71,7 +71,7 @@ function processTicksAndRejections() {
do {
while (tock = queue.shift()) {
const asyncId = tock[async_id_symbol];
emitBefore(asyncId, tock[trigger_async_id_symbol]);
emitBefore(asyncId, tock[trigger_async_id_symbol], tock);

try {
const callback = tock.callback;
Expand Down
6 changes: 3 additions & 3 deletions lib/internal/timers.js
Expand Up @@ -96,7 +96,7 @@ const {
emitInit,
emitBefore,
emitAfter,
emitDestroy
emitDestroy,
} = require('internal/async_hooks');

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

const asyncId = immediate[async_id_symbol];
emitBefore(asyncId, immediate[trigger_async_id_symbol]);
emitBefore(asyncId, immediate[trigger_async_id_symbol], immediate);

try {
const argv = immediate._argv;
Expand Down Expand Up @@ -537,7 +537,7 @@ function getTimerCallbacks(runNextTicks) {
continue;
}

emitBefore(asyncId, timer[trigger_async_id_symbol]);
emitBefore(asyncId, timer[trigger_async_id_symbol], timer);

let start;
if (timer._repeat)
Expand Down