Skip to content

Commit

Permalink
introduce AsyncResource class
Browse files Browse the repository at this point in the history
This commit adds support for the AsyncResource API to allow native
modules to asynchronously call back into JavaScript while preserving
node's async context. This acts as a higher level alternative to the
MakeCallback API. This is analogous to the AsyncResource JavaScript
class exposed by [async_hooks][] and similar to the `napi_async_init`,
`napi_async_destroy` and `napi_make_callback` APIs, albeit wrapped in
a convenient RAII form-factor.

Ref: nodejs/node#13254
[N-API]: https://nodejs.org/dist/latest-v9.x/docs/api/n-api.html#n_api_custom_asynchronous_operations
[async_hooks]: https://nodejs.org/api/async_hooks.html
  • Loading branch information
ofrobots committed Feb 8, 2018
1 parent 56cb17d commit 0ef3d51
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 2 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ LINT_SOURCES = \
nan_weak.h \
test/cpp/accessors.cpp \
test/cpp/accessors2.cpp \
test/cpp/asyncresource.cpp \
test/cpp/asyncworker.cpp \
test/cpp/asyncprogressworker.cpp \
test/cpp/asyncprogressworkerstream.cpp \
Expand Down
60 changes: 58 additions & 2 deletions doc/node_misc.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,69 @@
## Miscellaneous Node Helpers

- <a href="#api_nan_asyncresource"><b><code>Nan::AsyncResource</code></b></a>
- <a href="#api_nan_make_callback"><b><code>Nan::MakeCallback()</code></b></a>
- <a href="#api_nan_module_init"><b><code>NAN_MODULE_INIT()</code></b></a>
- <a href="#api_nan_export"><b><code>Nan::Export()</code></b></a>

<a name="api_nan_asyncresource"></a>
### Nan::AsyncResource

This class is analogous to the `AsyncResource` JavaScript class exposed by Node's [async_hooks][] API.

When calling back into JavaScript asynchornously, special care must be taken to ensure that the runtime can properly track
async hops. `Nan::AsyncResource` is a class that provides an RAII wrapper around `node::EmitAsyncInit`, `node::EmitAsyncDestroy`,
and `node::MakeCallback`. Using this mechanism to call back into JavaScript, as opposed to `Nan::MakeCallback` or
`v8::Function::Call` ensures that the callback is executed in the correct async context. This ensures that async mechanisms
such as domains and [async_hooks][] function correctly.

Definition:

```c++
class AsyncResource {
public:
AsyncResource(MaybeLocal<v8::Object> maybe_resource, v8::Local<v8::String> name);
AsyncResource(MaybeLocal<v8::Object> maybe_resource, const char* name);
~AsyncResource();

v8::MaybeLocal<v8::Value> runInAsyncScope(v8::Local<v8::Object> target,
v8::Local<v8::Function> func,
int argc,
v8::Local<v8::Value>* argv,
Nan::async_context async_context);
v8::MaybeLocal<v8::Value> runInAsyncScope(v8::Local<v8::Object> target,
v8::Local<v8::String> symbol,
int argc,
v8::Local<v8::Value>* argv,
Nan::async_context async_context);
v8::MaybeLocal<v8::Value> runInAsyncScope(v8::Local<v8::Object> target,
const char* method,
int argc,
v8::Local<v8::Value>* argv,
Nan::async_context async_context);
};
```
* `maybe_resource`: An optional object associated with the async work that will be passed to the possible [async_hooks][]
`init` hook.
* `name`: Identified for the kind of resource that is being provided for diagnostics information exposed by the [async_hooks][]
API. This will be passed to the possible `init` hook as the `type`. To avoid name collisions with other modules we recommend
that the name include the name of the owning module as a prefix. For example `mysql` module could use something like
`mysql:batch-db-query-resource`.
* When calling JS on behalf of this resource, one can use `runInAsyncScope`. This will ensure that the callback runs in the
correct async execution context.
* `AsyncDestroy` is automatically called when an AsyncResource object is destroyed.
For more details, see the Node [async_hooks][] documentation. You might also want to take a look at the documentation for the
[N-API counterpart][napi]. For example usage, see the `asyncresource.cpp` example in the `test/cpp` directory.
<a name="api_nan_make_callback"></a>
### Nan::MakeCallback()
Wrappers around `node::MakeCallback()` providing a consistent API across all supported versions of Node.
Wrappers around the legacy `node::MakeCallback()` APIs.
Use `MakeCallback()` rather than using `v8::Function#Call()` directly in order to properly process internal Node functionality including domains, async hooks, the microtask queue, and other debugging functionality.
We recommend that you use the `AsyncResource` class and `AsyncResource::runInAsyncScope` instead of using `Nan::MakeCallback` or
`v8::Function#Call()` directly. `AsyncResource` properly takes care of running the callback in the correct async execution
context – something that is essential for functionality like domains, async_hooks and async debugging.
Signatures:
Expand Down Expand Up @@ -61,3 +114,6 @@ NAN_MODULE_INIT(Init) {
NAN_EXPORT(target, Foo);
}
```

[async_hooks]: https://nodejs.org/dist/latest-v9.x/docs/api/async_hooks.html
[napi]: https://nodejs.org/dist/latest-v9.x/docs/api/n-api.html#n_api_custom_asynchronous_operations
98 changes: 98 additions & 0 deletions nan.h
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,104 @@ class Utf8String {

#endif // NODE_MODULE_VERSION

//=== async_context ============================================================

#if NODE_MODULE_VERSION >= NODE_8_0_MODULE_VERSION
typedef node::async_context async_context;
#else
struct async_context {};
#endif

// === AsyncResource ===========================================================

class AsyncResource {
public:
AsyncResource(
MaybeLocal<v8::Object> maybe_resource
, v8::Local<v8::String> resource_name) {
#if NODE_MODULE_VERSION >= NODE_8_0_MODULE_VERSION
v8::Isolate* isolate = v8::Isolate::GetCurrent();

v8::Local<v8::Object> resource =
maybe_resource.IsEmpty() ? New<v8::Object>()
: maybe_resource.ToLocalChecked();

node::async_context context =
node::EmitAsyncInit(isolate, resource, resource_name);
asyncContext = static_cast<async_context>(context);
#endif
}

AsyncResource(MaybeLocal<v8::Object> maybe_resource, const char* name) {
#if NODE_MODULE_VERSION >= NODE_8_0_MODULE_VERSION
v8::Isolate* isolate = v8::Isolate::GetCurrent();

v8::Local<v8::Object> resource =
maybe_resource.IsEmpty() ? New<v8::Object>()
: maybe_resource.ToLocalChecked();
v8::Local<v8::String> name_string =
New<v8::String>(name).ToLocalChecked();
node::async_context context =
node::EmitAsyncInit(isolate, resource, name_string);
asyncContext = static_cast<async_context>(context);
#endif
}

~AsyncResource() {
#if NODE_MODULE_VERSION >= NODE_8_0_MODULE_VERSION
v8::Isolate* isolate = v8::Isolate::GetCurrent();
node::async_context node_context =
static_cast<node::async_context>(asyncContext);
node::EmitAsyncDestroy(isolate, node_context);
#endif
}

inline MaybeLocal<v8::Value> runInAsyncScope(
v8::Local<v8::Object> target
, v8::Local<v8::Function> func
, int argc
, v8::Local<v8::Value>* argv) {
#if NODE_MODULE_VERSION < NODE_8_0_MODULE_VERSION
return MakeCallback(target, func, argc, argv);
#else
return node::MakeCallback(
v8::Isolate::GetCurrent(), target, func, argc, argv,
static_cast<node::async_context>(asyncContext));
#endif
}

inline MaybeLocal<v8::Value> runInAsyncScope(
v8::Local<v8::Object> target
, v8::Local<v8::String> symbol
, int argc
, v8::Local<v8::Value>* argv) {
#if NODE_MODULE_VERSION < NODE_8_0_MODULE_VERSION
return MakeCallback(target, symbol, argc, argv);
#else
return node::MakeCallback(
v8::Isolate::GetCurrent(), target, symbol, argc, argv,
static_cast<node::async_context>(asyncContext));
#endif
}

inline MaybeLocal<v8::Value> runInAsyncScope(
v8::Local<v8::Object> target
, const char* method
, int argc
, v8::Local<v8::Value>* argv) {
#if NODE_MODULE_VERSION < NODE_8_0_MODULE_VERSION
return MakeCallback(target, method, argc, argv);
#else
return node::MakeCallback(
v8::Isolate::GetCurrent(), target, method, argc, argv,
static_cast<node::async_context>(asyncContext));
#endif
}

private:
async_context asyncContext;
};

typedef void (*FreeCallback)(char *data, void *hint);

typedef const FunctionCallbackInfo<v8::Value>& NAN_METHOD_ARGS_TYPE;
Expand Down
4 changes: 4 additions & 0 deletions test/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@
"target_name" : "makecallback"
, "sources" : [ "cpp/makecallback.cpp" ]
}
, {
"target_name" : "asyncresource"
, "sources" : [ "cpp/asyncresource.cpp" ]
}
, {
"target_name" : "isolatedata"
, "sources" : [ "cpp/isolatedata.cpp" ]
Expand Down
69 changes: 69 additions & 0 deletions test/cpp/asyncresource.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*********************************************************************
* NAN - Native Abstractions for Node.js
*
* Copyright (c) 2018 NAN contributors
*
* MIT License <https://github.com/nodejs/nan/blob/master/LICENSE.md>
********************************************************************/

#include <nan.h>
#include <unistd.h>

using namespace Nan; // NOLINT(build/namespaces)

class DelayRequest : public AsyncResource {
public:
DelayRequest(int milliseconds_, v8::Local<v8::Function> callback_)
: AsyncResource(MaybeLocal<v8::Object>(), "nan:test.DelayRequest"),
milliseconds(milliseconds_) {
callback.Reset(callback_);
request.data = this;
}
~DelayRequest() {
callback.Reset();
}

Persistent<v8::Function> callback;
uv_work_t request;
int milliseconds;
};

void Delay(uv_work_t* req) {
DelayRequest *delay_request = static_cast<DelayRequest*>(req->data);
sleep(delay_request->milliseconds / 1000);
}

void AfterDelay(uv_work_t* req, int status) {
HandleScope scope;

DelayRequest *delay_request = static_cast<DelayRequest*>(req->data);
v8::Local<v8::Function> callback = New(delay_request->callback);
v8::Local<v8::Value> argv[0] = {};

v8::Local<v8::Object> target = New<v8::Object>();

// Run the callback in the async context.
delay_request->runInAsyncScope(target, callback, 0, argv);

delete delay_request;
}

NAN_METHOD(Delay) {
int delay = To<int>(info[0]).FromJust();
v8::Local<v8::Function> cb = To<v8::Function>(info[1]).ToLocalChecked();

DelayRequest* delay_request = new DelayRequest(delay, cb);

uv_queue_work(
uv_default_loop()
, &delay_request->request
, Delay
, reinterpret_cast<uv_after_work_cb>(AfterDelay));
}

NAN_MODULE_INIT(Init) {
Set(target, New<v8::String>("delay").ToLocalChecked(),
GetFunction(New<v8::FunctionTemplate>(Delay)).ToLocalChecked());
}

NODE_MODULE(asyncresource, Init)
70 changes: 70 additions & 0 deletions test/js/asyncresource-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*********************************************************************
* NAN - Native Abstractions for Node.js
*
* Copyright (c) 2018 NAN contributors
*
* MIT License <https://github.com/nodejs/nan/blob/master/LICENSE.md>
********************************************************************/

try {
require('async_hooks');
} catch (e) {
process.exit(0);
}

const test = require('tap').test
, testRoot = require('path').resolve(__dirname, '..')
, delay = require('bindings')({ module_root: testRoot, bindings: 'asyncresource' }).delay
, asyncHooks = require('async_hooks');

test('asyncresource', function (t) {
t.plan(7);

var resourceAsyncId;
var originalExecutionAsyncId;
var beforeCalled = false;
var afterCalled = false;
var destroyCalled = false;

var hooks = asyncHooks.createHook({
init: function(asyncId, type, triggerAsyncId, resource) {
if (type === 'nan:test.DelayRequest') {
resourceAsyncId = asyncId;
}
},
before: function(asyncId) {
if (asyncId === resourceAsyncId) {
beforeCalled = true;
}
},
after: function(asyncId) {
if (asyncId === resourceAsyncId) {
afterCalled = true;
}
},
destroy: function(asyncId) {
if (asyncId === resourceAsyncId) {
destroyCalled = true;
}
}

});
hooks.enable();

originalExecutionAsyncId = asyncHooks.executionAsyncId();
delay(1000, function() {
t.equal(asyncHooks.executionAsyncId(), resourceAsyncId,
'callback should have the correct execution context');
t.equal(asyncHooks.triggerAsyncId(), originalExecutionAsyncId,
'callback should have the correct trigger context');
t.ok(beforeCalled, 'before should have been called');
t.notOk(afterCalled, 'after should not have been called yet');
setTimeout(function() {
t.ok(afterCalled, 'after should have been called');
t.ok(destroyCalled, 'destroy should have been called');
t.equal(asyncHooks.triggerAsyncId(), resourceAsyncId,
'setTimeout should have been triggered by the async resource');
hooks.disable();
}, 1);
});
});

0 comments on commit 0ef3d51

Please sign in to comment.