Skip to content

Commit

Permalink
n-api: napi_make_callback emit async init with resource of async_context
Browse files Browse the repository at this point in the history
instead of emit async init with receiver of the callback.

PR-URL: #32930
Fixes: #32898
Reviewed-By: Stephen Belanger <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
Reviewed-By: Gerhard Stöbich <[email protected]>
  • Loading branch information
legendecas authored and targos committed Nov 3, 2020
1 parent 7691b67 commit d16e2fa
Show file tree
Hide file tree
Showing 10 changed files with 465 additions and 98 deletions.
36 changes: 26 additions & 10 deletions doc/api/n-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5189,19 +5189,31 @@ napi_status napi_async_init(napi_env env,

* `[in] env`: The environment that the API is invoked under.
* `[in] async_resource`: Object associated with the async work
that will be passed to possible `async_hooks` [`init` hooks][].
In order to retain ABI compatibility with previous versions,
passing `NULL` for `async_resource` does not result in an error. However,
this results in incorrect operation of async hooks for the
napi_async_context created. Potential issues include
loss of async context when using the AsyncLocalStorage API.
* `[in] async_resource_name`: Identifier for the kind of resource
that is being provided for diagnostic information exposed by the
`async_hooks` API.
that will be passed to possible `async_hooks` [`init` hooks][] and can be
accessed by [`async_hooks.executionAsyncResource()`][].
* `[in] async_resource_name`: Identifier for the kind of resource that is being
provided for diagnostic information exposed by the `async_hooks` API.
* `[out] result`: The initialized async context.

Returns `napi_ok` if the API succeeded.

The `async_resource` object needs to be kept alive until
[`napi_async_destroy`][] to keep `async_hooks` related API acts correctly. In
order to retain ABI compatibility with previous versions, `napi_async_context`s
are not maintaining the strong reference to the `async_resource` objects to
avoid introducing causing memory leaks. However, if the `async_resource` is
garbage collected by JavaScript engine before the `napi_async_context` was
destroyed by `napi_async_destroy`, calling `napi_async_context` related APIs
like [`napi_open_callback_scope`][] and [`napi_make_callback`][] can cause
problems like loss of async context when using the `AsyncLocalStoage` API.

In order to retain ABI compatibility with previous versions, passing `NULL`
for `async_resource` does not result in an error. However, this is not
recommended as this will result poor results with `async_hooks`
[`init` hooks][] and `async_hooks.executionAsyncResource()` as the resource is
now required by the underlying `async_hooks` implementation in order to provide
the linkage between async callbacks.

### napi_async_destroy
<!-- YAML
added: v8.6.0
Expand Down Expand Up @@ -5288,7 +5300,9 @@ NAPI_EXTERN napi_status napi_open_callback_scope(napi_env env,

* `[in] env`: The environment that the API is invoked under.
* `[in] resource_object`: An object associated with the async work
that will be passed to possible `async_hooks` [`init` hooks][].
that will be passed to possible `async_hooks` [`init` hooks][]. This
parameter has been deprecated and is ignored at runtime. Use the
`async_resource` parameter in [`napi_async_init`][] instead.
* `[in] context`: Context for the async operation that is invoking the callback.
This should be a value previously obtained from [`napi_async_init`][].
* `[out] result`: The newly created scope.
Expand Down Expand Up @@ -5985,13 +5999,15 @@ This API may only be called from the main thread.
[`Number.MAX_SAFE_INTEGER`]: https://tc39.github.io/ecma262/#sec-number.max_safe_integer
[`Number.MIN_SAFE_INTEGER`]: https://tc39.github.io/ecma262/#sec-number.min_safe_integer
[`Worker`]: worker_threads.md#worker_threads_class_worker
[`async_hooks.executionAsyncResource()`]: async_hooks.md#async_hooks_async_hooks_executionasyncresource
[`global`]: globals.md#globals_global
[`init` hooks]: async_hooks.md#async_hooks_init_asyncid_type_triggerasyncid_resource
[`napi_add_async_cleanup_hook`]: #n_api_napi_add_async_cleanup_hook
[`napi_add_env_cleanup_hook`]: #n_api_napi_add_env_cleanup_hook
[`napi_add_finalizer`]: #n_api_napi_add_finalizer
[`napi_async_cleanup_hook`]: #n_api_napi_async_cleanup_hook
[`napi_async_complete_callback`]: #n_api_napi_async_complete_callback
[`napi_async_destroy`]: #n_api_napi_async_destroy
[`napi_async_init`]: #n_api_napi_async_init
[`napi_callback`]: #n_api_napi_callback
[`napi_cancel_async_work`]: #n_api_napi_cancel_async_work
Expand Down
187 changes: 143 additions & 44 deletions src/node_api.cc
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
#include "async_wrap-inl.h"
#include "env-inl.h"
#define NAPI_EXPERIMENTAL
#include "js_native_api_v8.h"
#include "memory_tracker-inl.h"
#include "node_api.h"
#include "node_binding.h"
#include "node_buffer.h"
#include "node_errors.h"
#include "node_internals.h"
#include "threadpoolwork-inl.h"
#include "tracing/traced_value.h"
#include "util-inl.h"

#include <memory>
Expand Down Expand Up @@ -104,16 +107,6 @@ static inline napi_env NewEnv(v8::Local<v8::Context> context) {
return result;
}

static inline napi_callback_scope
JsCallbackScopeFromV8CallbackScope(node::CallbackScope* s) {
return reinterpret_cast<napi_callback_scope>(s);
}

static inline node::CallbackScope*
V8CallbackScopeFromJsCallbackScope(napi_callback_scope s) {
return reinterpret_cast<node::CallbackScope*>(s);
}

static inline void trigger_fatal_exception(
napi_env env, v8::Local<v8::Value> local_err) {
v8::Local<v8::Message> local_msg =
Expand Down Expand Up @@ -435,6 +428,111 @@ class ThreadSafeFunction : public node::AsyncResource {
bool handles_closing;
};

/**
* Compared to node::AsyncResource, the resource object in AsyncContext is
* gc-able. AsyncContext holds a weak reference to the resource object.
* AsyncContext::MakeCallback doesn't implicitly set the receiver of the
* callback to the resource object.
*/
class AsyncContext {
public:
AsyncContext(node_napi_env env,
v8::Local<v8::Object> resource_object,
const v8::Local<v8::String> resource_name,
bool externally_managed_resource)
: env_(env) {
async_id_ = node_env()->new_async_id();
trigger_async_id_ = node_env()->get_default_trigger_async_id();
resource_.Reset(node_env()->isolate(), resource_object);
lost_reference_ = false;
if (externally_managed_resource) {
resource_.SetWeak(
this, AsyncContext::WeakCallback, v8::WeakCallbackType::kParameter);
}

node::AsyncWrap::EmitAsyncInit(node_env(),
resource_object,
resource_name,
async_id_,
trigger_async_id_);
}

~AsyncContext() {
resource_.Reset();
lost_reference_ = true;
node::AsyncWrap::EmitDestroy(node_env(), async_id_);
}

inline v8::MaybeLocal<v8::Value> MakeCallback(
v8::Local<v8::Object> recv,
const v8::Local<v8::Function> callback,
int argc,
v8::Local<v8::Value> argv[]) {
EnsureReference();
return node::InternalMakeCallback(node_env(),
resource(),
recv,
callback,
argc,
argv,
{async_id_, trigger_async_id_});
}

inline napi_callback_scope OpenCallbackScope() {
EnsureReference();
napi_callback_scope it =
reinterpret_cast<napi_callback_scope>(new CallbackScope(this));
env_->open_callback_scopes++;
return it;
}

inline void EnsureReference() {
if (lost_reference_) {
const v8::HandleScope handle_scope(node_env()->isolate());
resource_.Reset(node_env()->isolate(),
v8::Object::New(node_env()->isolate()));
lost_reference_ = false;
}
}

inline node::Environment* node_env() { return env_->node_env(); }
inline v8::Local<v8::Object> resource() {
return resource_.Get(node_env()->isolate());
}
inline node::async_context async_context() {
return {async_id_, trigger_async_id_};
}

static inline void CloseCallbackScope(node_napi_env env,
napi_callback_scope s) {
CallbackScope* callback_scope = reinterpret_cast<CallbackScope*>(s);
delete callback_scope;
env->open_callback_scopes--;
}

static void WeakCallback(const v8::WeakCallbackInfo<AsyncContext>& data) {
AsyncContext* async_context = data.GetParameter();
async_context->resource_.Reset();
async_context->lost_reference_ = true;
}

private:
class CallbackScope : public node::CallbackScope {
public:
explicit CallbackScope(AsyncContext* async_context)
: node::CallbackScope(async_context->node_env()->isolate(),
async_context->resource_.Get(
async_context->node_env()->isolate()),
async_context->async_context()) {}
};

node_napi_env env_;
double async_id_;
double trigger_async_id_;
v8::Global<v8::Object> resource_;
bool lost_reference_;
};

} // end of anonymous namespace

} // end of namespace v8impl
Expand Down Expand Up @@ -627,28 +725,19 @@ NAPI_NO_RETURN void napi_fatal_error(const char* location,
}

napi_status napi_open_callback_scope(napi_env env,
napi_value resource_object,
napi_value /** ignored */,
napi_async_context async_context_handle,
napi_callback_scope* result) {
// Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw
// JS exceptions.
CHECK_ENV(env);
CHECK_ARG(env, result);

v8::Local<v8::Context> context = env->context();

node::async_context* node_async_context =
reinterpret_cast<node::async_context*>(async_context_handle);

v8::Local<v8::Object> resource;
CHECK_TO_OBJECT(env, context, resource, resource_object);
v8impl::AsyncContext* node_async_context =
reinterpret_cast<v8impl::AsyncContext*>(async_context_handle);

*result = v8impl::JsCallbackScopeFromV8CallbackScope(
new node::CallbackScope(env->isolate,
resource,
*node_async_context));
*result = node_async_context->OpenCallbackScope();

env->open_callback_scopes++;
return napi_clear_last_error(env);
}

Expand All @@ -661,8 +750,9 @@ napi_status napi_close_callback_scope(napi_env env, napi_callback_scope scope) {
return napi_callback_scope_mismatch;
}

env->open_callback_scopes--;
delete v8impl::V8CallbackScopeFromJsCallbackScope(scope);
v8impl::AsyncContext::CloseCallbackScope(reinterpret_cast<node_napi_env>(env),
scope);

return napi_clear_last_error(env);
}

Expand All @@ -678,20 +768,24 @@ napi_status napi_async_init(napi_env env,
v8::Local<v8::Context> context = env->context();

v8::Local<v8::Object> v8_resource;
bool externally_managed_resource;
if (async_resource != nullptr) {
CHECK_TO_OBJECT(env, context, v8_resource, async_resource);
externally_managed_resource = true;
} else {
v8_resource = v8::Object::New(isolate);
externally_managed_resource = false;
}

v8::Local<v8::String> v8_resource_name;
CHECK_TO_STRING(env, context, v8_resource_name, async_resource_name);

// TODO(jasongin): Consider avoiding allocation here by using
// a tagged pointer with 2×31 bit fields instead.
node::async_context* async_context = new node::async_context();
auto async_context =
new v8impl::AsyncContext(reinterpret_cast<node_napi_env>(env),
v8_resource,
v8_resource_name,
externally_managed_resource);

*async_context = node::EmitAsyncInit(isolate, v8_resource, v8_resource_name);
*result = reinterpret_cast<napi_async_context>(async_context);

return napi_clear_last_error(env);
Expand All @@ -702,11 +796,8 @@ napi_status napi_async_destroy(napi_env env,
CHECK_ENV(env);
CHECK_ARG(env, async_context);

node::async_context* node_async_context =
reinterpret_cast<node::async_context*>(async_context);
node::EmitAsyncDestroy(
reinterpret_cast<node_napi_env>(env)->node_env(),
*node_async_context);
v8impl::AsyncContext* node_async_context =
reinterpret_cast<v8impl::AsyncContext*>(async_context);

delete node_async_context;

Expand Down Expand Up @@ -734,17 +825,25 @@ napi_status napi_make_callback(napi_env env,
v8::Local<v8::Function> v8func;
CHECK_TO_FUNCTION(env, v8func, func);

node::async_context* node_async_context =
reinterpret_cast<node::async_context*>(async_context);
if (node_async_context == nullptr) {
static node::async_context empty_context = { 0, 0 };
node_async_context = &empty_context;
}
v8::MaybeLocal<v8::Value> callback_result;

v8::MaybeLocal<v8::Value> callback_result = node::MakeCallback(
env->isolate, v8recv, v8func, argc,
reinterpret_cast<v8::Local<v8::Value>*>(const_cast<napi_value*>(argv)),
*node_async_context);
if (async_context == nullptr) {
callback_result = node::MakeCallback(
env->isolate,
v8recv,
v8func,
argc,
reinterpret_cast<v8::Local<v8::Value>*>(const_cast<napi_value*>(argv)),
{0, 0});
} else {
auto node_async_context =
reinterpret_cast<v8impl::AsyncContext*>(async_context);
callback_result = node_async_context->MakeCallback(
v8recv,
v8func,
argc,
reinterpret_cast<v8::Local<v8::Value>*>(const_cast<napi_value*>(argv)));
}

if (try_catch.HasCaught()) {
return napi_set_last_error(env, napi_pending_exception);
Expand Down
Loading

0 comments on commit d16e2fa

Please sign in to comment.