Remove async_hooks runInAsyncIdScope API
AndreasMadsen opened this issue · 6 comments
- Version: master (016d81c)
- Platform: all
- Subsystem: async_hooks
As I mentioned a few months ago I'm not comfortable exposing the low-level async_hooks JS API. In the documentation PR #12953 we agreed to keep the low-level JS API undocumented. Some time has now passed and I think we are in a better position to discuss this.
The low-level JS API is quite large, thus I would like to separate the discussion into:
- A discussion about
setInitTriggerId
andtriggerIdScope
. (#14238) - A discussion about
runInAsyncIdScope
. (this) - A discussion about
newUid
,emitInit
,emitBefore
,emitAfter
andemitDestroy
(#15572).
Note: There is some overlap between emitInit
and setInitTriggerId
in terms of initTriggerId
. Hopefully, that won't be an issue.
Background
runInAsyncIdScope(asyncId, cb)
creates a new scope with the asyncId
as
the executionAsyncId
and with the current executionAsyncId
as
the triggerAsyncId
. It does so without invoking the before and after hooks.
runInAsyncIdScope
was not part of the original async_hooks
EP but
was included in the async_hooks
PR.
runInAsyncIdScope
is not used anywhere in node-core and as such, it purpose
is not well documented. @trevnorris mentions it only a single time:
This could also be used by something like a database module, where multiple queries should be treated as their own async operations, but the actual operation is combined into a single query internally. This is the reason for
async_hooks.runInAsyncIdScope()
:
// A pooled resource on top of a native resource
class MyResource inherits async_hooks.AsyncResource {
constructor() {
super('MyResource');
}
query(val, cb) {
// query() will pool the actual request then call each callback with
// the specific data it requested.
nativeResource.query(val, (er, data) => {
async_hooks.runInAsyncIdScope(this.asyncId(), () => cb(er, data));
});
}
}
const mr = new MyResource();
mr.query(val, (er, data) => {
// Now when init() fires for writeFile() it will have both the currentId and
// triggerId of the JS instance.
fs.writeFile(path, data.toString());
});
Although, later @trevnorris says it is a bad example.
Issues
- The primary issue is that
emitBefore
andemitAfter
are not used, thus
the before and after hooks are never invoked. This can be an issue if the user
depends on theexecutionAsyncId()
to match theasyncId
in the before hook.
For example, in trace
I at some point used:
let stack = [];
createHook({
before(asyncId) {
stack.push(states.get(asyncId));
},
after(asyncId) {
stack.pop();
}
})
However, because of runInAsyncIdScope
this is actually invalid code.
Solution
- We remove
runInAsyncIdScope
- The user should use
AsyncResource
andemitBefore
/emitAfter
to change
executionAsyncId()
andtriggerAsyncId()
.
Note: We may want to just deprecate the API in node 8 and remove it in a future version.
For example, the DB resource example should be implemented as:
// A pooled resource on top of a native resource
class MyResource inherits async_hooks.AsyncResource {
constructor() {
super('MyResource');
}
query(val, cb) {
// query() will pool the actual request then call each callback with
// the specific data it requested.
nativeResource.query(val, (er, data) => {
this.emitBefore();
cb(er, data);
this.emitAfter();
});
}
}
/cc @nodejs/async_hooks
I'd like to voice an issue I encountered when trying to use AsyncResource
. Because it is a ES6 class, it is impossible for me to create a class that extends both AsyncResource
and, say, EventEmitter
. Is there a way to resolve this issue?
@TimothyGu Short answer is that you can use newUid
, emitInit
, emitBefore
, emitAfter
and emitDestroy
. I will address it in a future issue about emitInit
and friends.
@AndreasMadsen Okay. I would still much prefer a more generic solution though, like so:
class MyClass extends EventEmitter {
constructor() {
super();
AsyncResource.call(this, 'name');
}
method(cb) {
this.on('event', () => {
native((err, data) => {
this.emitBefore();
try {
cb(err, data);
} finally {
this.emitAfter();
}
});
});
}
}
Object.defineProperties(
MyClass.prototype,
Object.getOwnPropertyDescriptors(AsyncResource.prototype));
I don't want to derail the original conversation of course, and I'd be happy to move this into a new issue.
runInAsyncIdScope()
is a bad API. It's should actually be initTriggerIdScope()
(i.e. it should dictate the triggerId
passed to init()
, nothing more). The pre-PR implementation did this, but I must have overlooked the functionality difference along the way. In short, this API shouldn't have existed in the first place.
So, setInitTriggerId()
and runInAsyncIdScope()
don't address the same problem. Let's remove this API completely, and continue discussion about the need for setInitTriggerId()
in #14238.
EDIT: I tracked down the last commit left in my repo to show that it did at one point do as previously intended at trevnorris@4b16a58#diff-0bb01a51b135a5f68d93540808bac801R179
I don't want to derail the original conversation of course, and I'd be happy to move this into a new issue.
@TimothyGu Please do.
@TimothyGu Please add the link to the new issue here when it's created.