Skip to content

Commit

Permalink
src: add environment cleanup hooks
Browse files Browse the repository at this point in the history
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 <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
addaleax authored and Gabriel Schulhof committed Aug 21, 2018
1 parent 01e2eba commit 63ed50f
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 0 deletions.
52 changes: 52 additions & 0 deletions doc/api/n-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,58 @@ If still valid, this API returns the `napi_value` representing the
JavaScript Object associated with the `napi_ref`. Otherwise, result
will be NULL.

### Cleanup on exit of the current Node.js instance

While a Node.js process typically releases all its resources when exiting,
embedders of Node.js, or future Worker support, may require addons to register
clean-up hooks that will be run once the current Node.js instance exits.

N-API provides functions for registering and un-registering such callbacks.
When those callbacks are run, all resources that are being held by the addon
should be freed up.

#### napi_add_env_cleanup_hook
<!-- YAML
added: REPLACEME
-->
```C
NODE_EXTERN napi_status napi_add_env_cleanup_hook(napi_env env,
void (*fun)(void* arg),
void* arg);
```

Registers `fun` as a function to be run with the `arg` parameter once the
current Node.js environment exits.

A function can safely be specified multiple times with different
`arg` values. In that case, it will be called multiple times as well.
Providing the same `fun` and `arg` values multiple times is not allowed
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`.
Typically, that happens when the resource for which this hook was added
is being torn down anyway.

#### napi_remove_env_cleanup_hook
<!-- YAML
added: REPLACEME
-->
```C
NAPI_EXTERN napi_status napi_remove_env_cleanup_hook(napi_env env,
void (*fun)(void* arg),
void* arg);
```

Unregisters `fun` as a function to be run with the `arg` parameter once the
current Node.js environment exits. Both the argument and the function value
need to be exact matches.

The function must have originally been registered
with `napi_add_env_cleanup_hook`, otherwise the process will abort.

## 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
Expand Down
23 changes: 23 additions & 0 deletions src/env-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,29 @@ inline void Environment::SetTemplateMethod(v8::Local<v8::FunctionTemplate> that,
t->SetClassName(name_string); // NODE_SET_METHOD() compatibility.
}

void Environment::AddCleanupHook(void (*fn)(void*), void* arg) {
auto insertion_info = cleanup_hooks_.emplace(CleanupHookCallback {
fn, arg, cleanup_hook_counter_++
});
// Make sure there was no existing element with these values.
CHECK_EQ(insertion_info.second, true);
}

void Environment::RemoveCleanupHook(void (*fn)(void*), void* arg) {
CleanupHookCallback search { fn, arg, 0 };
cleanup_hooks_.erase(search);
}

size_t Environment::CleanupHookCallback::Hash::operator()(
const CleanupHookCallback& cb) const {
return std::hash<void*>()(cb.arg_);
}

bool Environment::CleanupHookCallback::Equal::operator()(
const CleanupHookCallback& a, const CleanupHookCallback& b) const {
return a.fn_ == b.fn_ && a.arg_ == b.arg_;
}

#define VP(PropertyName, StringValue) V(v8::Private, PropertyName)
#define VS(PropertyName, StringValue) V(v8::String, PropertyName)
#define V(TypeName, PropertyName) \
Expand Down
29 changes: 29 additions & 0 deletions src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,35 @@ void Environment::PrintSyncTrace() const {
fflush(stderr);
}

void Environment::RunCleanup() {
while (!cleanup_hooks_.empty()) {
// Copy into a vector, since we can't sort an unordered_set in-place.
std::vector<CleanupHookCallback> callbacks(
cleanup_hooks_.begin(), cleanup_hooks_.end());
// We can't erase the copied elements from `cleanup_hooks_` yet, because we
// need to be able to check whether they were un-scheduled by another hook.

std::sort(callbacks.begin(), callbacks.end(),
[](const CleanupHookCallback& a, const CleanupHookCallback& b) {
// Sort in descending order so that the most recently inserted callbacks
// are run first.
return a.insertion_order_counter_ > b.insertion_order_counter_;
});

for (const CleanupHookCallback& cb : callbacks) {
if (cleanup_hooks_.count(cb) == 0) {
// This hook was removed from the `cleanup_hooks_` set during another
// hook that was run earlier. Nothing to do here.
continue;
}

cb.fn_(cb.arg_);
cleanup_hooks_.erase(cb);
CleanupHandles();
}
}
}

void Environment::RunAtExitCallbacks() {
for (AtExitCallback at_exit : at_exit_functions_) {
at_exit.cb_(at_exit.arg_);
Expand Down
31 changes: 31 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include <stdint.h>
#include <vector>
#include <unordered_map>
#include <unordered_set>

struct nghttp2_rcbuf;

Expand Down Expand Up @@ -706,6 +707,10 @@ class Environment {

static inline Environment* ForAsyncHooks(AsyncHooks* hooks);

inline void AddCleanupHook(void (*fn)(void*), void* arg);
inline void RemoveCleanupHook(void (*fn)(void*), void* arg);
void RunCleanup();

private:
inline void ThrowError(v8::Local<v8::Value> (*fun)(v8::Local<v8::String>),
const char* errmsg);
Expand Down Expand Up @@ -780,6 +785,32 @@ class Environment {
void RunAndClearNativeImmediates();
static void CheckImmediate(uv_check_t* handle);

struct CleanupHookCallback {
void (*fn_)(void*);
void* arg_;

// We keep track of the insertion order for these objects, so that we can
// call the callbacks in reverse order when we are cleaning up.
uint64_t insertion_order_counter_;

// Only hashes `arg_`, since that is usually enough to identify the hook.
struct Hash {
inline size_t operator()(const CleanupHookCallback& cb) const;
};

// Compares by `fn_` and `arg_` being equal.
struct Equal {
inline bool operator()(const CleanupHookCallback& a,
const CleanupHookCallback& b) const;
};
};

// Use an unordered_set, so that we have efficient insertion and removal.
std::unordered_set<CleanupHookCallback,
CleanupHookCallback::Hash,
CleanupHookCallback::Equal> cleanup_hooks_;
uint64_t cleanup_hook_counter_ = 0;

static void EnvPromiseHook(v8::PromiseHookType type,
v8::Local<v8::Promise> promise,
v8::Local<v8::Value> parent);
Expand Down
19 changes: 19 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,22 @@ void AddPromiseHook(v8::Isolate* isolate, promise_hook_func fn, void* arg) {
env->AddPromiseHook(fn, arg);
}

void AddEnvironmentCleanupHook(v8::Isolate* isolate,
void (*fun)(void* arg),
void* arg) {
Environment* env = Environment::GetCurrent(isolate);
env->AddCleanupHook(fun, arg);
}


void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
void (*fun)(void* arg),
void* arg) {
Environment* env = Environment::GetCurrent(isolate);
env->RemoveCleanupHook(fun, arg);
}


CallbackScope::CallbackScope(Isolate* isolate,
Local<Object> object,
async_context asyncContext)
Expand Down Expand Up @@ -4039,6 +4055,7 @@ Environment* CreateEnvironment(IsolateData* isolate_data,


void FreeEnvironment(Environment* env) {
env->RunCleanup();
delete env;
}

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

const int exit_code = EmitExit(&env);

env.RunCleanup();
RunAtExit(&env);
uv_key_delete(&thread_local_env);

Expand Down
13 changes: 13 additions & 0 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,19 @@ NODE_EXTERN void AddPromiseHook(v8::Isolate* isolate,
promise_hook_func fn,
void* arg);

/* This is a lot like node::AtExit, except that the hooks added via this
* function are run before the AtExit ones and will always be registered
* for the current Environment instance.
* These functions are safe to use in an addon supporting multiple
* threads/isolates. */
NODE_EXTERN void AddEnvironmentCleanupHook(v8::Isolate* isolate,
void (*fun)(void* arg),
void* arg);

NODE_EXTERN void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
void (*fun)(void* arg),
void* arg);

/* 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. */
Expand Down
22 changes: 22 additions & 0 deletions src/node_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,28 @@ void napi_module_register(napi_module* mod) {
node::node_module_register(nm);
}

napi_status napi_add_env_cleanup_hook(napi_env env,
void (*fun)(void* arg),
void* arg) {
CHECK_ENV(env);
CHECK_ARG(env, fun);

node::AddEnvironmentCleanupHook(env->isolate, fun, arg);

return napi_ok;
}

napi_status napi_remove_env_cleanup_hook(napi_env env,
void (*fun)(void* arg),
void* arg) {
CHECK_ENV(env);
CHECK_ARG(env, fun);

node::RemoveEnvironmentCleanupHook(env->isolate, fun, arg);

return napi_ok;
}

// Warning: Keep in-sync with napi_status enum
static
const char* error_messages[] = {nullptr,
Expand Down
7 changes: 7 additions & 0 deletions src/node_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ EXTERN_C_START

NAPI_EXTERN void napi_module_register(napi_module* mod);

NAPI_EXTERN napi_status napi_add_env_cleanup_hook(napi_env env,
void (*fun)(void* arg),
void* arg);
NAPI_EXTERN napi_status napi_remove_env_cleanup_hook(napi_env env,
void (*fun)(void* arg),
void* arg);

NAPI_EXTERN napi_status
napi_get_last_error_info(napi_env env,
const napi_extended_error_info** result);
Expand Down
24 changes: 24 additions & 0 deletions test/addons-napi/test_cleanup_hook/binding.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#include "node_api.h"
#include "uv.h"
#include "../common.h"

namespace {

void cleanup(void* arg) {
printf("cleanup(%d)\n", *static_cast<int*>(arg));
}

int secret = 42;
int wrong_secret = 17;

napi_value Init(napi_env env, napi_value exports) {
napi_add_env_cleanup_hook(env, cleanup, &wrong_secret);
napi_add_env_cleanup_hook(env, cleanup, &secret);
napi_remove_env_cleanup_hook(env, cleanup, &wrong_secret);

return nullptr;
}

} // anonymous namespace

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
9 changes: 9 additions & 0 deletions test/addons-napi/test_cleanup_hook/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
'sources': [ 'binding.cc' ]
}
]
}
12 changes: 12 additions & 0 deletions test/addons-napi/test_cleanup_hook/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const child_process = require('child_process');

if (process.argv[2] === 'child') {
require(`./build/${common.buildType}/binding`);
} else {
const { stdout } =
child_process.spawnSync(process.execPath, [__filename, 'child']);
assert.strictEqual(stdout.toString().trim(), 'cleanup(42)');
}

0 comments on commit 63ed50f

Please sign in to comment.