Skip to content

Commit e89ec46

Browse files
committed
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: #34567 PR-URL: #34572 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Gabriel Schulhof <[email protected]>
1 parent 0a51aa8 commit e89ec46

File tree

13 files changed

+368
-3
lines changed

13 files changed

+368
-3
lines changed

doc/api/addons.md

+11
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,12 @@ NODE_MODULE_INIT(/* exports, module, context */) {
232232
```
233233
234234
#### Worker support
235+
<!-- YAML
236+
changes:
237+
- version: REPLACEME
238+
pr-url: https://github.com/nodejs/node/pull/34572
239+
description: Cleanup hooks may now be asynchronous.
240+
-->
235241
236242
In order to be loaded from multiple Node.js environments,
237243
such as a main thread and a Worker thread, an add-on needs to either:
@@ -254,6 +260,11 @@ down. If necessary, such hooks can be removed using
254260
`RemoveEnvironmentCleanupHook()` before they are run, which has the same
255261
signature. Callbacks are run in last-in first-out order.
256262

263+
If necessary, there is an additional pair of `AddEnvironmentCleanupHook()`
264+
and `RemoveEnvironmentCleanupHook()` overloads, where the cleanup hook takes a
265+
callback function. This can be used for shutting down asynchronous resources,
266+
for example any libuv handles registered by the addon.
267+
257268
The following `addon.cc` uses `AddEnvironmentCleanupHook`:
258269

259270
```cpp

doc/api/n-api.md

+52-1
Original file line numberDiff line numberDiff line change
@@ -1550,10 +1550,12 @@ and will lead the process to abort.
15501550
The hooks will be called in reverse order, i.e. the most recently added one
15511551
will be called first.
15521552

1553-
Removing this hook can be done by using `napi_remove_env_cleanup_hook`.
1553+
Removing this hook can be done by using [`napi_remove_env_cleanup_hook`][].
15541554
Typically, that happens when the resource for which this hook was added
15551555
is being torn down anyway.
15561556

1557+
For asynchronous cleanup, [`napi_add_async_cleanup_hook`][] is available.
1558+
15571559
#### napi_remove_env_cleanup_hook
15581560
<!-- YAML
15591561
added: v10.2.0
@@ -1573,6 +1575,52 @@ need to be exact matches.
15731575
The function must have originally been registered
15741576
with `napi_add_env_cleanup_hook`, otherwise the process will abort.
15751577

1578+
#### napi_add_async_cleanup_hook
1579+
<!-- YAML
1580+
added: REPLACEME
1581+
-->
1582+
1583+
> Stability: 1 - Experimental
1584+
1585+
```c
1586+
NAPI_EXTERN napi_status napi_add_async_cleanup_hook(
1587+
napi_env env,
1588+
void (*fun)(void* arg, void(* cb)(void*), void* cbarg),
1589+
void* arg,
1590+
napi_async_cleanup_hook_handle* remove_handle);
1591+
```
1592+
1593+
Registers `fun` as a function to be run with the `arg` parameter once the
1594+
current Node.js environment exits. Unlike [`napi_add_env_cleanup_hook`][],
1595+
the hook is allowed to be asynchronous in this case, and must invoke the passed
1596+
`cb()` function with `cbarg` once all asynchronous activity is finished.
1597+
1598+
Otherwise, behavior generally matches that of [`napi_add_env_cleanup_hook`][].
1599+
1600+
If `remove_handle` is not `NULL`, an opaque value will be stored in it
1601+
that must later be passed to [`napi_remove_async_cleanup_hook`][],
1602+
regardless of whether the hook has already been invoked.
1603+
Typically, that happens when the resource for which this hook was added
1604+
is being torn down anyway.
1605+
1606+
#### napi_remove_async_cleanup_hook
1607+
<!-- YAML
1608+
added: REPLACEME
1609+
-->
1610+
1611+
> Stability: 1 - Experimental
1612+
1613+
```c
1614+
NAPI_EXTERN napi_status napi_remove_async_cleanup_hook(
1615+
napi_env env,
1616+
napi_async_cleanup_hook_handle remove_handle);
1617+
```
1618+
1619+
Unregisters the cleanup hook corresponding to `remove_handle`. This will prevent
1620+
the hook from being executed, unless it has already started executing.
1621+
This must be called on any `napi_async_cleanup_hook_handle` value retrieved
1622+
from [`napi_add_async_cleanup_hook`][].
1623+
15761624
## Module registration
15771625
N-API modules are registered in a manner similar to other modules
15781626
except that instead of using the `NODE_MODULE` macro the following
@@ -5704,6 +5752,7 @@ This API may only be called from the main thread.
57045752
[`Worker`]: worker_threads.html#worker_threads_class_worker
57055753
[`global`]: globals.html#globals_global
57065754
[`init` hooks]: async_hooks.html#async_hooks_init_asyncid_type_triggerasyncid_resource
5755+
[`napi_add_async_cleanup_hook`]: #n_api_napi_add_async_cleanup_hook
57075756
[`napi_add_env_cleanup_hook`]: #n_api_napi_add_env_cleanup_hook
57085757
[`napi_add_finalizer`]: #n_api_napi_add_finalizer
57095758
[`napi_async_complete_callback`]: #n_api_napi_async_complete_callback
@@ -5744,6 +5793,8 @@ This API may only be called from the main thread.
57445793
[`napi_queue_async_work`]: #n_api_napi_queue_async_work
57455794
[`napi_reference_ref`]: #n_api_napi_reference_ref
57465795
[`napi_reference_unref`]: #n_api_napi_reference_unref
5796+
[`napi_remove_async_cleanup_hook`]: #n_api_napi_remove_async_cleanup_hook
5797+
[`napi_remove_env_cleanup_hook`]: #n_api_napi_remove_env_cleanup_hook
57475798
[`napi_set_instance_data`]: #n_api_napi_set_instance_data
57485799
[`napi_set_property`]: #n_api_napi_set_property
57495800
[`napi_threadsafe_function_call_js`]: #n_api_napi_threadsafe_function_call_js

src/api/hooks.cc

+66-2
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,86 @@ int EmitExit(Environment* env) {
7373
.ToChecked();
7474
}
7575

76+
typedef void (*CleanupHook)(void* arg);
77+
typedef void (*AsyncCleanupHook)(void* arg, void(*)(void*), void*);
78+
79+
struct AsyncCleanupHookInfo final {
80+
Environment* env;
81+
AsyncCleanupHook fun;
82+
void* arg;
83+
bool started = false;
84+
// Use a self-reference to make sure the storage is kept alive while the
85+
// cleanup hook is registered but not yet finished.
86+
std::shared_ptr<AsyncCleanupHookInfo> self;
87+
};
88+
89+
// Opaque type that is basically an alias for `shared_ptr<AsyncCleanupHookInfo>`
90+
// (but not publicly so for easier ABI/API changes). In particular,
91+
// std::shared_ptr does not generally maintain a consistent ABI even on a
92+
// specific platform.
93+
struct ACHHandle final {
94+
std::shared_ptr<AsyncCleanupHookInfo> info;
95+
};
96+
// This is implemented as an operator on a struct because otherwise you can't
97+
// default-initialize AsyncCleanupHookHandle, because in C++ for a
98+
// std::unique_ptr to be default-initializable the deleter type also needs
99+
// to be default-initializable; in particular, function types don't satisfy
100+
// this.
101+
void DeleteACHHandle::operator ()(ACHHandle* handle) const { delete handle; }
102+
76103
void AddEnvironmentCleanupHook(Isolate* isolate,
77-
void (*fun)(void* arg),
104+
CleanupHook fun,
78105
void* arg) {
79106
Environment* env = Environment::GetCurrent(isolate);
80107
CHECK_NOT_NULL(env);
81108
env->AddCleanupHook(fun, arg);
82109
}
83110

84111
void RemoveEnvironmentCleanupHook(Isolate* isolate,
85-
void (*fun)(void* arg),
112+
CleanupHook fun,
86113
void* arg) {
87114
Environment* env = Environment::GetCurrent(isolate);
88115
CHECK_NOT_NULL(env);
89116
env->RemoveCleanupHook(fun, arg);
90117
}
91118

119+
static void FinishAsyncCleanupHook(void* arg) {
120+
AsyncCleanupHookInfo* info = static_cast<AsyncCleanupHookInfo*>(arg);
121+
std::shared_ptr<AsyncCleanupHookInfo> keep_alive = info->self;
122+
123+
info->env->DecreaseWaitingRequestCounter();
124+
info->self.reset();
125+
}
126+
127+
static void RunAsyncCleanupHook(void* arg) {
128+
AsyncCleanupHookInfo* info = static_cast<AsyncCleanupHookInfo*>(arg);
129+
info->env->IncreaseWaitingRequestCounter();
130+
info->started = true;
131+
info->fun(info->arg, FinishAsyncCleanupHook, info);
132+
}
133+
134+
AsyncCleanupHookHandle AddEnvironmentCleanupHook(
135+
Isolate* isolate,
136+
AsyncCleanupHook fun,
137+
void* arg) {
138+
Environment* env = Environment::GetCurrent(isolate);
139+
CHECK_NOT_NULL(env);
140+
auto info = std::make_shared<AsyncCleanupHookInfo>();
141+
info->env = env;
142+
info->fun = fun;
143+
info->arg = arg;
144+
info->self = info;
145+
env->AddCleanupHook(RunAsyncCleanupHook, info.get());
146+
return AsyncCleanupHookHandle(new ACHHandle { info });
147+
}
148+
149+
void RemoveEnvironmentCleanupHook(
150+
AsyncCleanupHookHandle handle) {
151+
if (handle->info->started) return;
152+
handle->info->self.reset();
153+
handle->info->env->RemoveCleanupHook(RunAsyncCleanupHook, handle->info.get());
154+
}
155+
92156
async_id AsyncHooksGetExecutionAsyncId(Isolate* isolate) {
93157
Environment* env = Environment::GetCurrent(isolate);
94158
if (env == nullptr) return -1;

src/node.h

+14
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,20 @@ NODE_EXTERN void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
872872
void (*fun)(void* arg),
873873
void* arg);
874874

875+
/* These are async equivalents of the above. After the cleanup hook is invoked,
876+
* `cb(cbarg)` *must* be called, and attempting to remove the cleanup hook will
877+
* have no effect. */
878+
struct ACHHandle;
879+
struct NODE_EXTERN DeleteACHHandle { void operator()(ACHHandle*) const; };
880+
typedef std::unique_ptr<ACHHandle, DeleteACHHandle> AsyncCleanupHookHandle;
881+
882+
NODE_EXTERN AsyncCleanupHookHandle AddEnvironmentCleanupHook(
883+
v8::Isolate* isolate,
884+
void (*fun)(void* arg, void (*cb)(void*), void* cbarg),
885+
void* arg);
886+
887+
NODE_EXTERN void RemoveEnvironmentCleanupHook(AsyncCleanupHookHandle holder);
888+
875889
/* Returns the id of the current execution context. If the return value is
876890
* zero then no execution has been set. This will happen if the user handles
877891
* I/O from native code. */

src/node_api.cc

+32
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,38 @@ napi_status napi_remove_env_cleanup_hook(napi_env env,
518518
return napi_ok;
519519
}
520520

521+
struct napi_async_cleanup_hook_handle__ {
522+
node::AsyncCleanupHookHandle handle;
523+
};
524+
525+
napi_status napi_add_async_cleanup_hook(
526+
napi_env env,
527+
void (*fun)(void* arg, void(* cb)(void*), void* cbarg),
528+
void* arg,
529+
napi_async_cleanup_hook_handle* remove_handle) {
530+
CHECK_ENV(env);
531+
CHECK_ARG(env, fun);
532+
533+
auto handle = node::AddEnvironmentCleanupHook(env->isolate, fun, arg);
534+
if (remove_handle != nullptr) {
535+
*remove_handle = new napi_async_cleanup_hook_handle__ { std::move(handle) };
536+
}
537+
538+
return napi_clear_last_error(env);
539+
}
540+
541+
napi_status napi_remove_async_cleanup_hook(
542+
napi_env env,
543+
napi_async_cleanup_hook_handle remove_handle) {
544+
CHECK_ENV(env);
545+
CHECK_ARG(env, remove_handle);
546+
547+
node::RemoveEnvironmentCleanupHook(std::move(remove_handle->handle));
548+
delete remove_handle;
549+
550+
return napi_clear_last_error(env);
551+
}
552+
521553
napi_status napi_fatal_exception(napi_env env, napi_value err) {
522554
NAPI_PREAMBLE(env);
523555
CHECK_ARG(env, err);

src/node_api.h

+14
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,20 @@ napi_ref_threadsafe_function(napi_env env, napi_threadsafe_function func);
250250

251251
#endif // NAPI_VERSION >= 4
252252

253+
#ifdef NAPI_EXPERIMENTAL
254+
255+
NAPI_EXTERN napi_status napi_add_async_cleanup_hook(
256+
napi_env env,
257+
void (*fun)(void* arg, void(* cb)(void*), void* cbarg),
258+
void* arg,
259+
napi_async_cleanup_hook_handle* remove_handle);
260+
261+
NAPI_EXTERN napi_status napi_remove_async_cleanup_hook(
262+
napi_env env,
263+
napi_async_cleanup_hook_handle remove_handle);
264+
265+
#endif // NAPI_EXPERIMENTAL
266+
253267
EXTERN_C_END
254268

255269
#endif // SRC_NODE_API_H_

src/node_api_types.h

+4
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,8 @@ typedef struct {
4141
const char* release;
4242
} napi_node_version;
4343

44+
#ifdef NAPI_EXPERIMENTAL
45+
typedef struct napi_async_cleanup_hook_handle__* napi_async_cleanup_hook_handle;
46+
#endif // NAPI_EXPERIMENTAL
47+
4448
#endif // SRC_NODE_API_TYPES_H_
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#include <assert.h>
2+
#include <node.h>
3+
#include <uv.h>
4+
5+
void MustNotCall(void* arg, void(*cb)(void*), void* cbarg) {
6+
assert(0);
7+
}
8+
9+
struct AsyncData {
10+
uv_async_t async;
11+
v8::Isolate* isolate;
12+
node::AsyncCleanupHookHandle handle;
13+
void (*done_cb)(void*);
14+
void* done_arg;
15+
};
16+
17+
void AsyncCleanupHook(void* arg, void(*cb)(void*), void* cbarg) {
18+
AsyncData* data = static_cast<AsyncData*>(arg);
19+
uv_loop_t* loop = node::GetCurrentEventLoop(data->isolate);
20+
assert(loop != nullptr);
21+
int err = uv_async_init(loop, &data->async, [](uv_async_t* async) {
22+
AsyncData* data = static_cast<AsyncData*>(async->data);
23+
// Attempting to remove the cleanup hook here should be a no-op since it
24+
// has already been started.
25+
node::RemoveEnvironmentCleanupHook(std::move(data->handle));
26+
27+
uv_close(reinterpret_cast<uv_handle_t*>(async), [](uv_handle_t* handle) {
28+
AsyncData* data = static_cast<AsyncData*>(handle->data);
29+
data->done_cb(data->done_arg);
30+
delete data;
31+
});
32+
});
33+
assert(err == 0);
34+
35+
data->async.data = data;
36+
data->done_cb = cb;
37+
data->done_arg = cbarg;
38+
uv_async_send(&data->async);
39+
}
40+
41+
void Initialize(v8::Local<v8::Object> exports,
42+
v8::Local<v8::Value> module,
43+
v8::Local<v8::Context> context) {
44+
AsyncData* data = new AsyncData();
45+
data->isolate = context->GetIsolate();
46+
auto handle = node::AddEnvironmentCleanupHook(
47+
context->GetIsolate(),
48+
AsyncCleanupHook,
49+
data);
50+
data->handle = std::move(handle);
51+
52+
auto must_not_call_handle = node::AddEnvironmentCleanupHook(
53+
context->GetIsolate(),
54+
MustNotCall,
55+
nullptr);
56+
node::RemoveEnvironmentCleanupHook(std::move(must_not_call_handle));
57+
}
58+
59+
NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, Initialize)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'binding',
5+
'sources': [ 'binding.cc' ],
6+
'includes': ['../common.gypi'],
7+
}
8+
]
9+
}
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict';
2+
const common = require('../../common');
3+
const path = require('path');
4+
const { Worker } = require('worker_threads');
5+
const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);
6+
7+
const w = new Worker(`require(${JSON.stringify(binding)})`, { eval: true });
8+
w.on('exit', common.mustCall(() => require(binding)));

0 commit comments

Comments
 (0)