Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

n-api: napi_make_callback emit async init event with resource of async_context #32930

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 26 additions & 10 deletions doc/api/n-api.md
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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 calls.

### 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
@@ -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_;
};
legendecas marked this conversation as resolved.
Show resolved Hide resolved

} // 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 */,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we perhaps use the resource_object to warn if it's different than the object contained in the AsyncContext?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@quad I see what you mean, but that might be a breaking change and we don't have breaking changes for N-API methods. If we felt strongly about it we should add a new method without the parameter and doc deprecate the existing method (but not remove of course). I think we did something similar for another API in terms of indicate /** ignored */

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just thinking a log message warning, no change in behaviour. Is that still considered breaking? Either way, not blocking on that. Just a thought that it'd be nice to have some indicator when the API is used improperly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my knowledge node has no logger. Logging to stdout/stderr tends to break command line tools.

But I think we should find a way how to deprecate something in NAPI in a way more visible to users like comments in doc.
I think a compile time warning like it is used for node::MakeCallback would be great.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a compile time warning like it is used for node::MakeCallback would be great.

AFAICT, it is not possible to emit compile-time warning without changing the API shapes. i.e. the signature of node::MakeCallback was migrated to the one with an additional parameter async_context. Though in the case of n_api, the deprecation is the parameter in the middle of the parameter list. So a new n-api function without the parameter will be required and the one existing can be deprecated.

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