Skip to content

Commit 63ed50f

Browse files
addaleaxGabriel Schulhof
authored and
Gabriel Schulhof
committed
src: add environment cleanup hooks
This adds pairs of methods to the `Environment` class and to public APIs which can add and remove cleanup handlers. Unlike `AtExit`, this API targets addon developers rather than embedders, giving them (and Node\u2019s internals) the ability to register per-`Environment` cleanup work. We may want to replace `AtExit` with this API at some point. Many thanks for Stephen Belanger for reviewing the original version of this commit in the Ayo.js project. Refs: ayojs/ayo#82 PR-URL: nodejs#19377 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 01e2eba commit 63ed50f

File tree

11 files changed

+241
-0
lines changed

11 files changed

+241
-0
lines changed

doc/api/n-api.md

+52
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,58 @@ If still valid, this API returns the `napi_value` representing the
905905
JavaScript Object associated with the `napi_ref`. Otherwise, result
906906
will be NULL.
907907

908+
### Cleanup on exit of the current Node.js instance
909+
910+
While a Node.js process typically releases all its resources when exiting,
911+
embedders of Node.js, or future Worker support, may require addons to register
912+
clean-up hooks that will be run once the current Node.js instance exits.
913+
914+
N-API provides functions for registering and un-registering such callbacks.
915+
When those callbacks are run, all resources that are being held by the addon
916+
should be freed up.
917+
918+
#### napi_add_env_cleanup_hook
919+
<!-- YAML
920+
added: REPLACEME
921+
-->
922+
```C
923+
NODE_EXTERN napi_status napi_add_env_cleanup_hook(napi_env env,
924+
void (*fun)(void* arg),
925+
void* arg);
926+
```
927+
928+
Registers `fun` as a function to be run with the `arg` parameter once the
929+
current Node.js environment exits.
930+
931+
A function can safely be specified multiple times with different
932+
`arg` values. In that case, it will be called multiple times as well.
933+
Providing the same `fun` and `arg` values multiple times is not allowed
934+
and will lead the process to abort.
935+
936+
The hooks will be called in reverse order, i.e. the most recently added one
937+
will be called first.
938+
939+
Removing this hook can be done by using `napi_remove_env_cleanup_hook`.
940+
Typically, that happens when the resource for which this hook was added
941+
is being torn down anyway.
942+
943+
#### napi_remove_env_cleanup_hook
944+
<!-- YAML
945+
added: REPLACEME
946+
-->
947+
```C
948+
NAPI_EXTERN napi_status napi_remove_env_cleanup_hook(napi_env env,
949+
void (*fun)(void* arg),
950+
void* arg);
951+
```
952+
953+
Unregisters `fun` as a function to be run with the `arg` parameter once the
954+
current Node.js environment exits. Both the argument and the function value
955+
need to be exact matches.
956+
957+
The function must have originally been registered
958+
with `napi_add_env_cleanup_hook`, otherwise the process will abort.
959+
908960
## Module registration
909961
N-API modules are registered in a manner similar to other modules
910962
except that instead of using the `NODE_MODULE` macro the following

src/env-inl.h

+23
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,29 @@ inline void Environment::SetTemplateMethod(v8::Local<v8::FunctionTemplate> that,
549549
t->SetClassName(name_string); // NODE_SET_METHOD() compatibility.
550550
}
551551

552+
void Environment::AddCleanupHook(void (*fn)(void*), void* arg) {
553+
auto insertion_info = cleanup_hooks_.emplace(CleanupHookCallback {
554+
fn, arg, cleanup_hook_counter_++
555+
});
556+
// Make sure there was no existing element with these values.
557+
CHECK_EQ(insertion_info.second, true);
558+
}
559+
560+
void Environment::RemoveCleanupHook(void (*fn)(void*), void* arg) {
561+
CleanupHookCallback search { fn, arg, 0 };
562+
cleanup_hooks_.erase(search);
563+
}
564+
565+
size_t Environment::CleanupHookCallback::Hash::operator()(
566+
const CleanupHookCallback& cb) const {
567+
return std::hash<void*>()(cb.arg_);
568+
}
569+
570+
bool Environment::CleanupHookCallback::Equal::operator()(
571+
const CleanupHookCallback& a, const CleanupHookCallback& b) const {
572+
return a.fn_ == b.fn_ && a.arg_ == b.arg_;
573+
}
574+
552575
#define VP(PropertyName, StringValue) V(v8::Private, PropertyName)
553576
#define VS(PropertyName, StringValue) V(v8::String, PropertyName)
554577
#define V(TypeName, PropertyName) \

src/env.cc

+29
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,35 @@ void Environment::PrintSyncTrace() const {
287287
fflush(stderr);
288288
}
289289

290+
void Environment::RunCleanup() {
291+
while (!cleanup_hooks_.empty()) {
292+
// Copy into a vector, since we can't sort an unordered_set in-place.
293+
std::vector<CleanupHookCallback> callbacks(
294+
cleanup_hooks_.begin(), cleanup_hooks_.end());
295+
// We can't erase the copied elements from `cleanup_hooks_` yet, because we
296+
// need to be able to check whether they were un-scheduled by another hook.
297+
298+
std::sort(callbacks.begin(), callbacks.end(),
299+
[](const CleanupHookCallback& a, const CleanupHookCallback& b) {
300+
// Sort in descending order so that the most recently inserted callbacks
301+
// are run first.
302+
return a.insertion_order_counter_ > b.insertion_order_counter_;
303+
});
304+
305+
for (const CleanupHookCallback& cb : callbacks) {
306+
if (cleanup_hooks_.count(cb) == 0) {
307+
// This hook was removed from the `cleanup_hooks_` set during another
308+
// hook that was run earlier. Nothing to do here.
309+
continue;
310+
}
311+
312+
cb.fn_(cb.arg_);
313+
cleanup_hooks_.erase(cb);
314+
CleanupHandles();
315+
}
316+
}
317+
}
318+
290319
void Environment::RunAtExitCallbacks() {
291320
for (AtExitCallback at_exit : at_exit_functions_) {
292321
at_exit.cb_(at_exit.arg_);

src/env.h

+31
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
#include <stdint.h>
4242
#include <vector>
4343
#include <unordered_map>
44+
#include <unordered_set>
4445

4546
struct nghttp2_rcbuf;
4647

@@ -706,6 +707,10 @@ class Environment {
706707

707708
static inline Environment* ForAsyncHooks(AsyncHooks* hooks);
708709

710+
inline void AddCleanupHook(void (*fn)(void*), void* arg);
711+
inline void RemoveCleanupHook(void (*fn)(void*), void* arg);
712+
void RunCleanup();
713+
709714
private:
710715
inline void ThrowError(v8::Local<v8::Value> (*fun)(v8::Local<v8::String>),
711716
const char* errmsg);
@@ -780,6 +785,32 @@ class Environment {
780785
void RunAndClearNativeImmediates();
781786
static void CheckImmediate(uv_check_t* handle);
782787

788+
struct CleanupHookCallback {
789+
void (*fn_)(void*);
790+
void* arg_;
791+
792+
// We keep track of the insertion order for these objects, so that we can
793+
// call the callbacks in reverse order when we are cleaning up.
794+
uint64_t insertion_order_counter_;
795+
796+
// Only hashes `arg_`, since that is usually enough to identify the hook.
797+
struct Hash {
798+
inline size_t operator()(const CleanupHookCallback& cb) const;
799+
};
800+
801+
// Compares by `fn_` and `arg_` being equal.
802+
struct Equal {
803+
inline bool operator()(const CleanupHookCallback& a,
804+
const CleanupHookCallback& b) const;
805+
};
806+
};
807+
808+
// Use an unordered_set, so that we have efficient insertion and removal.
809+
std::unordered_set<CleanupHookCallback,
810+
CleanupHookCallback::Hash,
811+
CleanupHookCallback::Equal> cleanup_hooks_;
812+
uint64_t cleanup_hook_counter_ = 0;
813+
783814
static void EnvPromiseHook(v8::PromiseHookType type,
784815
v8::Local<v8::Promise> promise,
785816
v8::Local<v8::Value> parent);

src/node.cc

+19
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,22 @@ void AddPromiseHook(v8::Isolate* isolate, promise_hook_func fn, void* arg) {
12991299
env->AddPromiseHook(fn, arg);
13001300
}
13011301

1302+
void AddEnvironmentCleanupHook(v8::Isolate* isolate,
1303+
void (*fun)(void* arg),
1304+
void* arg) {
1305+
Environment* env = Environment::GetCurrent(isolate);
1306+
env->AddCleanupHook(fun, arg);
1307+
}
1308+
1309+
1310+
void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
1311+
void (*fun)(void* arg),
1312+
void* arg) {
1313+
Environment* env = Environment::GetCurrent(isolate);
1314+
env->RemoveCleanupHook(fun, arg);
1315+
}
1316+
1317+
13021318
CallbackScope::CallbackScope(Isolate* isolate,
13031319
Local<Object> object,
13041320
async_context asyncContext)
@@ -4039,6 +4055,7 @@ Environment* CreateEnvironment(IsolateData* isolate_data,
40394055

40404056

40414057
void FreeEnvironment(Environment* env) {
4058+
env->RunCleanup();
40424059
delete env;
40434060
}
40444061

@@ -4112,6 +4129,8 @@ inline int Start(Isolate* isolate, IsolateData* isolate_data,
41124129
env.set_trace_sync_io(false);
41134130

41144131
const int exit_code = EmitExit(&env);
4132+
4133+
env.RunCleanup();
41154134
RunAtExit(&env);
41164135
uv_key_delete(&thread_local_env);
41174136

src/node.h

+13
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,19 @@ NODE_EXTERN void AddPromiseHook(v8::Isolate* isolate,
580580
promise_hook_func fn,
581581
void* arg);
582582

583+
/* This is a lot like node::AtExit, except that the hooks added via this
584+
* function are run before the AtExit ones and will always be registered
585+
* for the current Environment instance.
586+
* These functions are safe to use in an addon supporting multiple
587+
* threads/isolates. */
588+
NODE_EXTERN void AddEnvironmentCleanupHook(v8::Isolate* isolate,
589+
void (*fun)(void* arg),
590+
void* arg);
591+
592+
NODE_EXTERN void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
593+
void (*fun)(void* arg),
594+
void* arg);
595+
583596
/* Returns the id of the current execution context. If the return value is
584597
* zero then no execution has been set. This will happen if the user handles
585598
* I/O from native code. */

src/node_api.cc

+22
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,28 @@ void napi_module_register(napi_module* mod) {
909909
node::node_module_register(nm);
910910
}
911911

912+
napi_status napi_add_env_cleanup_hook(napi_env env,
913+
void (*fun)(void* arg),
914+
void* arg) {
915+
CHECK_ENV(env);
916+
CHECK_ARG(env, fun);
917+
918+
node::AddEnvironmentCleanupHook(env->isolate, fun, arg);
919+
920+
return napi_ok;
921+
}
922+
923+
napi_status napi_remove_env_cleanup_hook(napi_env env,
924+
void (*fun)(void* arg),
925+
void* arg) {
926+
CHECK_ENV(env);
927+
CHECK_ARG(env, fun);
928+
929+
node::RemoveEnvironmentCleanupHook(env->isolate, fun, arg);
930+
931+
return napi_ok;
932+
}
933+
912934
// Warning: Keep in-sync with napi_status enum
913935
static
914936
const char* error_messages[] = {nullptr,

src/node_api.h

+7
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ EXTERN_C_START
9999

100100
NAPI_EXTERN void napi_module_register(napi_module* mod);
101101

102+
NAPI_EXTERN napi_status napi_add_env_cleanup_hook(napi_env env,
103+
void (*fun)(void* arg),
104+
void* arg);
105+
NAPI_EXTERN napi_status napi_remove_env_cleanup_hook(napi_env env,
106+
void (*fun)(void* arg),
107+
void* arg);
108+
102109
NAPI_EXTERN napi_status
103110
napi_get_last_error_info(napi_env env,
104111
const napi_extended_error_info** result);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#include "node_api.h"
2+
#include "uv.h"
3+
#include "../common.h"
4+
5+
namespace {
6+
7+
void cleanup(void* arg) {
8+
printf("cleanup(%d)\n", *static_cast<int*>(arg));
9+
}
10+
11+
int secret = 42;
12+
int wrong_secret = 17;
13+
14+
napi_value Init(napi_env env, napi_value exports) {
15+
napi_add_env_cleanup_hook(env, cleanup, &wrong_secret);
16+
napi_add_env_cleanup_hook(env, cleanup, &secret);
17+
napi_remove_env_cleanup_hook(env, cleanup, &wrong_secret);
18+
19+
return nullptr;
20+
}
21+
22+
} // anonymous namespace
23+
24+
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'binding',
5+
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
6+
'sources': [ 'binding.cc' ]
7+
}
8+
]
9+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use strict';
2+
const common = require('../../common');
3+
const assert = require('assert');
4+
const child_process = require('child_process');
5+
6+
if (process.argv[2] === 'child') {
7+
require(`./build/${common.buildType}/binding`);
8+
} else {
9+
const { stdout } =
10+
child_process.spawnSync(process.execPath, [__filename, 'child']);
11+
assert.strictEqual(stdout.toString().trim(), 'cleanup(42)');
12+
}

0 commit comments

Comments
 (0)