Skip to content

Commit d16e2fa

Browse files
legendecastargos
authored andcommitted
n-api: napi_make_callback emit async init with resource of async_context
instead of emit async init with receiver of the callback. PR-URL: #32930 Fixes: #32898 Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
1 parent 7691b67 commit d16e2fa

File tree

10 files changed

+465
-98
lines changed

10 files changed

+465
-98
lines changed

doc/api/n-api.md

+26-10
Original file line numberDiff line numberDiff line change
@@ -5189,19 +5189,31 @@ napi_status napi_async_init(napi_env env,
51895189

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

52035198
Returns `napi_ok` if the API succeeded.
52045199

5200+
The `async_resource` object needs to be kept alive until
5201+
[`napi_async_destroy`][] to keep `async_hooks` related API acts correctly. In
5202+
order to retain ABI compatibility with previous versions, `napi_async_context`s
5203+
are not maintaining the strong reference to the `async_resource` objects to
5204+
avoid introducing causing memory leaks. However, if the `async_resource` is
5205+
garbage collected by JavaScript engine before the `napi_async_context` was
5206+
destroyed by `napi_async_destroy`, calling `napi_async_context` related APIs
5207+
like [`napi_open_callback_scope`][] and [`napi_make_callback`][] can cause
5208+
problems like loss of async context when using the `AsyncLocalStoage` API.
5209+
5210+
In order to retain ABI compatibility with previous versions, passing `NULL`
5211+
for `async_resource` does not result in an error. However, this is not
5212+
recommended as this will result poor results with `async_hooks`
5213+
[`init` hooks][] and `async_hooks.executionAsyncResource()` as the resource is
5214+
now required by the underlying `async_hooks` implementation in order to provide
5215+
the linkage between async callbacks.
5216+
52055217
### napi_async_destroy
52065218
<!-- YAML
52075219
added: v8.6.0
@@ -5288,7 +5300,9 @@ NAPI_EXTERN napi_status napi_open_callback_scope(napi_env env,
52885300

52895301
* `[in] env`: The environment that the API is invoked under.
52905302
* `[in] resource_object`: An object associated with the async work
5291-
that will be passed to possible `async_hooks` [`init` hooks][].
5303+
that will be passed to possible `async_hooks` [`init` hooks][]. This
5304+
parameter has been deprecated and is ignored at runtime. Use the
5305+
`async_resource` parameter in [`napi_async_init`][] instead.
52925306
* `[in] context`: Context for the async operation that is invoking the callback.
52935307
This should be a value previously obtained from [`napi_async_init`][].
52945308
* `[out] result`: The newly created scope.
@@ -5985,13 +5999,15 @@ This API may only be called from the main thread.
59855999
[`Number.MAX_SAFE_INTEGER`]: https://tc39.github.io/ecma262/#sec-number.max_safe_integer
59866000
[`Number.MIN_SAFE_INTEGER`]: https://tc39.github.io/ecma262/#sec-number.min_safe_integer
59876001
[`Worker`]: worker_threads.md#worker_threads_class_worker
6002+
[`async_hooks.executionAsyncResource()`]: async_hooks.md#async_hooks_async_hooks_executionasyncresource
59886003
[`global`]: globals.md#globals_global
59896004
[`init` hooks]: async_hooks.md#async_hooks_init_asyncid_type_triggerasyncid_resource
59906005
[`napi_add_async_cleanup_hook`]: #n_api_napi_add_async_cleanup_hook
59916006
[`napi_add_env_cleanup_hook`]: #n_api_napi_add_env_cleanup_hook
59926007
[`napi_add_finalizer`]: #n_api_napi_add_finalizer
59936008
[`napi_async_cleanup_hook`]: #n_api_napi_async_cleanup_hook
59946009
[`napi_async_complete_callback`]: #n_api_napi_async_complete_callback
6010+
[`napi_async_destroy`]: #n_api_napi_async_destroy
59956011
[`napi_async_init`]: #n_api_napi_async_init
59966012
[`napi_callback`]: #n_api_napi_callback
59976013
[`napi_cancel_async_work`]: #n_api_napi_cancel_async_work

src/node_api.cc

+143-44
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
#include "async_wrap-inl.h"
12
#include "env-inl.h"
23
#define NAPI_EXPERIMENTAL
34
#include "js_native_api_v8.h"
5+
#include "memory_tracker-inl.h"
46
#include "node_api.h"
57
#include "node_binding.h"
68
#include "node_buffer.h"
79
#include "node_errors.h"
810
#include "node_internals.h"
911
#include "threadpoolwork-inl.h"
12+
#include "tracing/traced_value.h"
1013
#include "util-inl.h"
1114

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

107-
static inline napi_callback_scope
108-
JsCallbackScopeFromV8CallbackScope(node::CallbackScope* s) {
109-
return reinterpret_cast<napi_callback_scope>(s);
110-
}
111-
112-
static inline node::CallbackScope*
113-
V8CallbackScopeFromJsCallbackScope(napi_callback_scope s) {
114-
return reinterpret_cast<node::CallbackScope*>(s);
115-
}
116-
117110
static inline void trigger_fatal_exception(
118111
napi_env env, v8::Local<v8::Value> local_err) {
119112
v8::Local<v8::Message> local_msg =
@@ -435,6 +428,111 @@ class ThreadSafeFunction : public node::AsyncResource {
435428
bool handles_closing;
436429
};
437430

431+
/**
432+
* Compared to node::AsyncResource, the resource object in AsyncContext is
433+
* gc-able. AsyncContext holds a weak reference to the resource object.
434+
* AsyncContext::MakeCallback doesn't implicitly set the receiver of the
435+
* callback to the resource object.
436+
*/
437+
class AsyncContext {
438+
public:
439+
AsyncContext(node_napi_env env,
440+
v8::Local<v8::Object> resource_object,
441+
const v8::Local<v8::String> resource_name,
442+
bool externally_managed_resource)
443+
: env_(env) {
444+
async_id_ = node_env()->new_async_id();
445+
trigger_async_id_ = node_env()->get_default_trigger_async_id();
446+
resource_.Reset(node_env()->isolate(), resource_object);
447+
lost_reference_ = false;
448+
if (externally_managed_resource) {
449+
resource_.SetWeak(
450+
this, AsyncContext::WeakCallback, v8::WeakCallbackType::kParameter);
451+
}
452+
453+
node::AsyncWrap::EmitAsyncInit(node_env(),
454+
resource_object,
455+
resource_name,
456+
async_id_,
457+
trigger_async_id_);
458+
}
459+
460+
~AsyncContext() {
461+
resource_.Reset();
462+
lost_reference_ = true;
463+
node::AsyncWrap::EmitDestroy(node_env(), async_id_);
464+
}
465+
466+
inline v8::MaybeLocal<v8::Value> MakeCallback(
467+
v8::Local<v8::Object> recv,
468+
const v8::Local<v8::Function> callback,
469+
int argc,
470+
v8::Local<v8::Value> argv[]) {
471+
EnsureReference();
472+
return node::InternalMakeCallback(node_env(),
473+
resource(),
474+
recv,
475+
callback,
476+
argc,
477+
argv,
478+
{async_id_, trigger_async_id_});
479+
}
480+
481+
inline napi_callback_scope OpenCallbackScope() {
482+
EnsureReference();
483+
napi_callback_scope it =
484+
reinterpret_cast<napi_callback_scope>(new CallbackScope(this));
485+
env_->open_callback_scopes++;
486+
return it;
487+
}
488+
489+
inline void EnsureReference() {
490+
if (lost_reference_) {
491+
const v8::HandleScope handle_scope(node_env()->isolate());
492+
resource_.Reset(node_env()->isolate(),
493+
v8::Object::New(node_env()->isolate()));
494+
lost_reference_ = false;
495+
}
496+
}
497+
498+
inline node::Environment* node_env() { return env_->node_env(); }
499+
inline v8::Local<v8::Object> resource() {
500+
return resource_.Get(node_env()->isolate());
501+
}
502+
inline node::async_context async_context() {
503+
return {async_id_, trigger_async_id_};
504+
}
505+
506+
static inline void CloseCallbackScope(node_napi_env env,
507+
napi_callback_scope s) {
508+
CallbackScope* callback_scope = reinterpret_cast<CallbackScope*>(s);
509+
delete callback_scope;
510+
env->open_callback_scopes--;
511+
}
512+
513+
static void WeakCallback(const v8::WeakCallbackInfo<AsyncContext>& data) {
514+
AsyncContext* async_context = data.GetParameter();
515+
async_context->resource_.Reset();
516+
async_context->lost_reference_ = true;
517+
}
518+
519+
private:
520+
class CallbackScope : public node::CallbackScope {
521+
public:
522+
explicit CallbackScope(AsyncContext* async_context)
523+
: node::CallbackScope(async_context->node_env()->isolate(),
524+
async_context->resource_.Get(
525+
async_context->node_env()->isolate()),
526+
async_context->async_context()) {}
527+
};
528+
529+
node_napi_env env_;
530+
double async_id_;
531+
double trigger_async_id_;
532+
v8::Global<v8::Object> resource_;
533+
bool lost_reference_;
534+
};
535+
438536
} // end of anonymous namespace
439537

440538
} // end of namespace v8impl
@@ -627,28 +725,19 @@ NAPI_NO_RETURN void napi_fatal_error(const char* location,
627725
}
628726

629727
napi_status napi_open_callback_scope(napi_env env,
630-
napi_value resource_object,
728+
napi_value /** ignored */,
631729
napi_async_context async_context_handle,
632730
napi_callback_scope* result) {
633731
// Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw
634732
// JS exceptions.
635733
CHECK_ENV(env);
636734
CHECK_ARG(env, result);
637735

638-
v8::Local<v8::Context> context = env->context();
639-
640-
node::async_context* node_async_context =
641-
reinterpret_cast<node::async_context*>(async_context_handle);
642-
643-
v8::Local<v8::Object> resource;
644-
CHECK_TO_OBJECT(env, context, resource, resource_object);
736+
v8impl::AsyncContext* node_async_context =
737+
reinterpret_cast<v8impl::AsyncContext*>(async_context_handle);
645738

646-
*result = v8impl::JsCallbackScopeFromV8CallbackScope(
647-
new node::CallbackScope(env->isolate,
648-
resource,
649-
*node_async_context));
739+
*result = node_async_context->OpenCallbackScope();
650740

651-
env->open_callback_scopes++;
652741
return napi_clear_last_error(env);
653742
}
654743

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

664-
env->open_callback_scopes--;
665-
delete v8impl::V8CallbackScopeFromJsCallbackScope(scope);
753+
v8impl::AsyncContext::CloseCallbackScope(reinterpret_cast<node_napi_env>(env),
754+
scope);
755+
666756
return napi_clear_last_error(env);
667757
}
668758

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

680770
v8::Local<v8::Object> v8_resource;
771+
bool externally_managed_resource;
681772
if (async_resource != nullptr) {
682773
CHECK_TO_OBJECT(env, context, v8_resource, async_resource);
774+
externally_managed_resource = true;
683775
} else {
684776
v8_resource = v8::Object::New(isolate);
777+
externally_managed_resource = false;
685778
}
686779

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

690-
// TODO(jasongin): Consider avoiding allocation here by using
691-
// a tagged pointer with 2×31 bit fields instead.
692-
node::async_context* async_context = new node::async_context();
783+
auto async_context =
784+
new v8impl::AsyncContext(reinterpret_cast<node_napi_env>(env),
785+
v8_resource,
786+
v8_resource_name,
787+
externally_managed_resource);
693788

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

697791
return napi_clear_last_error(env);
@@ -702,11 +796,8 @@ napi_status napi_async_destroy(napi_env env,
702796
CHECK_ENV(env);
703797
CHECK_ARG(env, async_context);
704798

705-
node::async_context* node_async_context =
706-
reinterpret_cast<node::async_context*>(async_context);
707-
node::EmitAsyncDestroy(
708-
reinterpret_cast<node_napi_env>(env)->node_env(),
709-
*node_async_context);
799+
v8impl::AsyncContext* node_async_context =
800+
reinterpret_cast<v8impl::AsyncContext*>(async_context);
710801

711802
delete node_async_context;
712803

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

737-
node::async_context* node_async_context =
738-
reinterpret_cast<node::async_context*>(async_context);
739-
if (node_async_context == nullptr) {
740-
static node::async_context empty_context = { 0, 0 };
741-
node_async_context = &empty_context;
742-
}
828+
v8::MaybeLocal<v8::Value> callback_result;
743829

744-
v8::MaybeLocal<v8::Value> callback_result = node::MakeCallback(
745-
env->isolate, v8recv, v8func, argc,
746-
reinterpret_cast<v8::Local<v8::Value>*>(const_cast<napi_value*>(argv)),
747-
*node_async_context);
830+
if (async_context == nullptr) {
831+
callback_result = node::MakeCallback(
832+
env->isolate,
833+
v8recv,
834+
v8func,
835+
argc,
836+
reinterpret_cast<v8::Local<v8::Value>*>(const_cast<napi_value*>(argv)),
837+
{0, 0});
838+
} else {
839+
auto node_async_context =
840+
reinterpret_cast<v8impl::AsyncContext*>(async_context);
841+
callback_result = node_async_context->MakeCallback(
842+
v8recv,
843+
v8func,
844+
argc,
845+
reinterpret_cast<v8::Local<v8::Value>*>(const_cast<napi_value*>(argv)));
846+
}
748847

749848
if (try_catch.HasCaught()) {
750849
return napi_set_last_error(env, napi_pending_exception);

0 commit comments

Comments
 (0)