Skip to content

Commit 90c0a17

Browse files
ofrobotskkoopa
authored andcommittedFeb 8, 2018
introduce AsyncResource class
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
1 parent 56cb17d commit 90c0a17

File tree

6 files changed

+300
-2
lines changed

6 files changed

+300
-2
lines changed
 

‎Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ LINT_SOURCES = \
3535
nan_weak.h \
3636
test/cpp/accessors.cpp \
3737
test/cpp/accessors2.cpp \
38+
test/cpp/asyncresource.cpp \
3839
test/cpp/asyncworker.cpp \
3940
test/cpp/asyncprogressworker.cpp \
4041
test/cpp/asyncprogressworkerstream.cpp \

‎doc/node_misc.md

+58-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,69 @@
11
## Miscellaneous Node Helpers
22

3+
- <a href="#api_nan_asyncresource"><b><code>Nan::AsyncResource</code></b></a>
34
- <a href="#api_nan_make_callback"><b><code>Nan::MakeCallback()</code></b></a>
45
- <a href="#api_nan_module_init"><b><code>NAN_MODULE_INIT()</code></b></a>
56
- <a href="#api_nan_export"><b><code>Nan::Export()</code></b></a>
67

8+
<a name="api_nan_asyncresource"></a>
9+
### Nan::AsyncResource
10+
11+
This class is analogous to the `AsyncResource` JavaScript class exposed by Node's [async_hooks][] API.
12+
13+
When calling back into JavaScript asynchornously, special care must be taken to ensure that the runtime can properly track
14+
async hops. `Nan::AsyncResource` is a class that provides an RAII wrapper around `node::EmitAsyncInit`, `node::EmitAsyncDestroy`,
15+
and `node::MakeCallback`. Using this mechanism to call back into JavaScript, as opposed to `Nan::MakeCallback` or
16+
`v8::Function::Call` ensures that the callback is executed in the correct async context. This ensures that async mechanisms
17+
such as domains and [async_hooks][] function correctly.
18+
19+
Definition:
20+
21+
```c++
22+
class AsyncResource {
23+
public:
24+
AsyncResource(MaybeLocal<v8::Object> maybe_resource, v8::Local<v8::String> name);
25+
AsyncResource(MaybeLocal<v8::Object> maybe_resource, const char* name);
26+
~AsyncResource();
27+
28+
v8::MaybeLocal<v8::Value> runInAsyncScope(v8::Local<v8::Object> target,
29+
v8::Local<v8::Function> func,
30+
int argc,
31+
v8::Local<v8::Value>* argv,
32+
Nan::async_context async_context);
33+
v8::MaybeLocal<v8::Value> runInAsyncScope(v8::Local<v8::Object> target,
34+
v8::Local<v8::String> symbol,
35+
int argc,
36+
v8::Local<v8::Value>* argv,
37+
Nan::async_context async_context);
38+
v8::MaybeLocal<v8::Value> runInAsyncScope(v8::Local<v8::Object> target,
39+
const char* method,
40+
int argc,
41+
v8::Local<v8::Value>* argv,
42+
Nan::async_context async_context);
43+
};
44+
```
45+
46+
* `maybe_resource`: An optional object associated with the async work that will be passed to the possible [async_hooks][]
47+
`init` hook.
48+
* `name`: Identified for the kind of resource that is being provided for diagnostics information exposed by the [async_hooks][]
49+
API. This will be passed to the possible `init` hook as the `type`. To avoid name collisions with other modules we recommend
50+
that the name include the name of the owning module as a prefix. For example `mysql` module could use something like
51+
`mysql:batch-db-query-resource`.
52+
* When calling JS on behalf of this resource, one can use `runInAsyncScope`. This will ensure that the callback runs in the
53+
correct async execution context.
54+
* `AsyncDestroy` is automatically called when an AsyncResource object is destroyed.
55+
56+
For more details, see the Node [async_hooks][] documentation. You might also want to take a look at the documentation for the
57+
[N-API counterpart][napi]. For example usage, see the `asyncresource.cpp` example in the `test/cpp` directory.
758
859
<a name="api_nan_make_callback"></a>
960
### Nan::MakeCallback()
1061
11-
Wrappers around `node::MakeCallback()` providing a consistent API across all supported versions of Node.
62+
Wrappers around the legacy `node::MakeCallback()` APIs.
1263
13-
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.
64+
We recommend that you use the `AsyncResource` class and `AsyncResource::runInAsyncScope` instead of using `Nan::MakeCallback` or
65+
`v8::Function#Call()` directly. `AsyncResource` properly takes care of running the callback in the correct async execution
66+
context – something that is essential for functionality like domains, async_hooks and async debugging.
1467
1568
Signatures:
1669
@@ -61,3 +114,6 @@ NAN_MODULE_INIT(Init) {
61114
NAN_EXPORT(target, Foo);
62115
}
63116
```
117+
118+
[async_hooks]: https://nodejs.org/dist/latest-v9.x/docs/api/async_hooks.html
119+
[napi]: https://nodejs.org/dist/latest-v9.x/docs/api/n-api.html#n_api_custom_asynchronous_operations

‎nan.h

+98
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,104 @@ class Utf8String {
12731273

12741274
#endif // NODE_MODULE_VERSION
12751275

1276+
//=== async_context ============================================================
1277+
1278+
#if NODE_MODULE_VERSION >= NODE_8_0_MODULE_VERSION
1279+
typedef node::async_context async_context;
1280+
#else
1281+
struct async_context {};
1282+
#endif
1283+
1284+
// === AsyncResource ===========================================================
1285+
1286+
class AsyncResource {
1287+
public:
1288+
AsyncResource(
1289+
MaybeLocal<v8::Object> maybe_resource
1290+
, v8::Local<v8::String> resource_name) {
1291+
#if NODE_MODULE_VERSION >= NODE_8_0_MODULE_VERSION
1292+
v8::Isolate* isolate = v8::Isolate::GetCurrent();
1293+
1294+
v8::Local<v8::Object> resource =
1295+
maybe_resource.IsEmpty() ? New<v8::Object>()
1296+
: maybe_resource.ToLocalChecked();
1297+
1298+
node::async_context context =
1299+
node::EmitAsyncInit(isolate, resource, resource_name);
1300+
asyncContext = static_cast<async_context>(context);
1301+
#endif
1302+
}
1303+
1304+
AsyncResource(MaybeLocal<v8::Object> maybe_resource, const char* name) {
1305+
#if NODE_MODULE_VERSION >= NODE_8_0_MODULE_VERSION
1306+
v8::Isolate* isolate = v8::Isolate::GetCurrent();
1307+
1308+
v8::Local<v8::Object> resource =
1309+
maybe_resource.IsEmpty() ? New<v8::Object>()
1310+
: maybe_resource.ToLocalChecked();
1311+
v8::Local<v8::String> name_string =
1312+
New<v8::String>(name).ToLocalChecked();
1313+
node::async_context context =
1314+
node::EmitAsyncInit(isolate, resource, name_string);
1315+
asyncContext = static_cast<async_context>(context);
1316+
#endif
1317+
}
1318+
1319+
~AsyncResource() {
1320+
#if NODE_MODULE_VERSION >= NODE_8_0_MODULE_VERSION
1321+
v8::Isolate* isolate = v8::Isolate::GetCurrent();
1322+
node::async_context node_context =
1323+
static_cast<node::async_context>(asyncContext);
1324+
node::EmitAsyncDestroy(isolate, node_context);
1325+
#endif
1326+
}
1327+
1328+
inline MaybeLocal<v8::Value> runInAsyncScope(
1329+
v8::Local<v8::Object> target
1330+
, v8::Local<v8::Function> func
1331+
, int argc
1332+
, v8::Local<v8::Value>* argv) {
1333+
#if NODE_MODULE_VERSION < NODE_8_0_MODULE_VERSION
1334+
return MakeCallback(target, func, argc, argv);
1335+
#else
1336+
return node::MakeCallback(
1337+
v8::Isolate::GetCurrent(), target, func, argc, argv,
1338+
static_cast<node::async_context>(asyncContext));
1339+
#endif
1340+
}
1341+
1342+
inline MaybeLocal<v8::Value> runInAsyncScope(
1343+
v8::Local<v8::Object> target
1344+
, v8::Local<v8::String> symbol
1345+
, int argc
1346+
, v8::Local<v8::Value>* argv) {
1347+
#if NODE_MODULE_VERSION < NODE_8_0_MODULE_VERSION
1348+
return MakeCallback(target, symbol, argc, argv);
1349+
#else
1350+
return node::MakeCallback(
1351+
v8::Isolate::GetCurrent(), target, symbol, argc, argv,
1352+
static_cast<node::async_context>(asyncContext));
1353+
#endif
1354+
}
1355+
1356+
inline MaybeLocal<v8::Value> runInAsyncScope(
1357+
v8::Local<v8::Object> target
1358+
, const char* method
1359+
, int argc
1360+
, v8::Local<v8::Value>* argv) {
1361+
#if NODE_MODULE_VERSION < NODE_8_0_MODULE_VERSION
1362+
return MakeCallback(target, method, argc, argv);
1363+
#else
1364+
return node::MakeCallback(
1365+
v8::Isolate::GetCurrent(), target, method, argc, argv,
1366+
static_cast<node::async_context>(asyncContext));
1367+
#endif
1368+
}
1369+
1370+
private:
1371+
async_context asyncContext;
1372+
};
1373+
12761374
typedef void (*FreeCallback)(char *data, void *hint);
12771375

12781376
typedef const FunctionCallbackInfo<v8::Value>& NAN_METHOD_ARGS_TYPE;

‎test/binding.gyp

+4
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@
9090
"target_name" : "makecallback"
9191
, "sources" : [ "cpp/makecallback.cpp" ]
9292
}
93+
, {
94+
"target_name" : "asyncresource"
95+
, "sources" : [ "cpp/asyncresource.cpp" ]
96+
}
9397
, {
9498
"target_name" : "isolatedata"
9599
, "sources" : [ "cpp/isolatedata.cpp" ]

‎test/cpp/asyncresource.cpp

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*********************************************************************
2+
* NAN - Native Abstractions for Node.js
3+
*
4+
* Copyright (c) 2018 NAN contributors
5+
*
6+
* MIT License <https://github.com/nodejs/nan/blob/master/LICENSE.md>
7+
********************************************************************/
8+
9+
#include <nan.h>
10+
#include <unistd.h>
11+
12+
using namespace Nan; // NOLINT(build/namespaces)
13+
14+
class DelayRequest : public AsyncResource {
15+
public:
16+
DelayRequest(int milliseconds_, v8::Local<v8::Function> callback_)
17+
: AsyncResource(MaybeLocal<v8::Object>(), "nan:test.DelayRequest"),
18+
milliseconds(milliseconds_) {
19+
callback.Reset(callback_);
20+
request.data = this;
21+
}
22+
~DelayRequest() {
23+
callback.Reset();
24+
}
25+
26+
Persistent<v8::Function> callback;
27+
uv_work_t request;
28+
int milliseconds;
29+
};
30+
31+
void Delay(uv_work_t* req) {
32+
DelayRequest *delay_request = static_cast<DelayRequest*>(req->data);
33+
sleep(delay_request->milliseconds / 1000);
34+
}
35+
36+
void AfterDelay(uv_work_t* req, int status) {
37+
HandleScope scope;
38+
39+
DelayRequest *delay_request = static_cast<DelayRequest*>(req->data);
40+
v8::Local<v8::Function> callback = New(delay_request->callback);
41+
v8::Local<v8::Value> argv[0] = {};
42+
43+
v8::Local<v8::Object> target = New<v8::Object>();
44+
45+
// Run the callback in the async context.
46+
delay_request->runInAsyncScope(target, callback, 0, argv);
47+
48+
delete delay_request;
49+
}
50+
51+
NAN_METHOD(Delay) {
52+
int delay = To<int>(info[0]).FromJust();
53+
v8::Local<v8::Function> cb = To<v8::Function>(info[1]).ToLocalChecked();
54+
55+
DelayRequest* delay_request = new DelayRequest(delay, cb);
56+
57+
uv_queue_work(
58+
uv_default_loop()
59+
, &delay_request->request
60+
, Delay
61+
, reinterpret_cast<uv_after_work_cb>(AfterDelay));
62+
}
63+
64+
NAN_MODULE_INIT(Init) {
65+
Set(target, New<v8::String>("delay").ToLocalChecked(),
66+
GetFunction(New<v8::FunctionTemplate>(Delay)).ToLocalChecked());
67+
}
68+
69+
NODE_MODULE(asyncresource, Init)

‎test/js/asyncresource-test.js

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*********************************************************************
2+
* NAN - Native Abstractions for Node.js
3+
*
4+
* Copyright (c) 2018 NAN contributors
5+
*
6+
* MIT License <https://github.com/nodejs/nan/blob/master/LICENSE.md>
7+
********************************************************************/
8+
9+
try {
10+
require('async_hooks');
11+
} catch (e) {
12+
process.exit(0);
13+
}
14+
15+
const test = require('tap').test
16+
, testRoot = require('path').resolve(__dirname, '..')
17+
, delay = require('bindings')({ module_root: testRoot, bindings: 'asyncresource' }).delay
18+
, asyncHooks = require('async_hooks');
19+
20+
test('asyncresource', function (t) {
21+
t.plan(7);
22+
23+
var resourceAsyncId;
24+
var originalExecutionAsyncId;
25+
var beforeCalled = false;
26+
var afterCalled = false;
27+
var destroyCalled = false;
28+
29+
var hooks = asyncHooks.createHook({
30+
init: function(asyncId, type, triggerAsyncId, resource) {
31+
if (type === 'nan:test.DelayRequest') {
32+
resourceAsyncId = asyncId;
33+
}
34+
},
35+
before: function(asyncId) {
36+
if (asyncId === resourceAsyncId) {
37+
beforeCalled = true;
38+
}
39+
},
40+
after: function(asyncId) {
41+
if (asyncId === resourceAsyncId) {
42+
afterCalled = true;
43+
}
44+
},
45+
destroy: function(asyncId) {
46+
if (asyncId === resourceAsyncId) {
47+
destroyCalled = true;
48+
}
49+
}
50+
51+
});
52+
hooks.enable();
53+
54+
originalExecutionAsyncId = asyncHooks.executionAsyncId();
55+
delay(1000, function() {
56+
t.equal(asyncHooks.executionAsyncId(), resourceAsyncId,
57+
'callback should have the correct execution context');
58+
t.equal(asyncHooks.triggerAsyncId(), originalExecutionAsyncId,
59+
'callback should have the correct trigger context');
60+
t.ok(beforeCalled, 'before should have been called');
61+
t.notOk(afterCalled, 'after should not have been called yet');
62+
setTimeout(function() {
63+
t.ok(afterCalled, 'after should have been called');
64+
t.ok(destroyCalled, 'destroy should have been called');
65+
t.equal(asyncHooks.triggerAsyncId(), resourceAsyncId,
66+
'setTimeout should have been triggered by the async resource');
67+
hooks.disable();
68+
}, 1);
69+
});
70+
});

0 commit comments

Comments
 (0)
Please sign in to comment.