-
Notifications
You must be signed in to change notification settings - Fork 30.4k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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. | ||||||||||||||||
|
||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
### napi_async_destroy | ||||||||||||||||
<!-- YAML | ||||||||||||||||
added: v8.6.0 | ||||||||||||||||
|
@@ -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. | ||||||||||||||||
|
@@ -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 | ||||||||||||||||
|
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> | ||
|
@@ -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 = | ||
|
@@ -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 | ||
|
@@ -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 */, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we perhaps use the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 */ There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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); | ||
} | ||
|
||
|
@@ -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); | ||
} | ||
|
||
|
@@ -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); | ||
|
@@ -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; | ||
|
||
|
@@ -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); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.