diff options
author | Matteo Collina <hello@matteocollina.com> | 2018-05-04 08:00:07 +0200 |
---|---|---|
committer | Anna Henningsen <anna@addaleax.net> | 2020-02-11 20:59:09 +0100 |
commit | 9fdb6e6aaf45b2364bac89a8f240772f49503ee6 (patch) | |
tree | 6ffbc990c3bdad00c598217cd6a63770a9d1cd3b | |
parent | 1c11ea43883256b6bc9e64a28bbc22f88c5c2b38 (diff) | |
download | node-new-9fdb6e6aaf45b2364bac89a8f240772f49503ee6.tar.gz |
async_hooks: add executionAsyncResource
Remove the need for the destroy hook in the basic APM case.
Co-authored-by: Stephen Belanger <admin@stephenbelanger.com>
PR-URL: https://github.com/nodejs/node/pull/30959
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
-rw-r--r-- | benchmark/async_hooks/async-resource-vs-destroy.js | 151 | ||||
-rw-r--r-- | doc/api/async_hooks.md | 56 | ||||
-rw-r--r-- | lib/async_hooks.js | 4 | ||||
-rw-r--r-- | lib/internal/async_hooks.js | 45 | ||||
-rw-r--r-- | lib/internal/process/task_queues.js | 2 | ||||
-rw-r--r-- | lib/internal/timers.js | 6 | ||||
-rw-r--r-- | src/api/callback.cc | 10 | ||||
-rw-r--r-- | src/async_wrap.cc | 38 | ||||
-rw-r--r-- | src/async_wrap.h | 6 | ||||
-rw-r--r-- | src/env-inl.h | 30 | ||||
-rw-r--r-- | src/env.h | 8 | ||||
-rw-r--r-- | src/node_internals.h | 6 | ||||
-rw-r--r-- | src/node_main_instance.cc | 5 | ||||
-rw-r--r-- | src/node_platform.cc | 6 | ||||
-rw-r--r-- | src/node_worker.cc | 5 | ||||
-rw-r--r-- | test/async-hooks/test-async-exec-resource-http.js | 30 | ||||
-rw-r--r-- | test/benchmark/test-benchmark-async-hooks.js | 4 | ||||
-rw-r--r-- | test/parallel/test-async-hooks-execution-async-resource-await.js | 54 | ||||
-rw-r--r-- | test/parallel/test-async-hooks-execution-async-resource.js | 49 |
19 files changed, 458 insertions, 57 deletions
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 0000000000..4464dd5f93 --- /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 66534ca850..51792e2dd1 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()` + +<!-- YAML +added: REPLACEME +--> + +* 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()` <!-- YAML diff --git a/lib/async_hooks.js b/lib/async_hooks.js index dadcf0f3df..923b9d6758 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -25,6 +25,7 @@ const { getHookArrays, enableHooks, disableHooks, + executionAsyncResource, // Internal Embedder API newAsyncId, getDefaultTriggerAsyncId, @@ -176,7 +177,7 @@ class AsyncResource { runInAsyncScope(fn, thisArg, ...args) { const asyncId = this[async_id_symbol]; - emitBefore(asyncId, this[trigger_async_id_symbol]); + emitBefore(asyncId, this[trigger_async_id_symbol], this); const ret = thisArg === undefined ? fn(...args) : @@ -211,6 +212,7 @@ module.exports = { createHook, executionAsyncId, triggerAsyncId, + executionAsyncResource, // Embedder API AsyncResource, }; diff --git a/lib/internal/async_hooks.js b/lib/internal/async_hooks.js index 02b0e91ac2..f7a7f7aad6 100644 --- a/lib/internal/async_hooks.js +++ b/lib/internal/async_hooks.js @@ -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 @@ -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') { @@ -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); @@ -342,7 +359,7 @@ function emitAfterScript(asyncId) { if (async_hook_fields[kAfter] > 0) emitAfterNative(asyncId); - popAsyncIds(asyncId); + popAsyncContext(asyncId); } @@ -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); } @@ -369,12 +387,13 @@ 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; @@ -382,18 +401,19 @@ function pushAsyncIds(asyncId, 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; } @@ -426,6 +446,7 @@ module.exports = { clearDefaultTriggerAsyncId, clearAsyncIdStack, hasAsyncIdStack, + executionAsyncResource, // Internal Embedder API newAsyncId, getOrSetAsyncId, diff --git a/lib/internal/process/task_queues.js b/lib/internal/process/task_queues.js index 8b2d2d808a..c07942587c 100644 --- a/lib/internal/process/task_queues.js +++ b/lib/internal/process/task_queues.js @@ -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; diff --git a/lib/internal/timers.js b/lib/internal/timers.js index b62ab9499c..bb80f57ee2 100644 --- a/lib/internal/timers.js +++ b/lib/internal/timers.js @@ -96,7 +96,7 @@ const { emitInit, emitBefore, emitAfter, - emitDestroy + emitDestroy, } = require('internal/async_hooks'); // Symbols for storing async id state. @@ -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; @@ -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) diff --git a/src/api/callback.cc b/src/api/callback.cc index 5e3fd65903..74a7836391 100644 --- a/src/api/callback.cc +++ b/src/api/callback.cc @@ -36,7 +36,7 @@ CallbackScope::~CallbackScope() { InternalCallbackScope::InternalCallbackScope(AsyncWrap* async_wrap, int flags) : InternalCallbackScope(async_wrap->env(), - async_wrap->object(), + async_wrap->GetResource(), { async_wrap->get_async_id(), async_wrap->get_trigger_async_id() }, flags) {} @@ -50,7 +50,6 @@ InternalCallbackScope::InternalCallbackScope(Environment* env, object_(object), skip_hooks_(flags & kSkipAsyncHooks), skip_task_queues_(flags & kSkipTaskQueues) { - CHECK_IMPLIES(!(flags & kAllowEmptyResource), !object.IsEmpty()); CHECK_NOT_NULL(env); env->PushAsyncCallbackScope(); @@ -69,8 +68,9 @@ InternalCallbackScope::InternalCallbackScope(Environment* env, AsyncWrap::EmitBefore(env, asyncContext.async_id); } - env->async_hooks()->push_async_ids(async_context_.async_id, - async_context_.trigger_async_id); + env->async_hooks()->push_async_context(async_context_.async_id, + async_context_.trigger_async_id, object); + pushed_ids_ = true; } @@ -89,7 +89,7 @@ void InternalCallbackScope::Close() { } if (pushed_ids_) - env_->async_hooks()->pop_async_id(async_context_.async_id); + env_->async_hooks()->pop_async_context(async_context_.async_id); if (failed_) return; diff --git a/src/async_wrap.cc b/src/async_wrap.cc index 58d89225e0..315d4177c8 100644 --- a/src/async_wrap.cc +++ b/src/async_wrap.cc @@ -260,8 +260,8 @@ static void PromiseHook(PromiseHookType type, Local<Promise> promise, if (wrap == nullptr) return; if (type == PromiseHookType::kBefore) { - env->async_hooks()->push_async_ids( - wrap->get_async_id(), wrap->get_trigger_async_id()); + env->async_hooks()->push_async_context(wrap->get_async_id(), + wrap->get_trigger_async_id(), wrap->object()); wrap->EmitTraceEventBefore(); AsyncWrap::EmitBefore(wrap->env(), wrap->get_async_id()); } else if (type == PromiseHookType::kAfter) { @@ -273,7 +273,7 @@ static void PromiseHook(PromiseHookType type, Local<Promise> promise, // Popping it off the stack can be skipped in that case, because it is // known that it would correspond to exactly one call with // PromiseHookType::kBefore that was not witnessed by the PromiseHook. - env->async_hooks()->pop_async_id(wrap->get_async_id()); + env->async_hooks()->pop_async_context(wrap->get_async_id()); } } else if (type == PromiseHookType::kResolve) { AsyncWrap::EmitPromiseResolve(wrap->env(), wrap->get_async_id()); @@ -382,7 +382,6 @@ static void RegisterDestroyHook(const FunctionCallbackInfo<Value>& args) { p->target.SetWeak(p, AsyncWrap::WeakCallback, WeakCallbackType::kParameter); } - void AsyncWrap::GetAsyncId(const FunctionCallbackInfo<Value>& args) { AsyncWrap* wrap; args.GetReturnValue().Set(kInvalidAsyncId); @@ -391,20 +390,20 @@ void AsyncWrap::GetAsyncId(const FunctionCallbackInfo<Value>& args) { } -void AsyncWrap::PushAsyncIds(const FunctionCallbackInfo<Value>& args) { +void AsyncWrap::PushAsyncContext(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); // No need for CHECK(IsNumber()) on args because if FromJust() doesn't fail // then the checks in push_async_ids() and pop_async_id() will. double async_id = args[0]->NumberValue(env->context()).FromJust(); double trigger_async_id = args[1]->NumberValue(env->context()).FromJust(); - env->async_hooks()->push_async_ids(async_id, trigger_async_id); + env->async_hooks()->push_async_context(async_id, trigger_async_id, args[2]); } -void AsyncWrap::PopAsyncIds(const FunctionCallbackInfo<Value>& args) { +void AsyncWrap::PopAsyncContext(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); double async_id = args[0]->NumberValue(env->context()).FromJust(); - args.GetReturnValue().Set(env->async_hooks()->pop_async_id(async_id)); + args.GetReturnValue().Set(env->async_hooks()->pop_async_context(async_id)); } @@ -433,6 +432,7 @@ void AsyncWrap::EmitDestroy() { AsyncWrap::EmitDestroy(env(), async_id_); // Ensure no double destroy is emitted via AsyncReset(). async_id_ = kInvalidAsyncId; + resource_.Reset(); } void AsyncWrap::QueueDestroyAsyncId(const FunctionCallbackInfo<Value>& args) { @@ -464,8 +464,8 @@ void AsyncWrap::Initialize(Local<Object> target, HandleScope scope(isolate); env->SetMethod(target, "setupHooks", SetupHooks); - env->SetMethod(target, "pushAsyncIds", PushAsyncIds); - env->SetMethod(target, "popAsyncIds", PopAsyncIds); + env->SetMethod(target, "pushAsyncContext", PushAsyncContext); + env->SetMethod(target, "popAsyncContext", PopAsyncContext); env->SetMethod(target, "queueDestroyAsyncId", QueueDestroyAsyncId); env->SetMethod(target, "enablePromiseHook", EnablePromiseHook); env->SetMethod(target, "disablePromiseHook", DisablePromiseHook); @@ -502,6 +502,10 @@ void AsyncWrap::Initialize(Local<Object> target, "async_id_fields", env->async_hooks()->async_id_fields().GetJSArray()); + FORCE_SET_TARGET_FIELD(target, + "execution_async_resources", + env->async_hooks()->execution_async_resources()); + target->Set(context, env->async_ids_stack_string(), env->async_hooks()->async_ids_stack().GetJSArray()).Check(); @@ -670,6 +674,12 @@ void AsyncWrap::AsyncReset(Local<Object> resource, double execution_async_id, : execution_async_id; trigger_async_id_ = env()->get_default_trigger_async_id(); + if (resource != object()) { + // TODO(addaleax): Using a strong reference here makes it very easy to + // introduce memory leaks. Move away from using a strong reference. + resource_.Reset(env()->isolate(), resource); + } + switch (provider_type()) { #define V(PROVIDER) \ case PROVIDER_ ## PROVIDER: \ @@ -776,6 +786,14 @@ Local<Object> AsyncWrap::GetOwner(Environment* env, Local<Object> obj) { } } +Local<Object> AsyncWrap::GetResource() { + if (resource_.IsEmpty()) { + return object(); + } + + return resource_.Get(env()->isolate()); +} + } // namespace node NODE_MODULE_CONTEXT_AWARE_INTERNAL(async_wrap, node::AsyncWrap::Initialize) diff --git a/src/async_wrap.h b/src/async_wrap.h index 58d6ca84db..33bed41a64 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -134,8 +134,8 @@ class AsyncWrap : public BaseObject { void* priv); static void GetAsyncId(const v8::FunctionCallbackInfo<v8::Value>& args); - static void PushAsyncIds(const v8::FunctionCallbackInfo<v8::Value>& args); - static void PopAsyncIds(const v8::FunctionCallbackInfo<v8::Value>& args); + static void PushAsyncContext(const v8::FunctionCallbackInfo<v8::Value>& args); + static void PopAsyncContext(const v8::FunctionCallbackInfo<v8::Value>& args); static void AsyncReset(const v8::FunctionCallbackInfo<v8::Value>& args); static void GetProviderType(const v8::FunctionCallbackInfo<v8::Value>& args); static void QueueDestroyAsyncId( @@ -202,6 +202,7 @@ class AsyncWrap : public BaseObject { v8::Local<v8::Object> obj); bool IsDoneInitializing() const override; + v8::Local<v8::Object> GetResource(); private: friend class PromiseWrap; @@ -216,6 +217,7 @@ class AsyncWrap : public BaseObject { // Because the values may be Reset(), cannot be made const. double async_id_ = kInvalidAsyncId; double trigger_async_id_; + v8::Global<v8::Object> resource_; }; } // namespace node diff --git a/src/env-inl.h b/src/env-inl.h index 3c7b83795d..71f0ff7f6d 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -68,7 +68,7 @@ inline AsyncHooks::AsyncHooks() : async_ids_stack_(env()->isolate(), 16 * 2), fields_(env()->isolate(), kFieldsCount), async_id_fields_(env()->isolate(), kUidFieldsCount) { - v8::HandleScope handle_scope(env()->isolate()); + clear_async_id_stack(); // Always perform async_hooks checks, not just when async_hooks is enabled. // TODO(AndreasMadsen): Consider removing this for LTS releases. @@ -113,6 +113,10 @@ inline AliasedFloat64Array& AsyncHooks::async_ids_stack() { return async_ids_stack_; } +inline v8::Local<v8::Array> AsyncHooks::execution_async_resources() { + return PersistentToLocal::Strong(execution_async_resources_); +} + inline v8::Local<v8::String> AsyncHooks::provider_string(int idx) { return providers_[idx].Get(env()->isolate()); } @@ -125,9 +129,12 @@ inline Environment* AsyncHooks::env() { return Environment::ForAsyncHooks(this); } -// Remember to keep this code aligned with pushAsyncIds() in JS. -inline void AsyncHooks::push_async_ids(double async_id, - double trigger_async_id) { +// Remember to keep this code aligned with pushAsyncContext() in JS. +inline void AsyncHooks::push_async_context(double async_id, + double trigger_async_id, + v8::Local<v8::Value> resource) { + v8::HandleScope handle_scope(env()->isolate()); + // Since async_hooks is experimental, do only perform the check // when async_hooks is enabled. if (fields_[kCheck] > 0) { @@ -143,10 +150,13 @@ inline void AsyncHooks::push_async_ids(double async_id, fields_[kStackLength] += 1; async_id_fields_[kExecutionAsyncId] = async_id; async_id_fields_[kTriggerAsyncId] = trigger_async_id; + + auto resources = execution_async_resources(); + USE(resources->Set(env()->context(), offset, resource)); } -// Remember to keep this code aligned with popAsyncIds() in JS. -inline bool AsyncHooks::pop_async_id(double async_id) { +// Remember to keep this code aligned with popAsyncContext() in JS. +inline bool AsyncHooks::pop_async_context(double async_id) { // In case of an exception then this may have already been reset, if the // stack was multiple MakeCallback()'s deep. if (fields_[kStackLength] == 0) return false; @@ -175,11 +185,18 @@ inline bool AsyncHooks::pop_async_id(double async_id) { async_id_fields_[kTriggerAsyncId] = async_ids_stack_[2 * offset + 1]; fields_[kStackLength] = offset; + auto resources = execution_async_resources(); + USE(resources->Delete(env()->context(), offset)); + return fields_[kStackLength] > 0; } // Keep in sync with clearAsyncIdStack in lib/internal/async_hooks.js. inline void AsyncHooks::clear_async_id_stack() { + auto isolate = env()->isolate(); + v8::HandleScope handle_scope(isolate); + execution_async_resources_.Reset(isolate, v8::Array::New(isolate)); + async_id_fields_[kExecutionAsyncId] = 0; async_id_fields_[kTriggerAsyncId] = 0; fields_[kStackLength] = 0; @@ -206,7 +223,6 @@ inline AsyncHooks::DefaultTriggerAsyncIdScope ::~DefaultTriggerAsyncIdScope() { old_default_trigger_async_id_; } - Environment* Environment::ForAsyncHooks(AsyncHooks* hooks) { return ContainerOf(&Environment::async_hooks_, hooks); } @@ -655,14 +655,16 @@ class AsyncHooks : public MemoryRetainer { inline AliasedUint32Array& fields(); inline AliasedFloat64Array& async_id_fields(); inline AliasedFloat64Array& async_ids_stack(); + inline v8::Local<v8::Array> execution_async_resources(); inline v8::Local<v8::String> provider_string(int idx); inline void no_force_checks(); inline Environment* env(); - inline void push_async_ids(double async_id, double trigger_async_id); - inline bool pop_async_id(double async_id); + inline void push_async_context(double async_id, double trigger_async_id, + v8::Local<v8::Value> execution_async_resource_); + inline bool pop_async_context(double async_id); inline void clear_async_id_stack(); // Used in fatal exceptions. AsyncHooks(const AsyncHooks&) = delete; @@ -707,6 +709,8 @@ class AsyncHooks : public MemoryRetainer { AliasedFloat64Array async_id_fields_; void grow_async_ids_stack(); + + v8::Global<v8::Array> execution_async_resources_; }; class ImmediateInfo : public MemoryRetainer { diff --git a/src/node_internals.h b/src/node_internals.h index 64b4d489e1..8d63f023c1 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -208,15 +208,13 @@ class InternalCallbackScope { public: enum Flags { kNoFlags = 0, - // Tell the constructor whether its `object` parameter may be empty or not. - kAllowEmptyResource = 1, // Indicates whether 'before' and 'after' hooks should be skipped. - kSkipAsyncHooks = 2, + kSkipAsyncHooks = 1, // Indicates whether nextTick and microtask queues should be skipped. // This should only be used when there is no call into JS in this scope. // (The HTTP parser also uses it for some weird backwards // compatibility issues, but it shouldn't.) - kSkipTaskQueues = 4 + kSkipTaskQueues = 2 }; InternalCallbackScope(Environment* env, v8::Local<v8::Object> object, diff --git a/src/node_main_instance.cc b/src/node_main_instance.cc index 1453e1ea6a..ad6966cf4f 100644 --- a/src/node_main_instance.cc +++ b/src/node_main_instance.cc @@ -121,10 +121,9 @@ int NodeMainInstance::Run() { { InternalCallbackScope callback_scope( env.get(), - Local<Object>(), + Object::New(isolate_), { 1, 0 }, - InternalCallbackScope::kAllowEmptyResource | - InternalCallbackScope::kSkipAsyncHooks); + InternalCallbackScope::kSkipAsyncHooks); LoadEnvironment(env.get()); } diff --git a/src/node_platform.cc b/src/node_platform.cc index b30f907a02..380a26ecb4 100644 --- a/src/node_platform.cc +++ b/src/node_platform.cc @@ -10,7 +10,6 @@ namespace node { using v8::Isolate; -using v8::Local; using v8::Object; using v8::Platform; using v8::Task; @@ -388,8 +387,9 @@ void PerIsolatePlatformData::RunForegroundTask(std::unique_ptr<Task> task) { DebugSealHandleScope scope(isolate); Environment* env = Environment::GetCurrent(isolate); if (env != nullptr) { - InternalCallbackScope cb_scope(env, Local<Object>(), { 0, 0 }, - InternalCallbackScope::kAllowEmptyResource); + v8::HandleScope scope(isolate); + InternalCallbackScope cb_scope(env, Object::New(isolate), { 0, 0 }, + InternalCallbackScope::kNoFlags); task->Run(); } else { task->Run(); diff --git a/src/node_worker.cc b/src/node_worker.cc index e4c5cd42ab..f0e5f5bc11 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -326,10 +326,9 @@ void Worker::Run() { HandleScope handle_scope(isolate_); InternalCallbackScope callback_scope( env_.get(), - Local<Object>(), + Object::New(isolate_), { 1, 0 }, - InternalCallbackScope::kAllowEmptyResource | - InternalCallbackScope::kSkipAsyncHooks); + InternalCallbackScope::kSkipAsyncHooks); if (!env_->RunBootstrapping().IsEmpty()) { CreateEnvMessagePort(env_.get()); diff --git a/test/async-hooks/test-async-exec-resource-http.js b/test/async-hooks/test-async-exec-resource-http.js new file mode 100644 index 0000000000..ecc654bea0 --- /dev/null +++ b/test/async-hooks/test-async-exec-resource-http.js @@ -0,0 +1,30 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { + executionAsyncResource, + executionAsyncId, + createHook, +} = require('async_hooks'); +const http = require('http'); + +const hooked = {}; +createHook({ + init(asyncId, type, triggerAsyncId, resource) { + hooked[asyncId] = resource; + } +}).enable(); + +const server = http.createServer((req, res) => { + res.end('ok'); +}); + +server.listen(0, () => { + assert.strictEqual(executionAsyncResource(), hooked[executionAsyncId()]); + + http.get({ port: server.address().port }, () => { + assert.strictEqual(executionAsyncResource(), hooked[executionAsyncId()]); + server.close(); + }); +}); diff --git a/test/benchmark/test-benchmark-async-hooks.js b/test/benchmark/test-benchmark-async-hooks.js index 9951d8c933..662a6a07f3 100644 --- a/test/benchmark/test-benchmark-async-hooks.js +++ b/test/benchmark/test-benchmark-async-hooks.js @@ -15,6 +15,8 @@ runBenchmark('async_hooks', 'asyncHooks=all', 'connections=50', 'method=trackingDisabled', - 'n=10' + 'n=10', + 'type=async-resource', + 'asyncMethod=async' ], {}); diff --git a/test/parallel/test-async-hooks-execution-async-resource-await.js b/test/parallel/test-async-hooks-execution-async-resource-await.js new file mode 100644 index 0000000000..8dc04c83d7 --- /dev/null +++ b/test/parallel/test-async-hooks-execution-async-resource-await.js @@ -0,0 +1,54 @@ +'use strict'; + +const common = require('../common'); +const sleep = require('util').promisify(setTimeout); +const assert = require('assert'); +const { executionAsyncResource, createHook } = require('async_hooks'); +const { createServer, get } = require('http'); +const sym = Symbol('cls'); + +// Tests continuation local storage with the currentResource API +// through an async function + +assert.ok(executionAsyncResource()); + +createHook({ + init(asyncId, type, triggerAsyncId, resource) { + const cr = executionAsyncResource(); + resource[sym] = cr[sym]; + } +}).enable(); + +async function handler(req, res) { + executionAsyncResource()[sym] = { state: req.url }; + await sleep(10); + const { state } = executionAsyncResource()[sym]; + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ state })); +} + +const server = createServer(function(req, res) { + handler(req, res); +}); + +function test(n) { + get(`http://localhost:${server.address().port}/${n}`, common.mustCall(function(res) { + res.setEncoding('utf8'); + + let body = ''; + res.on('data', function(chunk) { + body += chunk; + }); + + res.on('end', common.mustCall(function() { + assert.deepStrictEqual(JSON.parse(body), { state: `/${n}` }); + })); + })); +} + +server.listen(0, common.mustCall(function() { + server.unref(); + for (let i = 0; i < 10; i++) { + test(i); + } +})); diff --git a/test/parallel/test-async-hooks-execution-async-resource.js b/test/parallel/test-async-hooks-execution-async-resource.js new file mode 100644 index 0000000000..b3a9f76ef4 --- /dev/null +++ b/test/parallel/test-async-hooks-execution-async-resource.js @@ -0,0 +1,49 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { executionAsyncResource, createHook } = require('async_hooks'); +const { createServer, get } = require('http'); +const sym = Symbol('cls'); + +// Tests continuation local storage with the executionAsyncResource API + +assert.ok(executionAsyncResource()); + +createHook({ + init(asyncId, type, triggerAsyncId, resource) { + const cr = executionAsyncResource(); + resource[sym] = cr[sym]; + } +}).enable(); + +const server = createServer(function(req, res) { + executionAsyncResource()[sym] = { state: req.url }; + setTimeout(function() { + const { state } = executionAsyncResource()[sym]; + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ state })); + }, 10); +}); + +function test(n) { + get(`http://localhost:${server.address().port}/${n}`, common.mustCall(function(res) { + res.setEncoding('utf8'); + + let body = ''; + res.on('data', function(chunk) { + body += chunk; + }); + + res.on('end', common.mustCall(function() { + assert.deepStrictEqual(JSON.parse(body), { state: `/${n}` }); + })); + })); +} + +server.listen(0, common.mustCall(function() { + server.unref(); + for (let i = 0; i < 10; i++) { + test(i); + } +})); |