Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
f46649b
util: print External address from inspect
rosaxxny Jul 16, 2020
465968c
console: document the behavior of console.assert()
Jul 23, 2020
692a735
doc: use sentence-case for headers in SECURITY.md
Trott Jul 27, 2020
70e9ece
build: toolchain.gypi and node_gyp.py cleanup
Jul 9, 2020
14656e1
async_hooks: don't reuse resource in HttpAgent when queued
puzpuzpuz Jul 20, 2020
9c442f9
test: remove unneeded flag check in test-vm-memleak
Trott Jul 27, 2020
2703fe4
n-api: simplify bigint-from-word creation
Jul 29, 2020
70cf3cb
build: auto start Jenkins CI via PR labels
mmarchini Jun 27, 2020
c8104f3
doc: update .mailmap for mmarchini
mmarchini Jul 31, 2020
7a28c3d
doc: update mmarchini contact info
mmarchini Jul 31, 2020
212d17f
doc: add mmarchini pronouns
mmarchini Jul 31, 2020
8cc9e5e
n-api: support type-tagging objects
Jun 14, 2019
b69ff2f
doc: use consistent capitalization for addons
Trott Jul 28, 2020
f393ae9
doc: simplify and clarify console.assert() documentation
Trott Jul 29, 2020
be23e23
doc: use consistent spelling for "falsy"
Trott Jul 29, 2020
4af5dbd
build: fix auto-start-ci script path
mmarchini Jul 31, 2020
befbaf3
build: don't run auto-start-ci on push
mmarchini Aug 1, 2020
44e6c01
esm: fix hook mistypes and links to types
DerekNonGeneric Jul 7, 2020
c4457d8
benchmark: always throw the same Error instance
addaleax Jul 26, 2020
335cb0d
lib: absorb `path` error cases
gireeshpunathil Jul 26, 2020
e2bea73
doc: clarify N-API version 1
mhdawson Jul 13, 2020
c1abc8d
src: fix unused namespace member in node_util
puzpuzpuz Jul 30, 2020
36fd3da
http: provide keep-alive timeout response header
ronag Jul 30, 2020
d7eaf3a
doc: revise N-API versions matrix text
Trott Jul 30, 2020
7322e58
http: reset headers timeout on headers complete
ronag Jul 31, 2020
10dd7a0
doc: add DerekNonGeneric to collaborators
DerekNonGeneric Aug 2, 2020
0a9389b
doc: mention null special-case for `napi_typeof`
goto-bus-stop Jul 31, 2020
9af6264
async_hooks: execute destroy hooks earlier
Flarna Jul 13, 2020
7c4e1db
async_hooks: fix resource stack for deep stacks
addaleax Jul 31, 2020
283f5c3
test: fix flaky http-parser-timeout-reset
ronag Aug 3, 2020
1b0d3b2
doc: document the connection event for HTTP2 & TLS servers
pimterry Jul 27, 2020
dba869e
test: change Fixes: to Refs:
Trott Jul 30, 2020
18ca52d
async_hooks: fix id assignment in fast-path promise hook
puzpuzpuz Jul 28, 2020
2d2ea99
tools: add meta.fixable to fixable lint rules
cjihrig Aug 1, 2020
72f357a
tools: update ESLint to 7.6.0
cjihrig Aug 1, 2020
ae64ec4
repl: give repl entries unique names
bmeck Jul 14, 2020
d6b0a40
test: replace flaky pummel regression tests
addaleax Jul 27, 2020
d972c54
doc: clarify process.title inconsistencies
coreybutler Jul 29, 2020
ba137e0
doc: add release key for Ruy Adorno
ruyadorno Aug 5, 2020
938842e
repl: improve static import error message in repl
MylesBorins May 27, 2020
338994f
build: increase startCI verbosity and fix job name
mmarchini Aug 5, 2020
3eabb7e
policy: increase tests via permutation matrix
bmeck Jul 16, 2020
7be68cd
test: convert most N-API tests from C++ to C
Aug 3, 2020
d0a599a
module: unflag Top-Level Await
MylesBorins Jul 29, 2020
c21e62f
module: handle Top-Level Await non-fulfills better
addaleax Aug 6, 2020
22f499f
build: enable build for node-v8 push
gengjiawen Aug 5, 2020
221802d
build: run CI on release branches
codebytere Aug 6, 2020
4dbb931
async_hooks: don't read resource if ALS is disabled
Flarna Aug 3, 2020
ffd2c7a
meta: enable http2 team for CODEOWNERS
Trott Jul 28, 2020
a503470
crypto: add OP flag constants added in OpenSSL v1.1.1
mkrawczuk Jun 17, 2020
a0caf8c
doc: fix typo in path.md
Jul 29, 2020
767a5cb
doc: update fs.watch() availability for IBM i
Aug 3, 2020
4cd9c4f
test: add debugging for test-https-foafssl.js
Trott Aug 2, 2020
27333b1
test: add debugging for callbacks in test-https-foafssl.js
Trott Aug 2, 2020
7b4d40c
doc: edit process.title note for brevity and clarity
Trott Aug 4, 2020
8ff9caa
deps: update to uvwasi 0.0.10
cjihrig Aug 4, 2020
c4d373b
wasi: add __wasi_fd_filestat_set_times() test
cjihrig Aug 4, 2020
3132878
src: fix `size` underflow in CallbackQueue
addaleax Aug 7, 2020
4369c2a
src: spin shutdown loop while immediates are pending
addaleax Aug 7, 2020
f0b364f
build: do not run auto-start-ci on forks
evanlucas Aug 6, 2020
df8f9a4
doc: add Ricky Zhou to collaborators
rickyes Aug 8, 2020
33777a3
test: fix wrong method call
gengjiawen Aug 5, 2020
3ecb16b
repl: use _REPL_ in user-facing text
Trott Aug 6, 2020
7887ce2
net: don't return the stream object from onStreamRead
robey Jul 15, 2020
0c4226e
repl: use _Node.js_ in user-facing REPL text
Trott Aug 6, 2020
ef4fb68
async_hooks: add AsyncResource.bind utility
jasnell Jul 31, 2020
0730c76
async_hooks: improve property descriptors in als.bind
Flarna Aug 4, 2020
641a5fb
async_hooks: avoid unneeded AsyncResource creation
Flarna Aug 3, 2020
8630f34
n-api,src: provide asynchronous cleanup hooks
addaleax Jul 31, 2020
db6f9bd
async_hooks: avoid GC tracking of AsyncResource in ALS
Flarna Aug 3, 2020
4ed89a3
n-api: fix use-after-free with napi_remove_async_cleanup_hook
addaleax Aug 7, 2020
a944dab
doc: use _Class Method_ in async_hooks.md
Trott Aug 4, 2020
e97fe4b
src: fix linter failures
addaleax Jul 31, 2020
af0cfeb
tools: fix C++ import checker argument expansion
addaleax Jul 31, 2020
5b5f5f9
doc: tidy some addons.md text
Trott Aug 6, 2020
4efc5f6
lib: use non-symbols in isURLInstance check
codebytere Aug 4, 2020
41d0cf7
doc: use _Static method_ instead of _Class Method_
Trott Aug 7, 2020
bd00f26
meta: uncomment all codeowners
mmarchini Aug 7, 2020
b45ea94
2020-08-11, Version 14.8.0 (Current)
codebytere Aug 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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 <jasnell@gmail.com>
Reviewed-By: Gabriel Schulhof <gabriel.schulhof@intel.com>
  • Loading branch information
addaleax authored and codebytere committed Aug 11, 2020
commit 8630f3477697835719df93dbc49d03f60cdf2b31
11 changes: 11 additions & 0 deletions doc/api/addons.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,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:
Expand All @@ -254,6 +260,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
Expand Down
53 changes: 52 additions & 1 deletion doc/api/n-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1550,10 +1550,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
Expand All @@ -1573,6 +1575,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
Expand Down Expand Up @@ -5704,6 +5752,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
Expand Down Expand Up @@ -5744,6 +5793,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
Expand Down
68 changes: 66 additions & 2 deletions src/api/hooks.cc
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,86 @@ 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);
env->AddCleanupHook(fun, arg);
}

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;
Expand Down
14 changes: 14 additions & 0 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,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. */
Expand Down
32 changes: 32 additions & 0 deletions src/node_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions src/node_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_
4 changes: 4 additions & 0 deletions src/node_api_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 59 additions & 0 deletions test/addons/async-cleanup-hook/binding.cc
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions test/addons/async-cleanup-hook/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
}
]
}
8 changes: 8 additions & 0 deletions test/addons/async-cleanup-hook/test.js
Original file line number Diff line number Diff line change
@@ -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)));
Loading