mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
n-api,src: provide asynchronous cleanup hooks
Sometimes addons need to perform cleanup actions, for example closing libuv handles or waiting for requests to finish, that cannot be performed synchronously. Add C++ API and N-API functions that allow providing such asynchronous cleanup hooks. Fixes: https://github.com/nodejs/node/issues/34567 PR-URL: https://github.com/nodejs/node/pull/34572 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gabriel Schulhof <gabriel.schulhof@intel.com>
This commit is contained in:
@@ -234,6 +234,12 @@ NODE_MODULE_INIT(/* exports, module, context */) {
|
||||
```
|
||||
|
||||
#### Worker support
|
||||
<!-- YAML
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/34572
|
||||
description: Cleanup hooks may now be asynchronous.
|
||||
-->
|
||||
|
||||
In order to be loaded from multiple Node.js environments,
|
||||
such as a main thread and a Worker thread, an add-on needs to either:
|
||||
@@ -256,6 +262,11 @@ down. If necessary, such hooks can be removed using
|
||||
`RemoveEnvironmentCleanupHook()` before they are run, which has the same
|
||||
signature. Callbacks are run in last-in first-out order.
|
||||
|
||||
If necessary, there is an additional pair of `AddEnvironmentCleanupHook()`
|
||||
and `RemoveEnvironmentCleanupHook()` overloads, where the cleanup hook takes a
|
||||
callback function. This can be used for shutting down asynchronous resources,
|
||||
for example any libuv handles registered by the addon.
|
||||
|
||||
The following `addon.cc` uses `AddEnvironmentCleanupHook`:
|
||||
|
||||
```cpp
|
||||
|
||||
@@ -1543,10 +1543,12 @@ and will lead the process to abort.
|
||||
The hooks will be called in reverse order, i.e. the most recently added one
|
||||
will be called first.
|
||||
|
||||
Removing this hook can be done by using `napi_remove_env_cleanup_hook`.
|
||||
Removing this hook can be done by using [`napi_remove_env_cleanup_hook`][].
|
||||
Typically, that happens when the resource for which this hook was added
|
||||
is being torn down anyway.
|
||||
|
||||
For asynchronous cleanup, [`napi_add_async_cleanup_hook`][] is available.
|
||||
|
||||
#### napi_remove_env_cleanup_hook
|
||||
<!-- YAML
|
||||
added: v10.2.0
|
||||
@@ -1566,6 +1568,52 @@ need to be exact matches.
|
||||
The function must have originally been registered
|
||||
with `napi_add_env_cleanup_hook`, otherwise the process will abort.
|
||||
|
||||
#### napi_add_async_cleanup_hook
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
```c
|
||||
NAPI_EXTERN napi_status napi_add_async_cleanup_hook(
|
||||
napi_env env,
|
||||
void (*fun)(void* arg, void(* cb)(void*), void* cbarg),
|
||||
void* arg,
|
||||
napi_async_cleanup_hook_handle* remove_handle);
|
||||
```
|
||||
|
||||
Registers `fun` as a function to be run with the `arg` parameter once the
|
||||
current Node.js environment exits. Unlike [`napi_add_env_cleanup_hook`][],
|
||||
the hook is allowed to be asynchronous in this case, and must invoke the passed
|
||||
`cb()` function with `cbarg` once all asynchronous activity is finished.
|
||||
|
||||
Otherwise, behavior generally matches that of [`napi_add_env_cleanup_hook`][].
|
||||
|
||||
If `remove_handle` is not `NULL`, an opaque value will be stored in it
|
||||
that must later be passed to [`napi_remove_async_cleanup_hook`][],
|
||||
regardless of whether the hook has already been invoked.
|
||||
Typically, that happens when the resource for which this hook was added
|
||||
is being torn down anyway.
|
||||
|
||||
#### napi_remove_async_cleanup_hook
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
```c
|
||||
NAPI_EXTERN napi_status napi_remove_async_cleanup_hook(
|
||||
napi_env env,
|
||||
napi_async_cleanup_hook_handle remove_handle);
|
||||
```
|
||||
|
||||
Unregisters the cleanup hook corresponding to `remove_handle`. This will prevent
|
||||
the hook from being executed, unless it has already started executing.
|
||||
This must be called on any `napi_async_cleanup_hook_handle` value retrieved
|
||||
from [`napi_add_async_cleanup_hook`][].
|
||||
|
||||
## Module registration
|
||||
N-API modules are registered in a manner similar to other modules
|
||||
except that instead of using the `NODE_MODULE` macro the following
|
||||
@@ -5659,6 +5707,7 @@ This API may only be called from the main thread.
|
||||
[`Worker`]: worker_threads.html#worker_threads_class_worker
|
||||
[`global`]: globals.html#globals_global
|
||||
[`init` hooks]: async_hooks.html#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_complete_callback`]: #n_api_napi_async_complete_callback
|
||||
@@ -5699,6 +5748,8 @@ This API may only be called from the main thread.
|
||||
[`napi_queue_async_work`]: #n_api_napi_queue_async_work
|
||||
[`napi_reference_ref`]: #n_api_napi_reference_ref
|
||||
[`napi_reference_unref`]: #n_api_napi_reference_unref
|
||||
[`napi_remove_async_cleanup_hook`]: #n_api_napi_remove_async_cleanup_hook
|
||||
[`napi_remove_env_cleanup_hook`]: #n_api_napi_remove_env_cleanup_hook
|
||||
[`napi_set_instance_data`]: #n_api_napi_set_instance_data
|
||||
[`napi_set_property`]: #n_api_napi_set_property
|
||||
[`napi_threadsafe_function_call_js`]: #n_api_napi_threadsafe_function_call_js
|
||||
|
||||
@@ -73,8 +73,35 @@ int EmitExit(Environment* env) {
|
||||
.ToChecked();
|
||||
}
|
||||
|
||||
typedef void (*CleanupHook)(void* arg);
|
||||
typedef void (*AsyncCleanupHook)(void* arg, void(*)(void*), void*);
|
||||
|
||||
struct AsyncCleanupHookInfo final {
|
||||
Environment* env;
|
||||
AsyncCleanupHook fun;
|
||||
void* arg;
|
||||
bool started = false;
|
||||
// Use a self-reference to make sure the storage is kept alive while the
|
||||
// cleanup hook is registered but not yet finished.
|
||||
std::shared_ptr<AsyncCleanupHookInfo> self;
|
||||
};
|
||||
|
||||
// Opaque type that is basically an alias for `shared_ptr<AsyncCleanupHookInfo>`
|
||||
// (but not publicly so for easier ABI/API changes). In particular,
|
||||
// std::shared_ptr does not generally maintain a consistent ABI even on a
|
||||
// specific platform.
|
||||
struct ACHHandle final {
|
||||
std::shared_ptr<AsyncCleanupHookInfo> info;
|
||||
};
|
||||
// This is implemented as an operator on a struct because otherwise you can't
|
||||
// default-initialize AsyncCleanupHookHandle, because in C++ for a
|
||||
// std::unique_ptr to be default-initializable the deleter type also needs
|
||||
// to be default-initializable; in particular, function types don't satisfy
|
||||
// this.
|
||||
void DeleteACHHandle::operator ()(ACHHandle* handle) const { delete handle; }
|
||||
|
||||
void AddEnvironmentCleanupHook(Isolate* isolate,
|
||||
void (*fun)(void* arg),
|
||||
CleanupHook fun,
|
||||
void* arg) {
|
||||
Environment* env = Environment::GetCurrent(isolate);
|
||||
CHECK_NOT_NULL(env);
|
||||
@@ -82,13 +109,50 @@ void AddEnvironmentCleanupHook(Isolate* isolate,
|
||||
}
|
||||
|
||||
void RemoveEnvironmentCleanupHook(Isolate* isolate,
|
||||
void (*fun)(void* arg),
|
||||
CleanupHook fun,
|
||||
void* arg) {
|
||||
Environment* env = Environment::GetCurrent(isolate);
|
||||
CHECK_NOT_NULL(env);
|
||||
env->RemoveCleanupHook(fun, arg);
|
||||
}
|
||||
|
||||
static void FinishAsyncCleanupHook(void* arg) {
|
||||
AsyncCleanupHookInfo* info = static_cast<AsyncCleanupHookInfo*>(arg);
|
||||
std::shared_ptr<AsyncCleanupHookInfo> keep_alive = info->self;
|
||||
|
||||
info->env->DecreaseWaitingRequestCounter();
|
||||
info->self.reset();
|
||||
}
|
||||
|
||||
static void RunAsyncCleanupHook(void* arg) {
|
||||
AsyncCleanupHookInfo* info = static_cast<AsyncCleanupHookInfo*>(arg);
|
||||
info->env->IncreaseWaitingRequestCounter();
|
||||
info->started = true;
|
||||
info->fun(info->arg, FinishAsyncCleanupHook, info);
|
||||
}
|
||||
|
||||
AsyncCleanupHookHandle AddEnvironmentCleanupHook(
|
||||
Isolate* isolate,
|
||||
AsyncCleanupHook fun,
|
||||
void* arg) {
|
||||
Environment* env = Environment::GetCurrent(isolate);
|
||||
CHECK_NOT_NULL(env);
|
||||
auto info = std::make_shared<AsyncCleanupHookInfo>();
|
||||
info->env = env;
|
||||
info->fun = fun;
|
||||
info->arg = arg;
|
||||
info->self = info;
|
||||
env->AddCleanupHook(RunAsyncCleanupHook, info.get());
|
||||
return AsyncCleanupHookHandle(new ACHHandle { info });
|
||||
}
|
||||
|
||||
void RemoveEnvironmentCleanupHook(
|
||||
AsyncCleanupHookHandle handle) {
|
||||
if (handle->info->started) return;
|
||||
handle->info->self.reset();
|
||||
handle->info->env->RemoveCleanupHook(RunAsyncCleanupHook, handle->info.get());
|
||||
}
|
||||
|
||||
async_id AsyncHooksGetExecutionAsyncId(Isolate* isolate) {
|
||||
Environment* env = Environment::GetCurrent(isolate);
|
||||
if (env == nullptr) return -1;
|
||||
|
||||
14
src/node.h
14
src/node.h
@@ -735,6 +735,20 @@ NODE_EXTERN void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
|
||||
void (*fun)(void* arg),
|
||||
void* arg);
|
||||
|
||||
/* These are async equivalents of the above. After the cleanup hook is invoked,
|
||||
* `cb(cbarg)` *must* be called, and attempting to remove the cleanup hook will
|
||||
* have no effect. */
|
||||
struct ACHHandle;
|
||||
struct NODE_EXTERN DeleteACHHandle { void operator()(ACHHandle*) const; };
|
||||
typedef std::unique_ptr<ACHHandle, DeleteACHHandle> AsyncCleanupHookHandle;
|
||||
|
||||
NODE_EXTERN AsyncCleanupHookHandle AddEnvironmentCleanupHook(
|
||||
v8::Isolate* isolate,
|
||||
void (*fun)(void* arg, void (*cb)(void*), void* cbarg),
|
||||
void* arg);
|
||||
|
||||
NODE_EXTERN void RemoveEnvironmentCleanupHook(AsyncCleanupHookHandle holder);
|
||||
|
||||
/* Returns the id of the current execution context. If the return value is
|
||||
* zero then no execution has been set. This will happen if the user handles
|
||||
* I/O from native code. */
|
||||
|
||||
@@ -518,6 +518,38 @@ napi_status napi_remove_env_cleanup_hook(napi_env env,
|
||||
return napi_ok;
|
||||
}
|
||||
|
||||
struct napi_async_cleanup_hook_handle__ {
|
||||
node::AsyncCleanupHookHandle handle;
|
||||
};
|
||||
|
||||
napi_status napi_add_async_cleanup_hook(
|
||||
napi_env env,
|
||||
void (*fun)(void* arg, void(* cb)(void*), void* cbarg),
|
||||
void* arg,
|
||||
napi_async_cleanup_hook_handle* remove_handle) {
|
||||
CHECK_ENV(env);
|
||||
CHECK_ARG(env, fun);
|
||||
|
||||
auto handle = node::AddEnvironmentCleanupHook(env->isolate, fun, arg);
|
||||
if (remove_handle != nullptr) {
|
||||
*remove_handle = new napi_async_cleanup_hook_handle__ { std::move(handle) };
|
||||
}
|
||||
|
||||
return napi_clear_last_error(env);
|
||||
}
|
||||
|
||||
napi_status napi_remove_async_cleanup_hook(
|
||||
napi_env env,
|
||||
napi_async_cleanup_hook_handle remove_handle) {
|
||||
CHECK_ENV(env);
|
||||
CHECK_ARG(env, remove_handle);
|
||||
|
||||
node::RemoveEnvironmentCleanupHook(std::move(remove_handle->handle));
|
||||
delete remove_handle;
|
||||
|
||||
return napi_clear_last_error(env);
|
||||
}
|
||||
|
||||
napi_status napi_fatal_exception(napi_env env, napi_value err) {
|
||||
NAPI_PREAMBLE(env);
|
||||
CHECK_ARG(env, err);
|
||||
|
||||
@@ -250,6 +250,20 @@ napi_ref_threadsafe_function(napi_env env, napi_threadsafe_function func);
|
||||
|
||||
#endif // NAPI_VERSION >= 4
|
||||
|
||||
#ifdef NAPI_EXPERIMENTAL
|
||||
|
||||
NAPI_EXTERN napi_status napi_add_async_cleanup_hook(
|
||||
napi_env env,
|
||||
void (*fun)(void* arg, void(* cb)(void*), void* cbarg),
|
||||
void* arg,
|
||||
napi_async_cleanup_hook_handle* remove_handle);
|
||||
|
||||
NAPI_EXTERN napi_status napi_remove_async_cleanup_hook(
|
||||
napi_env env,
|
||||
napi_async_cleanup_hook_handle remove_handle);
|
||||
|
||||
#endif // NAPI_EXPERIMENTAL
|
||||
|
||||
EXTERN_C_END
|
||||
|
||||
#endif // SRC_NODE_API_H_
|
||||
|
||||
@@ -41,4 +41,8 @@ typedef struct {
|
||||
const char* release;
|
||||
} napi_node_version;
|
||||
|
||||
#ifdef NAPI_EXPERIMENTAL
|
||||
typedef struct napi_async_cleanup_hook_handle__* napi_async_cleanup_hook_handle;
|
||||
#endif // NAPI_EXPERIMENTAL
|
||||
|
||||
#endif // SRC_NODE_API_TYPES_H_
|
||||
|
||||
59
test/addons/async-cleanup-hook/binding.cc
Normal file
59
test/addons/async-cleanup-hook/binding.cc
Normal file
@@ -0,0 +1,59 @@
|
||||
#include <assert.h>
|
||||
#include <node.h>
|
||||
#include <uv.h>
|
||||
|
||||
void MustNotCall(void* arg, void(*cb)(void*), void* cbarg) {
|
||||
assert(0);
|
||||
}
|
||||
|
||||
struct AsyncData {
|
||||
uv_async_t async;
|
||||
v8::Isolate* isolate;
|
||||
node::AsyncCleanupHookHandle handle;
|
||||
void (*done_cb)(void*);
|
||||
void* done_arg;
|
||||
};
|
||||
|
||||
void AsyncCleanupHook(void* arg, void(*cb)(void*), void* cbarg) {
|
||||
AsyncData* data = static_cast<AsyncData*>(arg);
|
||||
uv_loop_t* loop = node::GetCurrentEventLoop(data->isolate);
|
||||
assert(loop != nullptr);
|
||||
int err = uv_async_init(loop, &data->async, [](uv_async_t* async) {
|
||||
AsyncData* data = static_cast<AsyncData*>(async->data);
|
||||
// Attempting to remove the cleanup hook here should be a no-op since it
|
||||
// has already been started.
|
||||
node::RemoveEnvironmentCleanupHook(std::move(data->handle));
|
||||
|
||||
uv_close(reinterpret_cast<uv_handle_t*>(async), [](uv_handle_t* handle) {
|
||||
AsyncData* data = static_cast<AsyncData*>(handle->data);
|
||||
data->done_cb(data->done_arg);
|
||||
delete data;
|
||||
});
|
||||
});
|
||||
assert(err == 0);
|
||||
|
||||
data->async.data = data;
|
||||
data->done_cb = cb;
|
||||
data->done_arg = cbarg;
|
||||
uv_async_send(&data->async);
|
||||
}
|
||||
|
||||
void Initialize(v8::Local<v8::Object> exports,
|
||||
v8::Local<v8::Value> module,
|
||||
v8::Local<v8::Context> context) {
|
||||
AsyncData* data = new AsyncData();
|
||||
data->isolate = context->GetIsolate();
|
||||
auto handle = node::AddEnvironmentCleanupHook(
|
||||
context->GetIsolate(),
|
||||
AsyncCleanupHook,
|
||||
data);
|
||||
data->handle = std::move(handle);
|
||||
|
||||
auto must_not_call_handle = node::AddEnvironmentCleanupHook(
|
||||
context->GetIsolate(),
|
||||
MustNotCall,
|
||||
nullptr);
|
||||
node::RemoveEnvironmentCleanupHook(std::move(must_not_call_handle));
|
||||
}
|
||||
|
||||
NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, Initialize)
|
||||
9
test/addons/async-cleanup-hook/binding.gyp
Normal file
9
test/addons/async-cleanup-hook/binding.gyp
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
'targets': [
|
||||
{
|
||||
'target_name': 'binding',
|
||||
'sources': [ 'binding.cc' ],
|
||||
'includes': ['../common.gypi'],
|
||||
}
|
||||
]
|
||||
}
|
||||
8
test/addons/async-cleanup-hook/test.js
Normal file
8
test/addons/async-cleanup-hook/test.js
Normal file
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
const common = require('../../common');
|
||||
const path = require('path');
|
||||
const { Worker } = require('worker_threads');
|
||||
const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);
|
||||
|
||||
const w = new Worker(`require(${JSON.stringify(binding)})`, { eval: true });
|
||||
w.on('exit', common.mustCall(() => require(binding)));
|
||||
82
test/node-api/test_async_cleanup_hook/binding.c
Normal file
82
test/node-api/test_async_cleanup_hook/binding.c
Normal file
@@ -0,0 +1,82 @@
|
||||
#define NAPI_EXPERIMENTAL
|
||||
#include "node_api.h"
|
||||
#include "assert.h"
|
||||
#include "uv.h"
|
||||
#include <stdlib.h>
|
||||
#include "../../js-native-api/common.h"
|
||||
|
||||
void MustNotCall(void* arg, void(*cb)(void*), void* cbarg) {
|
||||
assert(0);
|
||||
}
|
||||
|
||||
struct AsyncData {
|
||||
uv_async_t async;
|
||||
napi_env env;
|
||||
napi_async_cleanup_hook_handle handle;
|
||||
void (*done_cb)(void*);
|
||||
void* done_arg;
|
||||
};
|
||||
|
||||
struct AsyncData* CreateAsyncData() {
|
||||
struct AsyncData* data = (struct AsyncData*) malloc(sizeof(struct AsyncData));
|
||||
data->handle = NULL;
|
||||
return data;
|
||||
}
|
||||
|
||||
void AfterCleanupHookTwo(uv_handle_t* handle) {
|
||||
struct AsyncData* data = (struct AsyncData*) handle->data;
|
||||
data->done_cb(data->done_arg);
|
||||
free(data);
|
||||
}
|
||||
|
||||
void AfterCleanupHookOne(uv_async_t* async) {
|
||||
struct AsyncData* data = (struct AsyncData*) async->data;
|
||||
if (data->handle != NULL) {
|
||||
// Verify that removing the hook is okay between starting and finishing
|
||||
// of its execution.
|
||||
napi_status status =
|
||||
napi_remove_async_cleanup_hook(data->env, data->handle);
|
||||
assert(status == napi_ok);
|
||||
}
|
||||
|
||||
uv_close((uv_handle_t*) async, AfterCleanupHookTwo);
|
||||
}
|
||||
|
||||
void AsyncCleanupHook(void* arg, void(*cb)(void*), void* cbarg) {
|
||||
struct AsyncData* data = (struct AsyncData*) arg;
|
||||
uv_loop_t* loop;
|
||||
napi_status status = napi_get_uv_event_loop(data->env, &loop);
|
||||
assert(status == napi_ok);
|
||||
int err = uv_async_init(loop, &data->async, AfterCleanupHookOne);
|
||||
assert(err == 0);
|
||||
|
||||
data->async.data = data;
|
||||
data->done_cb = cb;
|
||||
data->done_arg = cbarg;
|
||||
uv_async_send(&data->async);
|
||||
}
|
||||
|
||||
napi_value Init(napi_env env, napi_value exports) {
|
||||
{
|
||||
struct AsyncData* data = CreateAsyncData();
|
||||
data->env = env;
|
||||
napi_add_async_cleanup_hook(env, AsyncCleanupHook, data, &data->handle);
|
||||
}
|
||||
|
||||
{
|
||||
struct AsyncData* data = CreateAsyncData();
|
||||
data->env = env;
|
||||
napi_add_async_cleanup_hook(env, AsyncCleanupHook, data, NULL);
|
||||
}
|
||||
|
||||
{
|
||||
napi_async_cleanup_hook_handle must_not_call_handle;
|
||||
napi_add_async_cleanup_hook(
|
||||
env, MustNotCall, NULL, &must_not_call_handle);
|
||||
napi_remove_async_cleanup_hook(env, must_not_call_handle);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
|
||||
9
test/node-api/test_async_cleanup_hook/binding.gyp
Normal file
9
test/node-api/test_async_cleanup_hook/binding.gyp
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
'targets': [
|
||||
{
|
||||
'target_name': 'binding',
|
||||
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
|
||||
'sources': [ 'binding.c' ]
|
||||
}
|
||||
]
|
||||
}
|
||||
8
test/node-api/test_async_cleanup_hook/test.js
Normal file
8
test/node-api/test_async_cleanup_hook/test.js
Normal file
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
const common = require('../../common');
|
||||
const path = require('path');
|
||||
const { Worker } = require('worker_threads');
|
||||
const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);
|
||||
|
||||
const w = new Worker(`require(${JSON.stringify(binding)})`, { eval: true });
|
||||
w.on('exit', common.mustCall(() => require(binding)));
|
||||
Reference in New Issue
Block a user