From dd0d929b4d8be466512d727b2930e79f46451499 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 4 May 2018 08:00:07 +0200 Subject: [PATCH 01/24] async_hooks: add currentResource Remove the need for the destroy hook in the basic APM case. --- .../current-resource-vs-destroy.js | 165 ++++++++++++++++++ doc/api/async_hooks.md | 51 ++++++ lib/async_hooks.js | 2 + lib/internal/async_hooks.js | 8 +- lib/internal/process/task_queues.js | 2 +- lib/internal/timers.js | 8 +- src/api/callback.cc | 2 +- src/async_wrap.cc | 36 +++- src/async_wrap.h | 3 +- ...test-async-hooks-current-resource-await.js | 56 ++++++ .../test-async-hooks-current-resource.js | 51 ++++++ 11 files changed, 375 insertions(+), 9 deletions(-) create mode 100644 benchmark/async_hooks/current-resource-vs-destroy.js create mode 100644 test/parallel/test-async-hooks-current-resource-await.js create mode 100644 test/parallel/test-async-hooks-current-resource.js diff --git a/benchmark/async_hooks/current-resource-vs-destroy.js b/benchmark/async_hooks/current-resource-vs-destroy.js new file mode 100644 index 00000000000000..ba5923f255bc0f --- /dev/null +++ b/benchmark/async_hooks/current-resource-vs-destroy.js @@ -0,0 +1,165 @@ +'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, + currentResource, + 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: ['current-resource', 'destroy'], + method: ['callbacks', 'async'], + n: [1e6] +}); + +function buildCurrentResource(getServe) { + const server = createServer(getServe(getCLS, setCLS)); + const hook = createHook({ init }); + const cls = Symbol('cls'); + let closed = false; + hook.enable(); + + return { + server, + close + }; + + function getCLS() { + // we need to protect this, as once the hook is + // disabled currentResource will return null + if (closed) { + return; + } + + const resource = currentResource(); + if (!resource[cls]) { + return null; + } + return resource[cls].state; + } + + function setCLS(state) { + // we need to protect this, as once the hook is + // disabled currentResource will return null + if (closed) { + return; + } + const resource = currentResource(); + if (!resource[cls]) { + resource[cls] = { state }; + } else { + resource[cls].state = state; + } + } + + function init(asyncId, type, triggerAsyncId, resource) { + if (type === 'TIMERWRAP') return; + + var cr = currentResource(); + if (cr) { + resource[cls] = cr[cls]; + } + } + + function close() { + closed = true; + 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) { + if (type === 'TIMERWRAP') return; + + 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 = { + 'current-resource': buildCurrentResource, + 'destroy': buildDestroy +}; + +const asyncMethod = { + 'callbacks': getServeCallbacks, + 'async': getServeAwait +}; + +function main({ type, method }) { + const { server, close } = types[type](asyncMethod[method]); + + 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..b133a33c0d6efb 100644 --- a/doc/api/async_hooks.md +++ b/doc/api/async_hooks.md @@ -459,6 +459,57 @@ init for PROMISE with id 6, trigger id: 5 # the Promise returned by then() after 6 ``` +#### `async_hooks.currentResource()` + + + +* Returns: {Object} The resource that triggered the current + execution context. + Useful to store data within the resource. + +```js +const { open } = require('fs'); +const { executionAsyncId, currentResource } = require('async_hooks'); + +console.log(executionAsyncId(), currentResource()); // 1 null +open(__filename, 'r', (err, fd) => { + console.log(executionAsyncId(), currentResource()); // 7 FSReqWrap +}); +``` + +This can be used to implement continuation local storage without the +using of a tracking `Map` to store the metadata: + +```js +const { createServer } = require('http'); +const { + executionAsyncId, + currentResource, + createHook +} = require('async_hooks'); +const sym = Symbol('state'); // Private symbol to avoid pollution + +createHook({ + init(asyncId, type, triggerAsyncId, resource) { + const cr = currentResource(); + if (cr) { + resource[sym] = cr[sym]; + } + } +}).enable(); + +const server = createServer(function(req, res) { + currentResource()[sym] = { state: req.url }; + setTimeout(function() { + res.end(JSON.stringify(currentResource()[sym])); + }, 100); +}).listen(3000); +``` + +`currentResource()` will return `null` during application bootstrap. + #### `async_hooks.executionAsyncId()` -* Returns: {Object} The resource that triggered the current - execution context. +* Returns: {Object} The resource representing the current execution. Useful to store data within the resource. ```js const { open } = require('fs'); const { executionAsyncId, executionAsyncResource } = require('async_hooks'); -console.log(executionAsyncId(), executionAsyncResource()); // 1 null +console.log(executionAsyncId(), executionAsyncResource()); // 1 {} open(__filename, 'r', (err, fd) => { console.log(executionAsyncId(), executionAsyncResource()); // 7 FSReqWrap }); @@ -512,6 +511,10 @@ Resource objects returned by `executionAsyncResource()` are often internal handle objects with undocumented APIs. Using any functions or properties on the object is not recommended and may crash your application. +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. + #### `async_hooks.executionAsyncId()`