From 6be51296e4e2a977c2c825719912bef1aa5b5bfc Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 4 May 2018 08:00:07 +0200 Subject: [PATCH] async_hooks: add executionAsyncResource Remove the need for the destroy hook in the basic APM case. Co-authored-by: Stephen Belanger PR-URL: https://github.com/nodejs/node/pull/30959 Reviewed-By: Matteo Collina Reviewed-By: Anna Henningsen Reviewed-By: Vladimir de Turckheim Reviewed-By: Chengzhong Wu Reviewed-By: James M Snell --- .../async_hooks/async-resource-vs-destroy.js | 151 ++++++++++++++++++ doc/api/async_hooks.md | 56 +++++++ lib/async_hooks.js | 4 +- lib/internal/async_hooks.js | 45 ++++-- lib/internal/process/task_queues.js | 2 +- lib/internal/timers.js | 6 +- src/api/callback.cc | 10 +- src/async_wrap.cc | 38 +++-- src/async_wrap.h | 6 +- src/env-inl.h | 30 +++- src/env.h | 8 +- src/node_internals.h | 6 +- src/node_main_instance.cc | 5 +- src/node_platform.cc | 6 +- src/node_worker.cc | 5 +- .../test-async-exec-resource-http.js | 30 ++++ test/benchmark/test-benchmark-async-hooks.js | 4 +- ...nc-hooks-execution-async-resource-await.js | 54 +++++++ ...st-async-hooks-execution-async-resource.js | 49 ++++++ 19 files changed, 458 insertions(+), 57 deletions(-) create mode 100644 benchmark/async_hooks/async-resource-vs-destroy.js create mode 100644 test/async-hooks/test-async-exec-resource-http.js create mode 100644 test/parallel/test-async-hooks-execution-async-resource-await.js create mode 100644 test/parallel/test-async-hooks-execution-async-resource.js diff --git a/benchmark/async_hooks/async-resource-vs-destroy.js b/benchmark/async_hooks/async-resource-vs-destroy.js new file mode 100644 index 00000000000000..4464dd5f93e7de --- /dev/null +++ b/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(); + }); + }); +} diff --git a/doc/api/async_hooks.md b/doc/api/async_hooks.md index 66534ca85029e6..51792e2dd1d5be 100644 --- a/doc/api/async_hooks.md +++ b/doc/api/async_hooks.md @@ -459,6 +459,62 @@ init for PROMISE with id 6, trigger id: 5 # the Promise returned by then() after 6 ``` +#### `async_hooks.executionAsyncResource()` + + + +* Returns: {Object} The resource representing the current execution. + Useful to store data within the resource. + +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 +use of a tracking `Map` to store the metadata: + +```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()`