Skip to content

Commit

Permalink
n-api: use AsyncResource for Work tracking
Browse files Browse the repository at this point in the history
Enable combining N-API async work with async-hooks.

PR-URL: #14697
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jason Ginchereau <jasongin@microsoft.com>
Reviewed-By: Michael Dawson <mhdawson@ibm.com>
  • Loading branch information
addaleax committed Sep 14, 2017
1 parent a564c1e commit 8c8c90b
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 20 deletions.
20 changes: 20 additions & 0 deletions doc/api/n-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3256,17 +3256,27 @@ callback invocation, even when it was cancelled.
### napi_create_async_work
<!-- YAML
added: v8.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/14697
description: Added `async_resource` and `async_resource_name` parameters.
-->
```C
NAPI_EXTERN
napi_status napi_create_async_work(napi_env env,
napi_value async_resource,
const char* async_resource_name,
napi_async_execute_callback execute,
napi_async_complete_callback complete,
void* data,
napi_async_work* result);
```

- `[in] env`: The environment that the API is invoked under.
- `[in] async_resource`: An optional object associated with the async work
that will be passed to possible async_hooks [`init` hooks][].
- `[in] async_resource_name`: An identifier for the kind of resource that is
being provided for diagnostic information exposed by the `async_hooks` API.
- `[in] execute`: The native function which should be called to excute
the logic asynchronously.
- `[in] complete`: The native function which will be called when the
Expand All @@ -3282,6 +3292,14 @@ This API allocates a work object that is used to execute logic asynchronously.
It should be freed using [`napi_delete_async_work`][] once the work is no longer
required.

`async_resource_name` should be a null-terminated, UTF-8-encoded string.

*Note*: The `async_resource_name` identifier is provided by the user and should
be representative of the type of async work being performed. It is also
recommended to apply namespacing to the identifier, e.g. by including the
module name. See the [`async_hooks` documentation][async_hooks `type`]
for more information.

### napi_delete_async_work
<!-- YAML
added: v8.0.0
Expand Down Expand Up @@ -3636,3 +3654,5 @@ NAPI_EXTERN napi_status napi_run_script(napi_env env,
[`napi_wrap`]: #n_api_napi_wrap

[`process.release`]: process.html#process_process_release
[`init` hooks]: async_hooks.html#async_hooks_init_asyncid_type_triggerasyncid_resource
[async_hooks `type`]: async_hooks.html#async_hooks_type
32 changes: 27 additions & 5 deletions src/node_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3246,13 +3246,18 @@ static napi_status ConvertUVErrorCode(int code) {
}

// Wrapper around uv_work_t which calls user-provided callbacks.
class Work {
class Work : public node::AsyncResource {
private:
explicit Work(napi_env env,
napi_async_execute_callback execute = nullptr,
v8::Local<v8::Object> async_resource,
const char* async_resource_name,
napi_async_execute_callback execute,
napi_async_complete_callback complete = nullptr,
void* data = nullptr)
: _env(env),
: AsyncResource(env->isolate,
async_resource,
async_resource_name),
_env(env),
_data(data),
_execute(execute),
_complete(complete) {
Expand All @@ -3264,10 +3269,13 @@ class Work {

public:
static Work* New(napi_env env,
v8::Local<v8::Object> async_resource,
const char* async_resource_name,
napi_async_execute_callback execute,
napi_async_complete_callback complete,
void* data) {
return new Work(env, execute, complete, data);
return new Work(env, async_resource, async_resource_name,
execute, complete, data);
}

static void Delete(Work* work) {
Expand All @@ -3288,6 +3296,7 @@ class Work {
// Establish a handle scope here so that every callback doesn't have to.
// Also it is needed for the exception-handling below.
v8::HandleScope scope(env->isolate);
CallbackScope callback_scope(work);

work->_complete(env, ConvertUVErrorCode(status), work->_data);

Expand Down Expand Up @@ -3330,6 +3339,8 @@ class Work {
} while (0)

napi_status napi_create_async_work(napi_env env,
napi_value async_resource,
const char* async_resource_name,
napi_async_execute_callback execute,
napi_async_complete_callback complete,
void* data,
Expand All @@ -3338,7 +3349,18 @@ napi_status napi_create_async_work(napi_env env,
CHECK_ARG(env, execute);
CHECK_ARG(env, result);

uvimpl::Work* work = uvimpl::Work::New(env, execute, complete, data);
v8::Local<v8::Object> resource;
if (async_resource != nullptr) {
auto value = v8impl::V8LocalValueFromJsValue(async_resource);
RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg);
resource = value.As<v8::Object>();
} else {
resource = v8::Object::New(env->isolate);
}

uvimpl::Work* work =
uvimpl::Work::New(env, resource, async_resource_name,
execute, complete, data);

*result = reinterpret_cast<napi_async_work>(work);

Expand Down
2 changes: 2 additions & 0 deletions src/node_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,8 @@ NAPI_EXTERN napi_status napi_get_dataview_info(napi_env env,
// Methods to manage simple async operations
NAPI_EXTERN
napi_status napi_create_async_work(napi_env env,
napi_value async_resource,
const char* async_resource_name,
napi_async_execute_callback execute,
napi_async_complete_callback complete,
void* data,
Expand Down
60 changes: 60 additions & 0 deletions test/addons-napi/test_async/test-async-hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const async_hooks = require('async_hooks');
const test_async = require(`./build/${common.buildType}/test_async`);

const events = [];
let testId;
const initAsyncId = async_hooks.executionAsyncId();

async_hooks.createHook({
init(id, provider, triggerAsyncId, resource) {
if (provider === 'TestResource') {
testId = id;
events.push({ type: 'init', id, provider, triggerAsyncId, resource });
}
},
before(id) {
if (testId === id) {
events.push({ type: 'before', id });
}
},
after(id) {
if (testId === id) {
events.push({ type: 'after', id });
}
},
destroy(id) {
if (testId === id) {
events.push({ type: 'destroy', id });
}
}
}).enable();

const resource = { foo: 'foo' };

events.push({ type: 'start' });
test_async.Test(5, resource, common.mustCall(function(err, val) {
assert.strictEqual(err, null);
assert.strictEqual(val, 10);
events.push({ type: 'complete' });
process.nextTick(common.mustCall());
}));
events.push({ type: 'scheduled' });

process.on('exit', () => {
assert.deepStrictEqual(events, [
{ type: 'start' },
{ type: 'init',
id: testId,
provider: 'TestResource',
triggerAsyncId: initAsyncId,
resource },
{ type: 'scheduled' },
{ type: 'before', id: testId },
{ type: 'complete' },
{ type: 'after', id: testId },
{ type: 'destroy', id: testId }
]);
});
4 changes: 2 additions & 2 deletions test/addons-napi/test_async/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const testException = 'test_async_cb_exception';
// Exception thrown from async completion callback.
// (Tested in a spawned process because the exception is fatal.)
if (process.argv[2] === 'child') {
test_async.Test(1, common.mustCall(function() {
test_async.Test(1, {}, common.mustCall(function() {
throw new Error(testException);
}));
return;
Expand All @@ -20,7 +20,7 @@ assert.ifError(p.error);
assert.ok(p.stderr.toString().includes(testException));

// Successful async execution and completion callback.
test_async.Test(5, common.mustCall(function(err, val) {
test_async.Test(5, {}, common.mustCall(function(err, val) {
assert.strictEqual(err, null);
assert.strictEqual(val, 10);
process.nextTick(common.mustCall());
Expand Down
31 changes: 18 additions & 13 deletions test/addons-napi/test_async/test_async.cc
Original file line number Diff line number Diff line change
Expand Up @@ -61,37 +61,40 @@ void Complete(napi_env env, napi_status status, void* data) {

napi_value result;
NAPI_CALL_RETURN_VOID(env,
napi_make_callback(env, global, callback, 2, argv, &result));
napi_call_function(env, global, callback, 2, argv, &result));

NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, c->_callback));
NAPI_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request));
}

napi_value Test(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value argv[2];
size_t argc = 3;
napi_value argv[3];
napi_value _this;
void* data;
NAPI_CALL(env,
napi_get_cb_info(env, info, &argc, argv, &_this, &data));
NAPI_ASSERT(env, argc >= 2, "Not enough arguments, expected 2.");
NAPI_ASSERT(env, argc >= 3, "Not enough arguments, expected 2.");

napi_valuetype t;
NAPI_CALL(env, napi_typeof(env, argv[0], &t));
NAPI_ASSERT(env, t == napi_number,
"Wrong first argument, integer expected.");
NAPI_CALL(env, napi_typeof(env, argv[1], &t));
NAPI_ASSERT(env, t == napi_object,
"Wrong second argument, object expected.");
NAPI_CALL(env, napi_typeof(env, argv[2], &t));
NAPI_ASSERT(env, t == napi_function,
"Wrong second argument, function expected.");
"Wrong third argument, function expected.");

the_carrier._output = 0;

NAPI_CALL(env,
napi_get_value_int32(env, argv[0], &the_carrier._input));
NAPI_CALL(env,
napi_create_reference(env, argv[1], 1, &the_carrier._callback));
NAPI_CALL(env, napi_create_async_work(
env, Execute, Complete, &the_carrier, &the_carrier._request));
napi_create_reference(env, argv[2], 1, &the_carrier._callback));
NAPI_CALL(env, napi_create_async_work(env, argv[1], "TestResource",
Execute, Complete, &the_carrier, &the_carrier._request));
NAPI_CALL(env,
napi_queue_async_work(env, the_carrier._request));

Expand All @@ -116,7 +119,7 @@ void CancelComplete(napi_env env, napi_status status, void* data) {
NAPI_CALL_RETURN_VOID(env, napi_get_global(env, &global));
napi_value result;
NAPI_CALL_RETURN_VOID(env,
napi_make_callback(env, global, callback, 0, nullptr, &result));
napi_call_function(env, global, callback, 0, nullptr, &result));
}

NAPI_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request));
Expand All @@ -140,8 +143,9 @@ napi_value TestCancel(napi_env env, napi_callback_info info) {
// make sure the work we are going to cancel will not be
// able to start by using all the threads in the pool
for (int i = 1; i < MAX_CANCEL_THREADS; i++) {
NAPI_CALL(env, napi_create_async_work(env, CancelExecute,
BusyCancelComplete, &async_carrier[i], &async_carrier[i]._request));
NAPI_CALL(env, napi_create_async_work(env, nullptr, "TestCancelBusy",
CancelExecute, BusyCancelComplete,
&async_carrier[i], &async_carrier[i]._request));
NAPI_CALL(env, napi_queue_async_work(env, async_carrier[i]._request));
}

Expand All @@ -151,8 +155,9 @@ napi_value TestCancel(napi_env env, napi_callback_info info) {
// workers above.
NAPI_CALL(env,
napi_get_cb_info(env, info, &argc, argv, &_this, &data));
NAPI_CALL(env, napi_create_async_work(env, CancelExecute,
CancelComplete, &async_carrier[0], &async_carrier[0]._request));
NAPI_CALL(env, napi_create_async_work(env, nullptr, "TestCancelled",
CancelExecute, CancelComplete,
&async_carrier[0], &async_carrier[0]._request));
NAPI_CALL(env,
napi_create_reference(env, argv[0], 1, &async_carrier[0]._callback));
NAPI_CALL(env, napi_queue_async_work(env, async_carrier[0]._request));
Expand Down

0 comments on commit 8c8c90b

Please sign in to comment.