summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatteo Collina <hello@matteocollina.com>2018-05-04 08:00:07 +0200
committerAnna Henningsen <anna@addaleax.net>2020-02-11 20:59:09 +0100
commit9fdb6e6aaf45b2364bac89a8f240772f49503ee6 (patch)
tree6ffbc990c3bdad00c598217cd6a63770a9d1cd3b
parent1c11ea43883256b6bc9e64a28bbc22f88c5c2b38 (diff)
downloadnode-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.js151
-rw-r--r--doc/api/async_hooks.md56
-rw-r--r--lib/async_hooks.js4
-rw-r--r--lib/internal/async_hooks.js45
-rw-r--r--lib/internal/process/task_queues.js2
-rw-r--r--lib/internal/timers.js6
-rw-r--r--src/api/callback.cc10
-rw-r--r--src/async_wrap.cc38
-rw-r--r--src/async_wrap.h6
-rw-r--r--src/env-inl.h30
-rw-r--r--src/env.h8
-rw-r--r--src/node_internals.h6
-rw-r--r--src/node_main_instance.cc5
-rw-r--r--src/node_platform.cc6
-rw-r--r--src/node_worker.cc5
-rw-r--r--test/async-hooks/test-async-exec-resource-http.js30
-rw-r--r--test/benchmark/test-benchmark-async-hooks.js4
-rw-r--r--test/parallel/test-async-hooks-execution-async-resource-await.js54
-rw-r--r--test/parallel/test-async-hooks-execution-async-resource.js49
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);
}
diff --git a/src/env.h b/src/env.h
index b1ec84bd75..e100802ee7 100644
--- a/src/env.h
+++ b/src/env.h
@@ -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);
+ }
+}));