AsyncWrap public API proposal #18

Open
wants to merge 15 commits into
from

Projects

None yet
@trevnorris
Contributor

After much investigation and communication this is the API that has
surfaced. Meant to be minimal, not impose any performance penalty to
core when not being used, and minimal impact when it is used, this
should serve public needs that have been expressed over the last two
years.

@nodejs/ctc I'd like the initial review explicitly from the CTC before this is opened for too much external debate. Because experience has shown me that there will be suggestions/changes for those who want specific features and/or additions to suit their specific use case. Usually not taking the time to realize that this API is enough. They just need to write the additional code for the hooks.

@Fishrock123 Fishrock123 commented on an outdated diff Apr 27, 2016
XXX-asyncwrap-api.md
+
+### Hook Callbacks
+
+Key events in the lifetime of asynchronous events have been categorized into
+four areas. On instantiation, before/after the callback is called and when the
+instance is destructed. For cases where resources are reused, instantiation and
+destructor calls are emulated.
+
+#### `init(id, type, parentId)`
+
+* `id` {Number}
+* `type` {String}
+* `parentId` {Number}
+
+Called when a class is constructed that has the possibility to trigger an
+asynchronous event. This does mean the instance _will_ trigger a
@Fishrock123
Fishrock123 Apr 27, 2016 Member

does not mean

@Fishrock123 Fishrock123 and 2 others commented on an outdated diff Apr 27, 2016
XXX-asyncwrap-api.md
+to `true`. If the callback threw but was not caught then the process will exit
+immediately without calling `post()`.
+
+#### `destroy(id)`
+
+* `id` {Number}
+
+Called either when the class destructor is run, or if the resource is marked as
+free. The destructor will usually run when explicitly called (the case for
+handles) or when a request has completed. In at least one case this will be
+triggered by GC. In the case of shared or cached resources, such as
+`HTTPParser`, `destroy()` will be manually called when the TCP connection no
+longer needs it.
+
+Because the callback is called during deconstruction of the class instance it
+is not safe to access the JS handle during `destroy()` execution. Reason this
@Fishrock123
Fishrock123 Apr 27, 2016 Member

Does that mean that this is not that handle, i.e. undefined?

@trevnorris
trevnorris Apr 28, 2016 Contributor

Yes. To reiterate a comment I just posted as to why this is the case:

Now, destroy() doesn't have a this because it's essentially a static method. It's unsafe to access a C++ class instance while deconstructing. So only the id of the handle is passed to JS so any storage resources can be released.

So you could essentially see undefined as the handle instance at that point in time. Since it has been (or soon will be) free'd.

@Fishrock123
Fishrock123 Apr 28, 2016 edited Member

But is the handle instance (i.e. if you access this) actually undefined here?

@trevnorris
trevnorris Apr 28, 2016 Contributor

Yes. Logging this will show undefined.

@jasnell
jasnell Apr 29, 2016 Member

The inconsistency here between destroy and the other callbacks is another argument in favor of avoiding the use of this and passing the context as an argument. The destroy method simply would not have that argument set. As @chrisdickinson mentions it's more of a stylistic choice but I'd certainly prefer it

@jasnell
jasnell Apr 29, 2016 Member

Nit: The reason this...

@trevnorris
trevnorris Apr 29, 2016 Contributor

@jasnell I disagree that there's fundamental inconsistency. When destroy() is called the handle should no longer available for access. It reflects the fact that by accessing the handle you have undefined behavior. Remember that it's possible to store the object prior to calling destroy(), and while the handle may exist in JS accessing it leads to undefined behavior because the backing C++ class no longer exists. Thus making this === undefined is because this is literally undefined at that point.

@trevnorris
trevnorris Apr 29, 2016 Contributor

screw it. i'm going through and making it safe to access this regardless of where it's accessed. can't having the application abort, even if they screwed up. so this in destroy() will be set to the handle.

@chrisdickinson chrisdickinson and 2 others commented on an outdated diff Apr 27, 2016
XXX-asyncwrap-api.md
+
+* `id` {Number}
+* `type` {String}
+* `parentId` {Number}
+
+Called when a class is constructed that has the possibility to trigger an
+asynchronous event. This does mean the instance _will_ trigger a
+`pre()`/`post()` event. Only that the possibility exists.
+
+The `this` of each call is that of the object being constructed. While core
+attempts to make accessing properties of these handles safe at construction, it
+is not guaranteed (especially if third-party modules are involved) that
+everything can be safely accessed. So it is not recommended to blindly
+iterate over the object's properties. Despite this, it is important to give
+access of the handle to the user so they can utilize tracking methods of their
+choosing. For example:
@chrisdickinson
chrisdickinson Apr 27, 2016

It'd be nice to avoid meaningful this in new APIs, but I understand if it's impossible due to perf concerns.

@trevnorris
trevnorris Apr 28, 2016 Contributor

I'd be lying if I said it was for performance. It's done this way because it makes syntactic sense. The callback is running as an extension of the handle, thus the handle becomes the calling context for the function. Why do you have an aversion to using this? Other than disallowing the use of arrow functions, which I don't accept as a valid argument over having a syntactically correct API, I don't see any disadvantage. While the advantage is that it more closely mimics the C++ class counterpart. Which is important here because they're so closely bound.

Now, destroy() doesn't have a this because it's essentially a static method. It's unsafe to access a C++ class instance while deconstructing. So only the id of the handle is passed to JS so any storage resources can be released.

@chrisdickinson
chrisdickinson Apr 29, 2016

The argument against this is primarily one of convention — anecdotally, many JS APIs have been moving away from "load-bearing this" over the last few years. When I was poking at the asyncwrap API (pre-docs), I was surprised that this carried pertinent info — it was not obvious to me that the hook was running as an extension of the handle.

It's admittedly a nit — but if it comes at zero cost & makes the API more obvious, it might be worth addressing.

@jasnell
jasnell Apr 29, 2016 Member

I agree with @chrisdickinson on this point.

@chrisdickinson chrisdickinson and 4 others commented on an outdated diff Apr 27, 2016
XXX-asyncwrap-api.md
+
+### `process.nextTick()` and Promises
+
+Execution of the `nextTickQueue` and `MicrotaskQueue` are slightly special
+asynchronous cases. Because they execute in the same `MakeCallback()` as the
+asynchronous callback they will ultimately end up with the same parent as the
+originating call.
+
+In the case of `nextTick()`, many calls are meant to call or queue other tasks.
+Which would easily end up causing async wrap to incur more of a cost than
+calling each callback in the `nextTickQueue`. Though not calling the hooks
+would lead to loss of stack information.
+
+Node currently doesn't have sufficient API to notify calls to Promise
+callbacks. In order to do so node would have to override the native
+implementation.
@chrisdickinson
chrisdickinson Apr 27, 2016

I think we have to solve this before the API can be made public.

@Fishrock123
Fishrock123 Apr 27, 2016 Member

This is a V8 API problem, right?

@chrisdickinson
chrisdickinson Apr 27, 2016

Yep — it was on my plate last but I ran out of time to work on it. It was looking like the solution would be pre/post hooks for executing microtaskqueue items — my original approach of making the queue itself pluggable wouldn't work because there are more than just JS functions in that queue.

@trevnorris
trevnorris Apr 28, 2016 Contributor

@chrisdickinson If we absolutely have to I'd prefer shimming native promises over not shipping this. We don't currently offer any type of support for Promises in this way. e.g. domains loosing context:

const domain = require('domain');
const print = process._rawDebug;
domain.create().run(() => Promise.resolve(1).then((val) => print(process.domain)));
// output: undefined

And TBH refusing this type of insight into node just because v8 doesn't provide us with more than a black box is a serious pain.

If, in fact, the CTC more unanimously decides that it's important that we wait for v8 to provide the appropriate APIs to accomplish this then I'd at least like to get approval for the EP so I can finish the implementation exposed via process.binding(). Then at least it's exposed in a way that can still be useful to many and will be ready once v8 provides what we need.

@chrisdickinson
chrisdickinson Apr 29, 2016

Right — this is a problem with domains as well, and I empathize with your desire to avoid making AsyncWrap dependent on V8 changes. However, without promise support, we can't make the sort of assertions we'd like to make — namely, being able to reliably back-associate any event with the event that queued it. This is somewhat less of an issue for domains, since domains and promises ostensibly both provide solutions for asynchronous error handling, and (leaving aside the domains-as-CLS use case) generally don't have much overlap.

On the upside, this is something we only have to solve once, and after that point any further microtask use from V8 is taken care of.

@jasnell
jasnell Apr 29, 2016 Member

Unless I overlooked it, something should be included about whether hooks are blocking or not. What happens, for instance, if I drop an endless loop into pre() for instance? Is there any risk to passing this to setImmediate() or process.nextTick() in the hook callback? That sort of thing.

@trevnorris
trevnorris Apr 29, 2016 Contributor

@jasnell as in whether async wrap callbacks run on the same thread? didn't think specifying that would be necessary, but can if you think so. as far as keeping a reference to this indefinitely, i'm in the middle of a PR to fix it so you can.

@jasnell
jasnell Apr 29, 2016 Member

I think the documentation should at least give a recommendation one way or the other as to whether the callbacks should be sync or async and what the possible ramifications are.

@mike-kaufman
mike-kaufman May 3, 2016

However, without promise support, we can't make the sort of assertions we'd like to make — namely, being able to reliably back-associate any event with the event that queued it.

+1.

@trevnorris
trevnorris May 5, 2016 Contributor

However, without promise support, we can't make the sort of assertions we'd like to make — namely, being able to reliably back-associate any event with the event that queued it.

This isn't completely accurate. It requires that we always drain the microtask queue in a specific way, and you will loose call stack information, but passing the correct parent id along is possible.

@jasnell jasnell and 3 others commented on an outdated diff Apr 29, 2016
XXX-asyncwrap-api.md
+The API presented below aims to accomplish this.
+
+## API
+
+### Overview
+
+```js
+// Standard way of requiring a module. Snake case follows core module
+// convention.
+const async_wrap = require('async_wrap');
+
+// List of asynchronous providers (e.g. CRYPTO or TCPWRAP). Can be used for
+// trace filtering of events.
+const providers = async_wrap.Providers;
+
+// init() is called during object construction. The object handle being
@jasnell
jasnell Apr 29, 2016 Member

Can you add a bit more up front about the over all design. Such as, what it a handle and what role does it play here? I know this isn't the API doc but it would help level set for anyone coming in and doing a review

@mike-kaufman
mike-kaufman May 3, 2016

It would be useful to describe the APIs in terms of the intended user model. For example, doesn't init() really mean enqueueAnAsyncCallback(). In the case of nextTick callbacks & promises, it doesn't correspond to an object being constructed, right?

@Fishrock123
Fishrock123 May 4, 2016 Member

enqueueAnAsyncCallback()

Pardon? This isn't actually where the callback gets called?

@mike-kaufman
mike-kaufman May 4, 2016

My understanding of the user model:

  • init(id, type, parentId) - invoked when an async callback as identified by id is eligible to be invoked. parentId is the async ID of the currently executing parent at the time init() is called.
  • pre(id) - invoked immediately before an async callback as identified by id is invoked
  • post(id, hasThrown) - invoked immediately after an async callback as identified by id has been invoked.
  • destroy(id) - invoked when an async callback as identified by id is no longer elligible to be called.

Above is brief wrt the "user model". It can be expanded on to provide more succinct definitions. E.g., an async callback is defined as a javascript function which is enqueued for execution at some later time. It is identified by a unique ID refered to as the async ID. It has the following lifecycle events: init() - ...

@trevnorris
trevnorris May 5, 2016 Contributor

For example, doesn't init() really mean enqueueAnAsyncCallback(). In the case of nextTick callbacks & promises, it doesn't correspond to an object being constructed, right?

Yes it does. It always does. It's always constructed at the time of call. You're confusing init() with something like when a callback is passed to the instances event handler, e.g. on('data' (which is the JS abstraction of the handle and has nothing to do with how the handle operates). The callback that will be called by MakeCallback is always defined at construction time. That will then trigger any callbacks from the user that have been abstracted via the JS API.

@Fishrock123
Fishrock123 May 5, 2016 Member

Does .created() make more sense?

@trevnorris
trevnorris May 5, 2016 Contributor

I used init as short for "initialized", which I believe well explains what's mechanically happening.

@jasnell
jasnell May 6, 2016 Member

init() should be fine as a name but as @mike-kaufman and I have indicated a few times, this would be much clearer if it included a bit more detail about the lifecycle of a hook. That's not a criticism, just a suggestion.

@jasnell jasnell and 3 others commented on an outdated diff Apr 29, 2016
XXX-asyncwrap-api.md
+
+// init() is called during object construction. The object handle being
+// constructed can be accessed as "this". Because init() is called during
+// construction it is not safe to blindly inspect the constructed object at the
+// moment. Though we are currently investigating how to make this safe.
+function init(id, type, parentId) { }
+
+// pre() is called just before the handle's callback is called. It can be
+// called 0-N times for handles (e.g. TCPWRAP), and should be called exactly 1
+// time for requests (e.g. FSREQWRAP).
+function pre(id) { }
+
+// post() is called just after the handle's callback has finished, and will be
+// called regardless whether the handle's callback threw. If the handle's
+// callback did throw then hasThrown will be true.
+function post(id, hasThrown) { }
@jasnell
jasnell Apr 29, 2016 Member

If hasThrown is true, is there any mechanism here for determining what the error was? Is there any intent for this to be used in any kind of recovery scenario? If not, that should be made explicit

@Fishrock123
Fishrock123 Apr 30, 2016 Member

Some notes that I asked trevor to outline should cover this, but you'd use process.on('unhandledException'), and you can register it safely in pre and cleanup in post without risking other code being caught.

@trevnorris
trevnorris May 5, 2016 Contributor

It's also useful if you want to see that someone is using domains or uncaughtException to handle errors. Rather than allowing the application to crash.

@jasnell
jasnell May 6, 2016 Member

Ok, but that's not quite what I asked. Inside post, I look and see that hasThrown is true. Then what? As a suggestion, when writing up the docs for this, it needs to be clear how this particular method relates to the actual handling of the error so that there's no confusion. People will see the hasThrown and will wonder.

@trevnorris
trevnorris Jun 21, 2016 Contributor

Ah, good point. Maybe instead we just pass the error object, if it occurred.

@rvagg
rvagg Jul 6, 2016 Member

is there resolution to this @trevnorris? seems to be a dangling artifact that needs to be fixed

@trevnorris
trevnorris Aug 9, 2016 Contributor

Going to remove it for now. Not a breaking change and can be added later in a minor version.

@jasnell jasnell and 3 others commented on an outdated diff Apr 29, 2016
XXX-asyncwrap-api.md
+// a JS object, destroy() will be triggered manually soon after post() has
+// completed. This is the only callback that does not receive the context as
+// "this". Because it is called while the associated class is being destructed
+// it is unsafe to operate on the object.
+function destroy(id) { }
+
+// Add a set of hooks to be called during the lifetime of asynchronous events.
+// createHook() returns an instance of the AsyncHook constructor that can
+// control how those hooks are used. For example, enabling or disabling their
+// execution.
+asyncHook = async_wrap.createHook({ init, pre, post, destroy });
+
+// Allow callbacks of this AsyncHook instance to fire. This is not an implicit
+// action after running createHook(), and must be explicitly run to being
+// executing callbacks.
+asyncHook.enable();
@jasnell
jasnell Apr 29, 2016 Member

Does this return a value? Perhaps a ref to the hook so calls can be chained?

@mhdawson
mhdawson May 4, 2016 Contributor

Did you consider if we'd want to enable selectively. ie be able to enable only for a subset of the supported providers ?

@Fishrock123
Fishrock123 May 5, 2016 Member

Is there a benefit do doing that here rather than in your hook handlers?

@trevnorris
trevnorris May 5, 2016 Contributor

@mhdawson That is coming in the future, but not spec'd for initial API. It would roughly look something like (mind you I'm changing API names on the fly, but you'll get what I mean):

const AsyncHook = require('async_wrap');
const pflags = AsyncHook.ProviderFlags;  // Providers by flag enum
const ah = new AsyncHook({ init, pre, post });
ah.filter(pflags.TCPWRAP | pflags.UDPWRAP | pflags.CRYPTO);
ah.enable();

The above will only trigger the callbacks for the three flags provided. There are a couple things ATM that need to be addressed:

  1. Currently stored in an enum, not by or-able flag. Easy fix. Though worried we may run out of room.
  2. Is there some type of propagation that should happen automatically under the hood? Otherwise the user is left with no stack traces, resource tracing, etc. Because of this I'm considering how useful this feature would be.
@mhdawson
mhdawson May 5, 2016 Contributor

Ok good to hear you have considered it and you have a good start at what it might look like.

@jasnell
jasnell May 6, 2016 edited Member

Granted that you said that filtering would come in the future so just take this with a grain of salt for now but please just put the filter flags on the constructor ;-)

const ah = new AsyncHook({init,pre,post}, flags.TCPWRAP | flags.UDPWRAP);
@jasnell jasnell and 4 others commented on an outdated diff Apr 29, 2016
XXX-asyncwrap-api.md
+Create a new `AsyncHook` instance that controls a given set of hooks. The four
+optional hooks are `init`/`pre`/`post`/`destroy`.
+
+#### `async_wrap.Providers`
+
+List of all providers that may trigger the `init` callback.
+
+### Constructor: `AsyncHook`
+
+The `AsyncHook` constructor returns an instance that contains information about
+the callbacks that are to fire during specific asynchronous events in the
+lifetime of the event loop. The focal point of these calls centers around the
+lifetime of `AsyncWrap`. These callbacks will also be called to emulate the
+lifetime of handles and requests that do not fit this model. For example,
+`HTTPPARSER` instances are recycled to improve performance. So the `destroy()`
+callback would be called manually after a connection is done using it, just
@jasnell
jasnell Apr 29, 2016 Member

Is there any reason to indicate that the handle is not actually being destroyed in cases such as HTTPPARSER? I can't think of any but want it to be clear.

@Fishrock123
Fishrock123 Apr 30, 2016 Member

Probably not, actual actions being done are a lot more interesting & useful than handles that are idley waiting from exterior events.

@Fishrock123
Fishrock123 Apr 30, 2016 Member

We could maybe add notes in the docs that some Providers actually use shared handles and these are then representative of individual events from those handles.

@jasnell
jasnell Apr 30, 2016 Member

I'd recommend it. My main concern would be someone either accidentally or unknowingly keeping one of the reusable handles around after destroy. Since the handle is not actually destroyed, you could end up with some odd, hard to debug edge cases. I'm not saying there's anything we should do other than simply document the fact.

@mhdawson
mhdawson May 2, 2016 Contributor

Would it be possible to make it so that even though the underlying resource is re-used, the same handle is not re-used ? That was avoid the problem James mentions above.

@trevnorris
trevnorris May 5, 2016 Contributor

Is there any reason to indicate that the handle is not actually being destroyed in cases such as HTTPPARSER? I can't think of any but want it to be clear.

There's no reason to. The resource will be assigned a new id and treated like a newly constructed object. We're just saving the hit of allocation and instantiation.

I'd recommend it. My main concern would be someone either accidentally or unknowingly keeping one of the reusable handles around after destroy.

You're thinking of the JS handle. We don't care about that. Internally we remove the pointer to the class from the JS object so it can no longer be referenced.

Would it be possible to make it so that even though the underlying resource is re-used, the same handle is not re-used ?

resource == handle.

@mhdawson
mhdawson May 5, 2016 Contributor

Ok, I think what you said is what I had in mind at least to some extent. There will be a new id even if the underlying resource is reused.

@jasnell
jasnell May 6, 2016 Member

Awesome, ok. Thank you.

@AndreasMadsen
AndreasMadsen May 10, 2016 Member

@trevnorris to be clear, the init hook will also be called manually, when the handle is reused?

@trevnorris
trevnorris Jun 21, 2016 Contributor

@AndreasMadsen Sorry, didn't see this. Yes, it will be manually called. When a resource is brought out of the pool it will be treated as if it were a new resource. All we're basically omitting is the memory allocation and class instantiation cost.

@jasnell jasnell commented on an outdated diff Apr 29, 2016
XXX-asyncwrap-api.md
+Key events in the lifetime of asynchronous events have been categorized into
+four areas. On instantiation, before/after the callback is called and when the
+instance is destructed. For cases where resources are reused, instantiation and
+destructor calls are emulated.
+
+#### `init(id, type, parentId)`
+
+* `id` {Number}
+* `type` {String}
+* `parentId` {Number}
+
+Called when a class is constructed that has the possibility to trigger an
+asynchronous event. This does mean the instance _will_ trigger a
+`pre()`/`post()` event. Only that the possibility exists.
+
+The `this` of each call is that of the object being constructed. While core
@jasnell
jasnell Apr 29, 2016 Member

Recommend repeating here the exception for destroy

@jasnell jasnell commented on an outdated diff Apr 29, 2016
XXX-asyncwrap-api.md
+// Pretend init, pre, post are all defined
+const asyncHook = async_wrap.createHook({ init, pre, post });
+asyncHook.enable();
+
+net.createServer((c) => {
+ // Only want to follow connections that match IP range.
+ if (ipRangeRegExp.test(c.address().address))
+ asyncHook.disable();
+}).listen(PORT);
+```
+
+At which point no further calls to the hooks on that instance will be made for
+that asynchronous branch.
+
+#### `asyncHook.scope()`
+
@jasnell
jasnell Apr 29, 2016 Member

An example illustrating this would be helpful

@jasnell jasnell commented on an outdated diff Apr 29, 2016
XXX-asyncwrap-api.md
+Disable the callbacks for a given `AsyncHook` instance. Doing this will prevent
+the `init()`, etc., calls from firing for any new roots of asynchronous call
+stacks, but will not prevent existing asynchronous call stacks that have
+already been captured by the `AsyncHook` instance from continuing to fire.
+
+While not part of the immediate development plan, it should be possible in the
+future to allow selective tracking of asynchronous call stacks. The following
+example demonstrates this:
+
+```js
+const async_wrap = require('async_wrap');
+const net = require('net');
+
+// Pretend init, pre, post are all defined
+const asyncHook = async_wrap.createHook({ init, pre, post });
+asyncHook.enable();
@jasnell
jasnell Apr 29, 2016 Member

If enable() returned the hook these could be chained

@jasnell jasnell and 1 other commented on an outdated diff Apr 29, 2016
XXX-asyncwrap-api.md
+The object returned from `require('async_wrap')`.
+
+#### `async_wrap.createHook(hooks)`
+
+* `hooks` {Object}
+* Return: {AsyncHook}
+
+Create a new `AsyncHook` instance that controls a given set of hooks. The four
+optional hooks are `init`/`pre`/`post`/`destroy`.
+
+#### `async_wrap.Providers`
+
+List of all providers that may trigger the `init` callback.
+
+### Constructor: `AsyncHook`
+
@jasnell
jasnell Apr 29, 2016 Member

API style nit: perhaps export either the Constructor or createHook function as the default export of the async_wrap module the way we do with events?

E.g. const createHook = require('async_wrap'), but based on whichever is expected to be the most used construction method.

@trevnorris
trevnorris May 5, 2016 Contributor

Maybe renaming the require'able to async_hook and have AsyncHook = require('async_hook') then can do new AsyncHook(...).

@jasnell jasnell commented on an outdated diff Apr 29, 2016
XXX-asyncwrap-api.md
+
+### `async_wrap`
+
+The object returned from `require('async_wrap')`.
+
+#### `async_wrap.createHook(hooks)`
+
+* `hooks` {Object}
+* Return: {AsyncHook}
+
+Create a new `AsyncHook` instance that controls a given set of hooks. The four
+optional hooks are `init`/`pre`/`post`/`destroy`.
+
+#### `async_wrap.Providers`
+
+List of all providers that may trigger the `init` callback.
@jasnell
jasnell Apr 29, 2016 Member

What data type? Array of strings? Array of objects?

@jasnell jasnell and 1 other commented on an outdated diff Apr 29, 2016
XXX-asyncwrap-api.md
+
+Disable the callbacks for a given `AsyncHook` instance. Doing this will prevent
+the `init()`, etc., calls from firing for any new roots of asynchronous call
+stacks, but will not prevent existing asynchronous call stacks that have
+already been captured by the `AsyncHook` instance from continuing to fire.
+
+While not part of the immediate development plan, it should be possible in the
+future to allow selective tracking of asynchronous call stacks. The following
+example demonstrates this:
+
+```js
+const async_wrap = require('async_wrap');
+const net = require('net');
+
+// Pretend init, pre, post are all defined
+const asyncHook = async_wrap.createHook({ init, pre, post });
@jasnell
jasnell Apr 29, 2016 Member

Is there a practical limit to how many of these should be created per process? What is the performance impact of creating too many of these?

@Fishrock123
Fishrock123 Apr 30, 2016 Member

As far as I know, almost entirely dependant on the work done from the hook than the hook itself.

@indutny
Member
indutny commented May 2, 2016

LGTM, except the mentioned nits.

@mhdawson
Contributor
mhdawson commented May 3, 2016

@trevnorris any chance to you have a branch somewhere with this API implemented that I could checkout and experiment with ?

@trevnorris
Contributor

@mhdawson Not completely. While writing this a few tweaks were added for API consistency. Most of it is implemented in process.binding('async_wrap'), but scope() and support for multiple listeners (i.e. it doesn't return a new instance when called) aren't there. Easiest way to see it in action is look at test-async-wrap*.

@mike-kaufman mike-kaufman and 3 others commented on an outdated diff May 3, 2016
XXX-asyncwrap-api.md
@@ -0,0 +1,309 @@
+| Title | AsyncWrap API |
+|--------|---------------|
+| Author | @trevnorris |
+| Status | DRAFT |
+| Date | 2016-04-05 |
+
+## Description
+
+Since its initial introduction with the `AsyncListener` API, `AsyncWrap` has
+slowly evolved to ensure a generalized API that would serve as a solid base for
+module authors who wished to add events to the life cycle of the event loop.
@mike-kaufman
mike-kaufman May 3, 2016

I think it would be worthwhile to describe these life-cycle events at a high level. That is, describe the user model for this API. A lot of what I read below is tied up with implementation details around the native handles. I think most users are concerned about when a callback was enqueued, when that callback started execution, and when that callback completed execution.

@trevnorris
trevnorris May 4, 2016 Contributor

the high level of node's event loop is already covered in detail in the-event-loop-timers-and-nexttick.md. i can reference that in the EP. also remember this isn't the actual docs entry. it's meant to be a high level overview of the change.

A lot of what I read below is tied up with implementation details around the native handles.

async wrap has always been partially tied to implementation details. that's the reason for its existence, and attempting to abstract that would remove the utility from the user. for example I was doing testing between 4 and 6 and realized that the order of events fires differently for certain operations. this is exactly the type of thing I wanted to know. it helps me debug my code. if we wanted all this abstracted away then we could simply wrap all the js calls. that's not the purpose for async wrap.

I think most users are concerned about when a callback was enqueued

It doesn't work like this. An oncomplete or similar is assigned to the newly created request upon construction. Hence why I chose to notify on construction of the instance

I think what you want is when the I/O request itself was created, which I believe the actual request instance to perform I/O is always created when requested by the user. So init() will handle that. the implementation is also simpler than injecting at all I/O calls. e.g. uv_write(), and since they're run at approximately the same time any counter measurements should be sufficiently correct.

Then pre() will be called when the async operation is complete, and post() will be called when the callback has completed execution. Covering your final points, and both of these are documented in the Hook Callbacks section.

As far as measuring fire-to-completion, it's impossible to do reliably in node. Best we can give is when node knows the request completed, but since all completed requests are queued by the kernel until node asks for them it's impossible to get perfect measurements. For example if 100 requests complete at the same time and each callback runs for 10 ms, we wouldn't know the last request completed until 1 second after it actually did. This is fundamentally part of node and not something we can do much about. Users will be able to track these details, and referring back to exposing implementation details of node this is exactly why it's important and not something that can be abstracted in a way to be useful.

@mike-kaufman
mike-kaufman May 5, 2016

Thanks Trevor for the link to the doc on the event loop - that's really helpful for me. If the goal of the API is to "add events to the life cycle of the event loop", then why isn't the event loop's phase exposed through API calls? Now, I'm not saying that it should be part of the API, but given stated goal, it isn't clear to why this is omitted.

Which is one of the things I'm driving at: Clarification/simplification around the goals API's goals will help get everyone on the same page around the utility of the API & how it is to be used. Ideally, this would include some definitions, goals/non-goals and some canonical use cases/examples.

async wrap has always been partially tied to implementation details. that's the reason for its existence, and attempting to abstract that would remove the utility from the user.

Can you expand on your example here? I'm not following how the description of the API impacts the ability to understand event ordering.

if we wanted all this abstracted away then we could simply wrap all the js calls. that's not the purpose for async wrap.

Again, precisely what I'm driving for - what is the purpose of the API? My understanding is the goal is to expose lifecycle events around async code execution. If not, then that's fine. If the goal is to provide lifecycle events around handles, then great, but it begs the question of "what is a handle and why does the user care?" (per @jasnell's comment).

It doesn't work like this. An oncomplete or similar is assigned to the newly created request upon construction. Hence why I chose to notify on construction of the instance

Sorry, I'm not following this.

I think what you want is...

I'm not following. We may be talking past each other with terminology. What I need is a notification model around async code execution in node. That is, I want to know precisely the following:

  • when is a callback c "made available" for invocation, and what is the parent invocation "making it available"?
  • when does a callback c begin invocation?
  • when does a callback c end invocation?
  • when is a callback c no longer available for invocation (arguably optional, but let's stick with it for now as it cleanly maps to proposed destroy() call).

As far as measuring fire-to-completion, it's impossible to do reliably in node.

I think this is OK, but again, it will help to explicitly list the goals & non-goals of the API. At a minimum it will get everyone reading this on the same page.

I'm happy to take a stab at writing up what I think is a user model for the API, and something that fits cleanly into current async wrap implementation. Likely not 100% correct, but at a minimum useful to highlight differences in understanding. Let me know if people think this is a useful effort.

@Fishrock123
Fishrock123 May 5, 2016 Member

but it begs the question of "what is a handle and why does the user care?" (per @jasnell's comment).

I think this has already been answered but if you don't know it may be more helpful to dig though node core while we're working out a potential API for it. We shouldn't expect 100% of things to be documented for end users in an EP. (see the other EPs)

@mike-kaufman
mike-kaufman May 5, 2016

I've been through the code. A clear definition of a "handle" and how it relates to the goals of the API aren't in the EP. Again, I'm happy to write this up and iterate on it.

@Fishrock123
Fishrock123 May 6, 2016 Member

I think that is probably out of scope. This exposes existing mechanics in a more usable way.

@jasnell
jasnell May 6, 2016 Member

I don't think telling someone to just go dig through node core while we're working out a potential API for it is really all that helpful (or friendly). @mike-kaufman appears to be making the point that while this new API appears useful it's not clear given the description in this eps exactly how it would be used and what benefit it brings. Several examples and a description of the lifecycle of a hook would make that much clearer. No one is asking for 100% of things to be documented for end users. What is being asked for is a bit more clarity... and I think a bit more patience would likely be a good thing also.

@Fishrock123
Fishrock123 May 6, 2016 Member

I still think defining exactly what a handle is, is out of scope here.

@mike-kaufman mike-kaufman and 3 others commented on an outdated diff May 3, 2016
XXX-asyncwrap-api.md
+const async_wrap = require('async_wrap');
+
+// List of asynchronous providers (e.g. CRYPTO or TCPWRAP). Can be used for
+// trace filtering of events.
+const providers = async_wrap.Providers;
+
+// init() is called during object construction. The object handle being
+// constructed can be accessed as "this". Because init() is called during
+// construction it is not safe to blindly inspect the constructed object at the
+// moment. Though we are currently investigating how to make this safe.
+function init(id, type, parentId) { }
+
+// pre() is called just before the handle's callback is called. It can be
+// called 0-N times for handles (e.g. TCPWRAP), and should be called exactly 1
+// time for requests (e.g. FSREQWRAP).
+function pre(id) { }
@mike-kaufman
mike-kaufman May 3, 2016

for debug purposes, I suggest passing along the type to all callbacks. There were some cases with current APIs where I wasn't observing init() getting called for some IDs, but was seeing pre()/post(). Having the type in pre()/post)() would help to debug situations like this.

@jasnell
jasnell May 3, 2016 Member

Agree. I can imagine that in some cases (such as logging) I may not want to have to maintain state to know what type of thing id references.

@Fishrock123
Fishrock123 May 4, 2016 Member

Might be easier to attach type to the handle?

@mike-kaufman
mike-kaufman May 4, 2016

Might be easier to attach type to the handle?

Not clear how that will work for the destroy() case where there is no handle. It may be worthwhile defining an async context type that can be passed to these events. Such a type can evolve to include appropriate information such as the type, handle-specific state, etc...

@mike-kaufman
mike-kaufman May 4, 2016

The other useful bit of information is the invocation count. i.e., in cases where pre()/post() can be called 0..n times, what will uniquely identify an invocation? I agree that this can be tracked by individually by the hooks, but it is part of the identity of an invocation of a callback. (i.e., an execution of a callback is uniquely identifed by the tuple [asyncId, invocationId])

@trevnorris
trevnorris May 4, 2016 Contributor

Not clear how that will work for the destroy() case where there is no handle

There's always a handle. Just not passed in destroy() to prevent accidental abort, but I'm currently working on fixing this so the instance can be safely accessed at any time.

Such a type can evolve to include appropriate information such as the type, handle-specific state, etc...

All this information already lives in the native layer. It would be far more cost effective and less complicated to query that information.

The other useful bit of information is the invocation count. i.e., in cases where pre()/post() can be called 0..n times, what will uniquely identify an invocation?

The invocation count can be easily enough tracked by the user by a combination of tracking each request id to each handle that created it, and then when pre() is called by the request. Those are mechanics that are too specific for the initial implementation. I don't understand what you mean by "uniquely identify an invocation".

I agree that this can be tracked by individually by the hooks, but it is part of the identity of an invocation of a callback. (i.e., an execution of a callback is uniquely identifed by the tuple [asyncId, invocationId])

The point of this API at this moment is to offer only that which can't be gained by any other means (with exception to .scope(), but added to prevent easily hazardous scenarios). I'm not sure what you mean by invocationId. If you simply mean a unique id for every call to MakeCallback() that would be the simplest piece of information to track in user land. A simple invocationId++; would do the trick.

Though I think what you may be asking for is assigning an invocationId in the init() area so it can be tracked and identified in the pre(). Is this correct?

@trevnorris
trevnorris May 4, 2016 Contributor

Might be easier to attach type to the handle?

User could do this themselves in init(). Would be easy enough to store as a class member variable and passed up on callback invocation. Can write that in if deemed necessary.

@trevnorris
trevnorris May 4, 2016 Contributor

There were some cases with current APIs where I wasn't observing init() getting called for some IDs, but was seeing pre()/post().

If you can repro this please let me know. I'd like to assess why that's happening sooner than later in case there's a lurking troll that'll make that one circumstance difficult.

@mike-kaufman
mike-kaufman May 5, 2016

I don't understand what you mean by "uniquely identify an invocation".

I'll try to be as precise as I can. lmk if this still isn't clear: There are two high-level concepts we're talking about here. We're discussing names, (e.g., handles vs requests). I'll use callback and callback invocation.

A callback is a "registration" of a javascript function that is to be called asynchronously at some later point during program execution. It may be invoked 0 to n times. A callback is uniquely identified by a 53-bit integer called an async ID. It has two lifecycle events - init() & destroy().

The other concept is a callback invocation, which is a specific invocation of a callback. A callback invocation has two lifecycle events pre() and post(). Each callback invocation is uniquely identified by the tuple [async_id, invocation_id], where invocation_id is simply a count of the number of invocations.

@trevnorris
trevnorris May 5, 2016 Contributor

Please don't use the term "callback". That complicates the existing meaning. Which are the callbacks attached to the handle. It would be more clear if you simply used the term "handle" for both a handle and request. The only time I differentiate the two is when there are important technical differences.

A callback is a "registration" of a javascript function that is to be called asynchronously at some later point during program execution.

This is technically inaccurate and confusing. Callbacks are never "registered". They are simply used by handles to notify when work has been done (e.g. new connection was made). Callbacks also don't contain state, which is an important part of the existence of a handle.

invocation_id is no necessary to keep internally. We should expose the callback type to the user. e.g. TCP can call either an onread or onconnection callback, but from there it's easy to track that count themselves with little overhead. while it would require a more than trivial change to core to be done in an acceptable way.

@mike-kaufman
mike-kaufman May 5, 2016

Sure, let's use "handle". How would you define "handle"? And what term would you use for an invocation of the callback associated with the "handle" and how would you define this?

I'm not claiming the invocation_id is necessary to keep internally. I am claiming that the invocation_id uniquely identifies a specific invocation of a method associated with a "handle".

@Fishrock123
Fishrock123 May 5, 2016 Member

@mike-kaufman Just make a Stacktrace?

How would you define "handle"?

C++ I/O handler

@mike-kaufman
mike-kaufman May 5, 2016

Just make a Stacktrace?

Sorry, I'm not following your suggestion.

C++ I/O handler

That's not the case for promises or nextTick() timers. Are there other outliers not represented by an c++ IO handler? Moreover, if the goal of the API is to provide "lifecycle events around async code execution", defining a "handle" as a C++ I/O handler doesn't relate to the goals of the API.

@Fishrock123
Fishrock123 May 6, 2016 edited Member

Sorry, I'm not following your suggestion.

Literally just check a strack trace, Besides, that is the only way to know what you are asking for.

That's not the case for promises or nextTick() timers.

Fine. "I/O Handler". Next.

@jasnell
jasnell May 6, 2016 Member

Fine. "I/O Handler". Next.

Again, this isn't helpful. @Fishrock123 you have the advantage of being far more familiar with the Node.js internals and therefore make certain assumptions about what should just be "known". The questions that @mike-kaufman is asking are not unreasonable and are the kinds of questions that anyone trying to use this API would be asking. Being flippant doesn't address the concern. Can I ask you to please be more patient.

@mike-kaufman mike-kaufman and 3 others commented on an outdated diff May 3, 2016
XXX-asyncwrap-api.md
+// Standard way of requiring a module. Snake case follows core module
+// convention.
+const async_wrap = require('async_wrap');
+
+// List of asynchronous providers (e.g. CRYPTO or TCPWRAP). Can be used for
+// trace filtering of events.
+const providers = async_wrap.Providers;
+
+// init() is called during object construction. The object handle being
+// constructed can be accessed as "this". Because init() is called during
+// construction it is not safe to blindly inspect the constructed object at the
+// moment. Though we are currently investigating how to make this safe.
+function init(id, type, parentId) { }
+
+// pre() is called just before the handle's callback is called. It can be
+// called 0-N times for handles (e.g. TCPWRAP), and should be called exactly 1
@mike-kaufman
mike-kaufman May 3, 2016

Also, I don't understand the distinction between "handles" and "requests" and why this impacts the expected number of calls. This is an example of where the implementation details are bleeding through the API.

@Fishrock123
Fishrock123 May 4, 2016 Member

Which will happen regardless due to how close this API is to tracking what actually happens... which is the point.

Some clarification could perhaps be used though.

@mike-kaufman
mike-kaufman May 4, 2016

Which will happen regardless due to how close this API is to tracking what actually happens... which is the point.

Is this what actually happens when you consider promises & next tick callbacks? In these cases, there is no "handle", right? There is no "object" being constructed during init().

My point is, if the goal of the API is to provide life cycle events around async code execution, then the API should be described in terms of life cycle events of async code execution. I believe that getting to a crisp definition of these events will help ensure API consistency across different types of handles, and help drive correct semantics for next tick & promise callbacks.

@chrisdickinson
chrisdickinson May 4, 2016

With promises there is a handle (or maybe a "request" is a better term), in the form of the microtask queue entry — which does not correspond to the promise, but to a call to resolve/reject() (if there are downstream promises dependent on an upstream promise-yet-to-be-settled) or .then (if the upstream promise is already resolved, but a new dependent promise is introduced.)

@trevnorris
trevnorris May 4, 2016 Contributor

It's not an implementation detail. It's a concrete detail in how node works. handles do work via requests. e.g. when a TCP server handle receives a new connection, it receives a new connection request. which is then converted into a handle that can receive requests like new data packet. this has an important distinction when wanting to track metrics and resources.

and just because an object isn't an instance of ReqWrap doesn't mean it doesn't fit in this model. hypothetically all async activity could be made a subclass of HandleWrap or ReqWrap, but we don't for performance reasons.

There is no "object" being constructed during init().

TickObject() is constructed for every call to nextTick(). Promise has an internal construction mechanism. Promises construct a new object. The ECMA spec shows that each new promise has several internal fields set for each newly constructed instance.

But technically they would each be a request. They only trigger once (Promises will fire on multiple provided resolve/reject callbacks, but only once) and they themselves will never hold references to another request.

@chrisdickinson
chrisdickinson May 6, 2016 edited

But technically they would each be a request. They only trigger once (Promises will fire on multiple provided resolve/reject callbacks, but only once) and they themselves will never hold references to another request.

Just to be sure we're all on the same page about promises-as-an-async-resource: the asynchronous component of a promise is not tied to Promise object creation, but rather through the addition of a handler (for a resolved settled promise) or the resolution of a value (for a pending promise with existing handlers.) The asynchronous resource is the microtask — not the promise. In node handle parlance, a promise is a factory for creating requests, not a request in and of itself.

@mike-kaufman mike-kaufman and 3 others commented on an outdated diff May 3, 2016
XXX-asyncwrap-api.md
+// callback did throw then hasThrown will be true.
+function post(id, hasThrown) { }
+
+// destroy() is called when an AsyncWrap instance is destroyed. In cases like
+// HTTPPARSER where the resource is reused, or timers where the handle is only
+// a JS object, destroy() will be triggered manually soon after post() has
+// completed. This is the only callback that does not receive the context as
+// "this". Because it is called while the associated class is being destructed
+// it is unsafe to operate on the object.
+function destroy(id) { }
+
+// Add a set of hooks to be called during the lifetime of asynchronous events.
+// createHook() returns an instance of the AsyncHook constructor that can
+// control how those hooks are used. For example, enabling or disabling their
+// execution.
+asyncHook = async_wrap.createHook({ init, pre, post, destroy });
@mike-kaufman
mike-kaufman May 3, 2016

I would expect a corresponding unhook() call to deregister a set of callbacks.

@jasnell
jasnell May 3, 2016 Member

Yes, I was wondering about this also. It appears that with this API there is no way to explicitly remove a hook. Clear documentation about the lifecycle of a hook would be fantastic.

@Fishrock123
Fishrock123 May 4, 2016 Member

Maybe better as new Hook()? As far as I understand it doesn't actually attach until you enable() it, so the counter is disable().

@mike-kaufman
mike-kaufman May 4, 2016

My understanding is adding a "hook" instance will put it in the list of async hooks. On each callback, this list needs to be iterated, and for each hook, test if enabled & if enabled, then invoke the hook. So, a remove() call is different from disable(); it explicitly removes the hook from the set of async hooks. Let me know if my understanding is incorrect.

@trevnorris
trevnorris May 4, 2016 Contributor

disable() is a flag that toggles which set of callbacks need to be called for that instance. so if there's only one instance and it's disabled then the normal checks won't anything needs to be called.

since even after a hook has been disabled, it will continue to receive notifications on async paths already taken (100% intentional design). if you want an api that removes even those calls then that comes with the caveat that resource cleanup is then on the user I can think of a way to make that possible.

Maybe better as new Hook()?

I could live with that.

@mike-kaufman
mike-kaufman May 5, 2016

If I understand correctly, desired behavior is that if init() fires, then all callbacks fire for that handle instance. An explicit remove() will break this assertion.

@trevnorris
trevnorris May 5, 2016 Contributor

so basically running hooks.remove() means that those callbacks will never fire again. even if they've propagated though other asynchronous there are a couple technical difficulties there that'll have to be worked out, but probably possible to implement efficiently.

@mike-kaufman mike-kaufman and 3 others commented on an outdated diff May 3, 2016
XXX-asyncwrap-api.md
+asyncHook = async_wrap.createHook({ init, pre, post, destroy });
+
+// Allow callbacks of this AsyncHook instance to fire. This is not an implicit
+// action after running createHook(), and must be explicitly run to being
+// executing callbacks.
+asyncHook.enable();
+
+// Disable listening for new asynchronous events. Though this will not prevent
+// callbacks from firing on asynchronous chains that have already run within
+// the scope of an enabled AsyncHook instance.
+asyncHook.disable();
+
+// Enable hooks for the current synchronous execution scope. This will ensure
+// the hooks are not in effect in case of multiple returns, or if an exception
+// is thrown.
+asyncHook.scope();
@mike-kaufman
mike-kaufman May 3, 2016

I'm not following the use of the scope() function. Can you elaborate on this?

@trevnorris
trevnorris May 4, 2016 Contributor

Running the following:

hook.enable();
net.createServer(fn).listen(8080);
hook.disable();

will allow hook's callback to continue firing on all callbacks that are triggered by the new Server instance. Though this can be error prone if there are multiple return locations. e.g.

setTimeout(() => {
  hooks.enable();
  if (checkState())
    return;
  fs.open(path, r, fn);
  hooks.disable();
}, 100);

Because of the early return and not first running hooks.disable() that set of hooks could remain enabled indefinitely. So hooks.scope() would guarantee that they are disabled when the current stack unwinded. e.g.

setTimeout(() => {
  hooks.scope();
  if (checkState())
    return;
  fs.open(path, r, fn);
}, 100);

I'll probably end up needing to place in massive bold letters that it lasts until the current stack has unwound, or else users will think it's function scope specific. Like const or let.

@mike-kaufman
mike-kaufman May 5, 2016

Someone could achieve analogous functionality with a try/finally block, right?

@trevnorris
trevnorris May 5, 2016 Contributor

@mike-kaufman I don't see how those two address the same point.

@mike-kaufman
mike-kaufman May 5, 2016

If I understand scope() properly, this

() => {
  hooks.scope();
  if (checkState())
    return;
  fs.open(path, r, fn);
}

is equivalent to this:

() => {
  try {
    hooks.enable();
    if (checkState())
      return;
    fs.open(path, r, fn);
  }
  finally {
    hooks.disable();
  }
}
@jasnell
jasnell May 6, 2016 Member

Based on the description that would be my understanding as well @mike-kaufman ... only that try/finally bring along it's own additional cost.

At the risk of repainting the bike shed, my key concern with scope() is with the name -- it's not clear at all and @trevnorris has already indicated that there could be some confusion around what scope it's actually referring to. Not sure if I have a better suggestion tho. I know it's ugly (and don't have a better suggestion) but something like hook.forThisStack() would be clearer but admittedly is a horrible name.

@trevnorris
trevnorris Jun 21, 2016 Contributor

@jasnell What about .syncScope() or .stackScoped()?

@rvagg
rvagg Jul 6, 2016 Member

I like .syncScope() for this, it adds a lot more clarity to what it's trying to do

@mike-kaufman mike-kaufman commented on an outdated diff May 3, 2016
XXX-asyncwrap-api.md
+Resources like `HTTPParser` are reused throughout the lifetime of the
+application. This means node will have to synthesize the `init()` and
+`destroy()` calls. Also the id on the class instance will need to be changed
+every time the resource is acquired for use.
+
+For shared resources like `TimerWrap` this is not necessary since there is a
+unique JS handle that will contain the unique id necessary for the calls.
+
+
+## Notes
+
+### `process.nextTick()` and Promises
+
+Execution of the `nextTickQueue` and `MicrotaskQueue` are slightly special
+asynchronous cases. Because they execute in the same `MakeCallback()` as the
+asynchronous callback they will ultimately end up with the same parent as the
@mike-kaufman
mike-kaufman May 3, 2016

Because they execute in the same MakeCallback() as the asynchronous callback they will ultimately end up with the same parent as the originating call

IMO this needs to be addressed. Specifically, nextTick() & promises need to maintain correct semantics wrt their async parent and wrt the callbacks that are being fired.

@mhdawson
Contributor
mhdawson commented May 4, 2016 edited

My biggest question after reading this and doing some tests with process.binding('async_wrap'); is:

don't we have to define what you can/cannot assume in terms of the object passed as 'this' ?

I'm thinking there needs to be a mapping between the providers, their callbacks and what object you can expect 'this' to be in each case. If that's true then I start to wonder about how much of the internals we'll be exposing and how that might constrain what we change in the future. Key would be to document what will or won't change across releases in terms of what you get for 'this' and the shape of those objects.

As a concrete example for:

crypto.randomBytes(): this is -> InternalFieldObject {
  ondone:
   { [Function]
     [length]: 0,
     [name]: '',
     [arguments]: null,
     [caller]: null,
     [prototype]: { [constructor]: [Circular] } } }
crypto.pbkdf2() this is -> InternalFieldObject { ondone: { [Function] [length]: 2, [name]: '' } }

and its not clear how I figure out from what's being passed to the hooks how you figure out which specific hook triggered the callback.

Maybe more than is currently encoded in the private api is going to be encoded into type in the init call, currently it just looks like the provider. If the type will help to identify the specific callback per provider then defining what will be in type and the values would help.

@mike-kaufman

Per @mhdawson's comments, are there specific reasons why the actual handle object is being passed to the hooks? I am also concerned about the level of internal details being exposed here. Would be nice to understand the use cases for this.

@trevnorris
Contributor

I'm thinking there needs to be a mapping between the providers, their callbacks and what object you can expect 'this' to be in each case.

I'm fine with the idea that each constructor receives its own unique provider id. Not sure what you mean by "their callbacks", and the expected this would be an instance of the specified provider.

If that's true then I start to wonder about how much of the internals we'll be exposing and how that might constrain what we change in the future.

All of this is already exposed via ._handle on pretty much everything. For most (maybe all) handles you can access the user's constructed instance via this.owner. Reason I'm not passing that by default is because it doesn't always exist, and always passing the handle attached to the C++ class instance is more consistent.

As for what we can change, there's no guarantee what fields will be available. Part of the initial concept of this API was users who wanted to know what node was doing. Not have just another abstracted API, that could be done easily enough in another way. I'm sure there'll be disagreement about what we should be able to rely on once a branch has reached stable, but that was never part of the initial design plan. It's basically "accessing this is equivalent to accessing _handle, and as such there's no guarantee to what fields are available". The one possible exception is that .owner is always made available so if it exists then users can get access to the JS object instance.

Re: InternalFieldObject That can be easily enough changed so every constructor has their own provider id. As explained above.

are there specific reasons why the actual handle object is being passed to the hooks? I am also concerned about the level of internal details being exposed here. Would be nice to understand the use cases for this.

Some users want to store information directly on the handle. Despite the id each has, it's the easiest way to propagate information and allow the GC to clean it up automatically. Ideally in the future there could be a basic set of calls that could be standardized (e.g. .providerType()), and this isn't information that isn't now available. e.g.

'use strict';
const async_wrap = process.binding('async_wrap');
const print = process._rawDebug;
var handle;
async_wrap.setupHooks({ init() { handle = this } });
async_wrap.enable();
var server = require('net').createServer().listen(8080);
print(server._handle === handle);
server.close();
// output: true

I use it for debugging as well. With the understanding that things change, that's part of its utility. By addition of the id, explicitly passing the provider, etc. we're not forcing use of the handle on anyone. Simply making it available in a way that makes sense for the context of the call, and in a way that users like APMs will find very useful.

@mike-kaufman

Some users want to store information directly on the handle. Despite the id each has, it's the easiest way to propagate information and allow the GC to clean it up automatically.

Providing storage for the async context is different than exposing the handle though.

I use it for debugging as well.

IMO, I think providing a context object which has a consistent shape & properties like provider type and handle is a cleaner API than passing the handle directly. It still meets the criteria of providing arbitrary storage associated with the "handle", it provides a place to define a common interface across handles, and it can evolve independently of the underlying handle.

Simply making it available in a way that makes sense for the context of the call, and in a way that users like APMs will find very useful.

I'm still not following how APMs will utilize the handle. Is there specfiic data on the handle that is useful? If so, what is this?

@trevnorris
Contributor

Providing storage for the async context is different than exposing the handle though.

Creating and tracking a new async context for every handle, and tracking it, is expensive. By attaching properties directly to the handle instance GC will take care of it all automatically, and at the least expense.

I think providing a context object which has a consistent shape & properties like provider type and handle is a cleaner API than passing the handle directly.

This can be, or at least should be, construct-able by the user. Creating all these new objects filled with properties is expensive, and you're missing that printing the actual contents of the handle is useful. And I don't share the concern about possibly needing to standardize properties in the handle and making it difficult for node to move forward. I've been aiming for a more standardized lower-level API, and "hiding" properties on an object in a significant way has become easier with ES6. But this is a separate topic.

I'm still not following how APMs will utilize the handle. Is there specfiic data on the handle that is useful? If so, what is this?

Here's a really basic example script that should explain how useful it is to be able to see the handles themselves while debugging:

'use strict';
const async_wrap = process.binding('async_wrap');
const print = process._rawDebug;
const ctx_array = [];

async_wrap.setupHooks({
  init() { /*print(this)*/ },
  pre() {
    if (ctx_array.indexOf(this) === -1) {
      ctx_array.push(this);
      print(this);
    }
  },
});
async_wrap.enable();

process.on('exit', () => print(ctx_array.length));

require('net').createServer(function(c) {
  require('fs').readFile(__filename, () => {
    c.end(new Buffer(1024 * 1024 * 100).fill('a'));
    this.close();
  });
}).listen(8080);

require('net').connect(8080, function() { this.resume() });

In there you'll see a WriteWrap which encapsulates the writing of the data from server to client, and gives access to the buffer being written. Useful for inspecting all TCP packets going through the server. Also the GetAddrInfoReqWrap which indicates there was a dns lookup for a host. Which is available under hostname. Or the TCPConnectWrap which gives you information about the remote server attempting to connect. Or the ShutdownWrap that alerts us that the connection is closing. The FSReqWrap is useful that we can see the contents of the file that's been read in, and even the position of the file that was read.

I hope this demonstrates the utility for being able to analyse each handle. All of the things mentioned in the previous paragraph cannot be obtained any other way. Removing the ability to see the handle would be a blow to the API, and basically be one step towards moving it to nothing more than a continuation-local-storage API.

@mhdawson
Contributor
mhdawson commented May 6, 2016 edited

@trevnorris what I was referring to in respect to "their callbacks" was that for the CRYPTO provider there are multiple cases were callbacks are wrapped such that they pre, post methods are invoked (as in my example). I think your comment about making each of these have their own provider id addresses that question.

I terms of the discussion about visibility of the handles, from what you describe we should document both in this eps what it's ok/not ok to use the handlers for and what expectations are. For example:

  • it is ok to store data in the handle by adding fields, but it is your responsibility to ensure that the namespace is unique enough that the names will not collide with any additions made in future Node.js versions
  • you may choose to inspect the contents of the handle, however, these are not part of the public API and will change between releases.
  • The list of providers may change from release to release, it is up to your code to handle any additions/deletions in a graceful manner.

If we believe that documenting a list like this is enough protection from being boxed in later when users of the API are broken by later Node.js releases and complain, then passing the handles would be fine. If we were concerned that despite the warnings we'd still be trying to avoid breakage passing some other field from wrapper could make sense.

@jasnell
Member
jasnell commented May 6, 2016 edited

@trevnorris a couple more clarifying questions ...

Let's say I create a hook and some dependency module I'm using creates a hook... when those are called, are they passed the same id and handle, different id's same handle, same id's different handle or different ids and different handles? (and by handle here I mean the js object that wraps the actual handle). The main reason I ask is that if I'm attaching additional context to the handle, it would be helpful to also know that other hooks could be attaching their own context to the same handle.

I'm still wondering about the potential cost of creating too many of these which is why I think describing the specific lifecycle from when a hook is created to when it is destroyed would be very helpful. While I understand that you've designed and implemented this to be as low impact on performance as possible, there is a non-zero cost to calling these hooks. Have you had the opportunity yet to benchmark an upper limit to the number of hooks that can be created without having a serious impact on performance? My key concern with this is that an app developer may not have any idea that dependency modules they may be using could be going out and creating hooks. Depending on how many such dependencies they have, they could end up seeing degraded performance without any clear indication as to why since installing the hook appears to be a completely transparent operation (that is, there's no indication that a new hook was created).

@Fishrock123
Member

Let's say I create a hook and some dependency module I'm using creates a hook... when those are called, are they passed the same id and handle, different id's same handle, same id's different handle or different ids and different handles? (and by handle here I mean the js object that wraps the actual handle).

imo they'd have to be the same in order to work well with potential libraries and modules.

@Fishrock123
Member
Fishrock123 commented May 6, 2016 edited

Possible alternate exposed API:

(based on naming feedback and my own mulling over it)

const AsyncHook = require('async-hook')

const hook = new AsyncHook({ create, before, after, destroy })

hook.enable()
hook.disable()
hook.withinStack() // or something

Not this doesn't really consider that it may be useful to have things other than the AsyncHook constructor in the future, so perhaps the following is better (and probably also better semantically):

const AsyncHook = require('async_wrap').AsyncHook

const hook = new AsyncHook({ create, before, after, destroy })
@joshgav joshgav referenced this pull request in nodejs/diagnostics May 9, 2016
Closed

Expanding this WG to all Diagnostics for Node #46

@AndreasMadsen AndreasMadsen and 2 others commented on an outdated diff May 10, 2016
XXX-asyncwrap-api.md
+not intuitive in how the asynchronous chain would propagate. So instead make
+the client the active id for the duration of the `'connection'` callback.
+
+### Reused Resources
+
+Resources like `HTTPParser` are reused throughout the lifetime of the
+application. This means node will have to synthesize the `init()` and
+`destroy()` calls. Also the id on the class instance will need to be changed
+every time the resource is acquired for use.
+
+For shared resources like `TimerWrap` this is not necessary since there is a
+unique JS handle that will contain the unique id necessary for the calls.
+
+
+## Notes
+
@AndreasMadsen
AndreasMadsen May 10, 2016 Member

@trevnorris can you add a note about how native addon modules do/don't break async_wrap?

If a native addon just call MakeCallback, there will be no way for the init hook to be called at the correct time. If they should inherit from an C++ AsyncWrap class, that is likely a big change.

@trevnorris
trevnorris May 17, 2016 Contributor

Can do. Hadn't added it yet b/c don't have a good solution for native modules yet.

@AndreasMadsen
AndreasMadsen Jun 20, 2016 Member

@trevnorris don't forget this.

@trevnorris
trevnorris Jun 21, 2016 Contributor

Actually think I just came up with a potential way to fix this on both the native and JS side. Will also fix the batched calls problem. Will write this up.

@AndreasMadsen
AndreasMadsen Jul 6, 2016 Member

Sounds amazing.

@rvagg
rvagg Jul 6, 2016 Member

Need this information @trevnorris, whatever magic you have here. Also, we could work with NAN to make this easier, perhaps even by providing automatic support for existing users if that makes sense (i.e. MakeCallback is already heavily wrapped by NAN now so additional things could be put into it if possible).

@trevnorris
Contributor

First, I've found a flaw in the design and now no handle will be passed automatically. Instead there'll be a sort of getHandleById(id) that'll return the handle of id. There'll be more details in the EP when I update it soon.

@jasnell

when those are called, are they passed the same id and handle, different id's same handle, same id's different handle or different ids and different handles?

Same id and handle. Can outline in the final docs these implications, but this EP is already going far beyond what an EP is intended to cover.

I'm still wondering about the potential cost of creating too many of these

By "these" do you mean AsyncHooks? The only added cost is running the callbacks. My design has been geared to remove performance impact of tracking handles and the callbacks.

which is why I think describing the specific lifecycle from when a hook is created to when it is destroyed would be very helpful.

I'm confused. A hook's lifecycle is that like any other object. Except that node will maintain a reference to it, so if the library looses its reference then it'll cause an object leak.

there is a non-zero cost to calling these hooks. Have you had the opportunity yet to benchmark an upper limit to the number of hooks that can be created without having a serious impact on performance?

That completely depends on how much work each hooks do, and which hooks are hooked into. If all you want to do is store a tree of async calls, it's really cheap. if you want to look at application state, probably a bit more.

My key concern with this is that an app developer may not have any idea that dependency modules they may be using could be going out and creating hooks.

Good point. We can just add an API to show how many are registered, or some such. Transparency FTW.

@Fishrock123 The later of the two is what I'm thinking.

@rvagg rvagg referenced this pull request in nodejs/CTC Jun 9, 2016
Closed

Zones language proposal and Node.js #4

@trevnorris
Contributor

@rvagg I've rewritten or reworded almost the entire thing. If you wouldn't mind giving it a look at your convenience.

@chrisdickinson

With async/await coming (and Promise already exposed) I'm not super comfortable waiting to support Promises until after we release AsyncHook.

@ofrobots What do you think the feasibility is on getting OnMicrotaskEnqueued / BeforeMicrotask / AfterMicrotask into V8? Or something like a WrapEnqueuedJSMicrotask that we could use to register the new async source & return a wrapped version that fires before/after callbacks?

@ofrobots

@chrisdickinson I had a good discussion on microtask visibility with @jeisigner. He's looking at ways of supporting the use-case through the V8 API.

@trevnorris
Contributor

@chrisdickinson Regardless I would like to get approval on the API in general so that implementation can proceed along side getting Promise support.

@jasnell jasnell and 2 others commented on an outdated diff Jun 20, 2016
XXX-asyncwrap-api.md
+## Notes
+
+### Promises
+
+Node currently doesn't have sufficient API to notify calls to Promise
+callbacks. In order to do so node would have to override the native
+implementation.
+
+
+### Immediate Write Without Request
+
+When data is written through `StreamWrap` node first attempts to write as much
+to the kernel as possible. If all the data can be flushed to the kernel then
+the function exists without creating a `WriteWrap` and calls the user's callback
+in `nextTick()`. Meaning the user would never be notified of data being written
+because the entire operation was synchronous. Is this problematic?
@jasnell
jasnell Jun 20, 2016 Member

For most cases I don't believe this should be problematic but I could see it causing confusion in some odd edge cases. I'm wondering if it would be possible to have a hook specific for these kinds of cases (do we have any other instances in core where this kind of thing could happen?).

@trevnorris
trevnorris Jun 21, 2016 Contributor

The only case I've found this is usage of uv_try_write, which is only used by StreamWrap. Which includes TCPWrap, PipeWrap and TTYWrap.

@rvagg
rvagg Jul 6, 2016 Member

Remove "Is this problematic?" and just go with it. I can see this as an area for future improvement if we come across use-cases where it is actually problematic. Without a clear idea of those, for now we can just let it be and make sure the documentation contains this caveat.

@jasnell
Member
jasnell commented Jun 20, 2016

Definitely better. I'm certainly +1 on this.

@mhdawson mhdawson and 2 others commented on an outdated diff Jun 20, 2016
XXX-asyncwrap-api.md
+
+
+#### `async_wrap.subsystems`
+
+List of all subsystems that may trigger the `init` callback. These subsystems
+group relevant calls together. For example the `CRYPTO` provider encompasses
+all calls from the crypto library that only creates a request. Purposes of this
+is to allow quick filtering or reporting of incoming requests.
+
+**Note(trevnorris):** "provider" was originally intended to be "subsystem" then
+later essentially turned into the class name, unless the class being
+instantiated inherited directly from `AsyncWrap`. so should subsystems simply be
+changed to include the name of every class constructor? or should it properly
+group calls together, such as `TCPWRAP` and `TCPCONNECTWRAP` into a single TCP
+provider? or possibly both.
+
@mhdawson
mhdawson Jun 20, 2016 Contributor

I'm thinking its useful to know which specific one it is in init(id, type, parentId) so if provider ends up being what is passed for 'type' I think there should be one for each class.

@trevnorris
trevnorris Jun 21, 2016 Contributor

My notes are poorly conveyed. What I meant was simply the name "subsystem" seemed more appropriate for this case. The same values that were planned to be passed still will be. Only that the name of the variable in the documentation and property on the async_wrap object will change.

@rvagg
rvagg Jul 6, 2016 Member

at this stage, without further feedback, you should just remove this paragraph and assert one or the other. "subsystem" seems good enough to me but neither option seems more clear than the other so just go with what you think is appropriate since you've been poking at this for so long and have a better feel for the terminology that makes sense. If you go with "subsystem", remove the remaining instance of "provider" in the doc (para above).

@mhdawson
Contributor
mhdawson commented Jun 20, 2016 edited

I agree the description is looking quite clear and complete now + 1from me as well. Just added one comment to one of the embedded questions.

@mike-kaufman mike-kaufman commented on the diff Jun 20, 2016
XXX-asyncwrap-api.md
+asyncHook.enable();
+
+net.createServer((c) => {
+ // Only want to follow connections that match IP range.
+ if (ipRangeRegExp.test(c.address().address))
+ asyncHook.disable();
+}).listen(PORT);
+```
+
+At which point no further calls to the hooks on that instance will be made for
+that asynchronous branch.
+
+
+### Hook Callbacks
+
+Key events in the lifetime of asynchronous events have been categorized into
@mike-kaufman
mike-kaufman Jun 20, 2016

Given that you've defined handle/request above, I think it would be beneficial to describe the API in those terms consistently. Most importantly is with the naming of init()/pre()/post()/destroy() be changed to indicate it's applicability on the handle/request. E.g., names like initHandle()/destoryHandle()/preRequest()/postRequest() reinforce the mental model of the API, and IMO result in an API that is more self-descriptive. My .02. :)

@trevnorris
trevnorris Jun 21, 2016 Contributor

While I'm not opposed to the concept, that approach would mean passing 8 callbacks. How about instead we explicitly pass whether the call is for a handle/request to the init() callback?

@mike-kaufman
mike-kaufman Jun 21, 2016

Sorry, not sure why you say 8 callbacks? I'm suggesting the callbacks be renamed to reinforce the conceptual model of the API. Specifically,

  • init() be renamed initHandle()
  • destroy() be renamed to destroyHandle()
  • pre() be renamed to preRequest()
  • post() be renamed to postRequest()
@trevnorris
trevnorris Jun 21, 2016 Contributor

All four fire for both both handles and requests. I'm not sure what conceptual model would be reinforced by these names.

@mike-kaufman
mike-kaufman Jun 23, 2016

My bad. I was under the impression that you wanted to use the term Request to refer to an invocation of a handle's callback, but I guess a Request is a special type of Handle. This wasn't immediately clear to me based on descriptions above, or descriptions of the API callbacks.

@mike-kaufman mike-kaufman and 3 others commented on an outdated diff Jun 20, 2016
XXX-asyncwrap-api.md
+### net Client connection Event
+
+Technically the `pre()`/`post()` events of the `'connection'` event for
+`net.Server` would place the server as the active id. Problem is that this is
+not intuitive in how the asynchronous chain would propagate. So instead make
+the client the active id for the duration of the `'connection'` callback.
+
+
+### Reused Resources
+
+Resources like `HTTPParser` are reused throughout the lifetime of the
+application. This means node will have to synthesize the `init()` and
+`destroy()` calls. Also the id on the class instance will need to be changed
+every time the resource is acquired for use.
+
+For shared resources like `TimerWrap` this is not necessary since there is a
@mike-kaufman
mike-kaufman Jun 20, 2016

I'm not following the explanation around TimerWrap. Take a look at this sample code. Running on node 6.2.1, it shows only one handle being constructed and two "requests".

In particular, how does one distinguish timer functions that are in separate branches of the "logical async call tree"? In this example, I expect that t1a's parent is t1 and t2a's parent is t2.

@Fishrock123
Fishrock123 Jun 20, 2016 Member

Unsure. Timers pool handles for efficiency, but I think @trevnorris had a thing for tracking individual timers.

@trevnorris
trevnorris Jun 21, 2016 Contributor

My description totally sucks (note to self to elaborate on this). What it means is that the parent of the timer instance, which is what we care about in this case, will be linked to the calling resource. AsyncWrap isn't meant to show retainers (i.e. resources that keep other resources alive). It shows what handles are responsible for creating other handles.

In the TimerWrap case the construction of a TimerWrap is simply a means of storage for the JS timer instance. Conceptually the JS timer instance is what the calling code actually wanted. So each JS timer instance will also be assigned it's own unique id, just like all the native classes.

@rvagg
rvagg Jul 6, 2016 Member

still need to translate the clarification above into the text in the doc to deal with the confusion

@trevnorris
Contributor

@Fishrock123 Yeah yeah. I'm going to change it to before()/after(). :P When I saw my own documentation realized the naming was off.

On instantiation, before/after the callback [...]

@mike-kaufman
mike-kaufman commented Jun 22, 2016 edited

[Edit 6/23 - fixed formatting, updated point 4, clarified some wording]
[Edit 6/24 - updated tree definition based on comments from @trevnorris]

The thing I’m struggling with here is most of the use cases of this cited herein (lines 12-15, including async stack traces, continuation storage and profiling) all require an understanding of the async call tree. Yet throughout the entire proposal of the API, the async call tree is never mentioned. I'm left to infer what this tree is supposed is look like, and how it is constructed from the proposed API, and if such a tree actually solves the intended use cases.

To test my understanding, I’ll define the Async Call Tree as follows:

  • The Async Call Tree is a tree with 3 types of nodes: root, deferment and invocation.
    • The root node is a special node at the root of the tree.
    • A deferment node represents an async transition, namely a point during program execution when a function (a callback) is deferred for execution at some later point in time.
    • An invocation node represents a single execution of the node’s parent’s callback.
    • There are several constraints on the tree
      • A deferment node may have 0..n children, which can be either invocation nodes or deferment nodes.
      • child invocation nodes represent a single execution of the deferement's callback.
      • child deferment nodes represent cases where logical async transitions are transitive as a result of internal processing . (better explanation?)
      • A deferment node’s parent is either
        • an invocation node, indicating cases where an async deferment is triggered directly via code executing on the current js stack.
        • a deferment node, indicating when an async deferment is triggered as a result of a internal processing of a parent deferment (better explanation?)
        • the root node, when neither of the above are the case.
      • An invocation node’s parent is always a deferment node.
      • An invocation node can have 0..n deferment nodes. These children represent new async transitions created during the invocation.
    • deferment and invocation nodes can be in one of two states:
      • Active, meaning that a node can have new children created.
      • Inactive, meaning that no new child nodes can be created. Note that an inactive node may have a subtree with Active children.
        • root node is always in the Active state.

Now, ignoring implementation gaps (nextTick, promises, TimerWrap, others?), the async call tree defined above can be constructed through the init()/destroy()/pre()/post() callbacks propsed in this EPS.

  • init() callback fires when a deferment node is added to the tree.
    • The deferment node’s ID is passed in as a parameter.
    • The parent is
    • The currently executing inovcation node, if available (i.e., the invocation ID of the last pre() without a corresponding post()), or
    • The parentID parameter, or
    • The root node, if neither of the above.
    • The intial state of the node is Active
  • destroy() callback fires when a deferment node transition from Active state to Inactive state.
    • Note this doesn’t imply that the node in the tree can be removed, as it may have active nodes in its subtree. A subtree can be pruned when all nodes in the subtree are in the inactive state.
  • pre() fires when an invocation node is created (specifically, before the callback associated with the parent deferement is invoked).
    • the parent deferment node is specified by the passed in ID.
    • The identity of the invocation node is not specified by the API, and it is up to the callee to track identity here.
    • the initial state of the node is Active.
  • post() fires when an invocation node transitions from Active to Inactive state.
    • the parent deferment node is specified by the passed in ID.

If you would like to see a simple visualization of the tree defined above, see here. For code to generate such a graph, see here. The code will produce a graph.dot file that you can load into this online viewer. (If you don’t see a graph after loading in the file, try refreshing).

Appreciate feedback. Happy to change terminology (e.g. handles/requests). I have several hopes with the above:

  1. It provides a level of abstraction that makes reasoning about how to achieve the use cases clear. E.g., CLS is simply “walk the tree from current node to the root, and check a property bag at each invocation node”. Long-stack-traces is simply “capture the stack at instantiation of each deferement node, and when you want a long stack, walk the tree to the root, stitching together the stacks at each deferment node”.
  2. It provides a succinct vocabulary to discuss & convey async code execution.
  3. It provides a well-defined data structure to define async code execution. Debug tooling can use such a tree to visually display runtime code execution flow. With a system-defined node identity, disparate tools can interop about nodes in the tree. The tree can be transformed to a DAG by introducing new edge types (e.g., linking a callback in a deferment node to the invocation where the callback was created). Edges can be labeled with key statistics (e.g., the time span between when the deferment node was created & when an invocation node was created.
  4. This is a “conceptual model” that I think highlights some gaps in the proposed API. In particular,
    • current names of callback methods are not tied to the concepts defined in the model. Specifically, when framed as above, the callback methods are really life-cycle/state-transition events around nodes in the Async Call Tree.
    • The proposed API does not treat invocation nodes as first class citizens, as they do not have a system-defined identity.
    • The ID param passed to the pre() call is not the ID of the invocation node, but rather the ID of the new node's parent (adeferment node).
    • the parentId param of the init() method is not the new node's parent ID in in the Async Call Tree. i.e., as defined above, the parent of a deferment node is the ID of the currently executing invocation node.
@joshgav joshgav referenced this pull request in nodejs/diagnostics Jun 23, 2016
Closed

Diagnostics WG Meeting - 2016-07-13 #60

@trevnorris
Contributor

Yet throughout the entire proposal of the API, the async call tree is never mentioned. I'm left to infer what this tree is supposed is look like, and how it is constructed from the proposed API, and if such a tree actually solves the intended use cases.

The API does (should) allow the user to gather enough information to create/maintain their own call trees. Because the complexity differs for the needs of various async tree implementations, as does the performance overhead for those, I don't think it prudent to implement in node. Especially at this time where I'd consider it feature creep of the initial implementation. Taken you've already designed an async tree model, what I would like is for it to be developed outside of core using the given API and provide feedback on any shortcomings the API has.

There are a few things that need some adjustments/correcting:

The root node is a special node at the root of the tree.

The root would start at the point of running enable(). Since we will not be tracking all trees internally.

A deferment node represents an async transition, namely a point during program execution when a function (a callback) is deferred for execution at some later point in time.

Not sure what you mean by this. A handle can have a single callback that's queued to be executed after any number of completed tasks. Or the handle can have a callback that can always be called as long as the handle is alive (e.g. the 'connection' event). So I'm going to translate this as meaning "the point at which an asynchronous operation was called".

An invocation node represents a single execution of the node’s parent’s callback.

This doesn't relate to anything actually happening, except for a call to pre(), and doesn't directly relate to how things are executed. This is why its necessary to pass parentId to init(). Because it's possible that the underlying resource isn't created until after the pre()/post() calls of the parent. As far as tracking the async tree, pre()/post() aren't (shouldn't be) necessary.

The deferment node’s parent is the currently executing inovcation node (as determined by the last pre() invocation), or the special root node.

As explained above, you cannot depend on pre() to convey the parent id information.

pre() fires when an invocation node is created (specifically, before the callback associated with the parent deferement is invoked).

Same thing as the above two comments.

Note this doesn’t imply that the node in the tree can be removed, as it may have active nodes in its subtree. A subtree can be pruned when all nodes in the subtree are in the inactive state.

This doesn't capture the fact that handles can be unref'd. Meaning they won't keep the application open. One would suspect that if there are Active nodes then the application must remain open.

The proposed API does not treat invocation nodes as first class citizens, as they do not have a system-defined identity.

Because by "invocation node" you mean "pre() was called". I don't see the need to introduce that added complexity for something so easily track-able in the userland API.

The ID param passed to the pre() call is not the ID of the invocation node, but rather the ID of the new node's parent (adeferment node).

There is no need to keep an incrementing counter for every pre() called. The one piece of information essential to continue the async tree is the id of the handle created, that's responsible for calling pre().

As briefly mentioned above, there are some more API changes/additions I'm working on and will hopefully have committed by the weekend.

@AndreasMadsen
Member

@mike-kaufman you can also see https://github.com/AndreasMadsen/dprof for how to implement an async call tree.

@Fishrock123
Member
Fishrock123 commented Jun 24, 2016 edited

Seems good to me as a starting point to futher work on.

Note: EPs are not intended to be documentation and as such may not explain things quite the same as documentation might.)

LGTM, I think we should move to have this reviewed by the CTC and hopefully merged soon, it's been open for a long while and the responses seem mostly positive.

@mike-kaufman

Thanks Trevor for the feedback. Also, I think you deserve a huge amount of thanks for driving forward on this API. Clearly, without your effort this conversation wouldn't be happening. There’s a ton of potential around this API. From numerous conversations with node/js developers, it is clear that the complexity of async code execution is one of the biggest complaints. So, I’m trying to make sure that the API being discussed enables runtime & tooling scenarios that can build really slick new capabilities around understanding async execution.

I honestly don't think we're that far apart here. My concern is not with the implementation. In fact, the API described in the EPS and in its currently implemented form is able to construct the tree I defined. That said, I do think that the API can be communicated in a slightly different way that provides an increased level of clarity. Granted, I could implement a separate module that sits on top of the API you’ve spec’d, but my claim is they would do nearly the same thing. The only differences would be the names of methods and parameters, and support for a system-defined invocation identity.

One thing I want to be clear about, is I’m not suggesting that the implementation in node needs to track the Async Call Tee. I agree with you that can be done by clients, as-needed, via the current APIs.

I am suggesting the following:

  • framing the API in concrete terms around precise definitions of concepts that directly relate to the problem being solved makes the API far more clear.
  • Precisely defined data structures are a powerful and lasting thing.
  • When framed by the precise definition of the Async Call Tree that I gave, some names and parameters in this EPS become less than clear.
  • invocations of callbacks are first-class citizens in the tree.
  • “identity” (node identity in this case) is a powerful thing, as it enables disparate modules to interop and easily share data about the same entity.

To address specific comments:

There are a few things that need some adjustments/correcting:

I've updated the definition inline. Please let me know if you have further feedback.

Not sure what you mean by this [...snip...] I'm going to translate this as meaning "the point at which an asynchronous operation was called".

I think we're on the same page here. My thinking is if I have code doSomethingAsync(function f() {});, that implies an async transition (a deferment node) with callback f. When f is actually invoked, that is an invocation node.

This doesn't relate to anything actually happening, except for a call to pre() [...snip...] As far as tracking the async tree, pre() / post() aren't (shouldn't be) necessary.

My claim is that invocations of callbacks are first-class citizens in the logical tree. There is something happening, namely we’re executing user code. Consider the following scenarios:

  • continuation local storage. Every invocation of a callback can set a new value of for some property in the CLS property bag, and then call new async operations. For correct behavior, the CLS storage mechanism needs to associate property bags with the invocation node, not the deferment node; stated another way, the CLS property bag wouldn't be associated with the handle's ID, but rather with each unique invocation of the handle's callback.
  • "advanced tracing". Imagine a logging framework that is able to associate log statements with a specific invocation, and then place those log statements in context of a visual representation of the async call tree.
  • "advanced post-mortem debugging". Imagine a JS-engine level debug framework that is able to capture low-level traces per callback invocation, and then imagine the ability to put such traces in context across the async call tree. These traces are captured on a per-invocation basis.
  • “instance origin” – imagine being able to link a function (or any object) to the invocation where it was constructed, and then being able place that in context of the async execution flow of the entire program.
  • tool interop. Now, imagine that I have several disparate tools like above, and they want to interop. How do these tools agree on the identity of an invocation?

So, there’s a lot of powerful things that can be done that rely on a uniquely identifiable invocation in the async call tree. As you’ve described, this is distinctly different from a handle. My understanding is the overheard to track a system-defined invocation identity is one word of memory (per handle, possibly global) and one increment instruction per invocation. This seems negligible.

As explained above, you cannot depend on pre() to convey the parent id information.

I think I edited my definition above to capture this, but the explanation could probably use some work. Feel free to suggest something.

Note this doesn’t imply that the node in the tree can be removed, as it may have active nodes in its subtree. A subtree can be pruned when all nodes in the subtree are in the inactive state.

This doesn't capture the fact that handles can be unref'd. Meaning they won't keep the application open. One would suspect that if there are Active nodes then the application must remain open.

unref'd handles are interesting. Can a callback associated with an uref'd handle be invoked? I wasn't placing any constraints that a node in the active state implies the program can't be shut down, but it's certainly something that can be incorporated into the definition of the tree (provided there's an notification mechanism when a handle is unref'd).

Note I was intentionally trying to decouple the concept of the handle (meaning the underlying system resource), and the deferment node, as their lifetimes are different. A handle can be destroyed, but its corresponding node in the Async Call Tree can be kept around, e.g., if I want to capture the tree in its entirety for post-mortem visualization.

There is no need to keep an incrementing counter for every pre() called. The one piece of information essential to continue the async tree is the id of the handle created, that's responsible for calling pre() .

I think we disagree here. See above for the examples I gave. Please let me know if this isn’t clear or we’re talking past each other?

@mike-kaufman

@AndreasMadsen - thanks for the pointer. It looks like we've arrived at a very similar conceptual model? All the more reason to formalize its definition, no? e.g., how would I build a graphical tool with my graph & your dprof data, display both, and link user's selection between the two?

@Fishrock123
Member

Can a callback associated with an uref'd handle be invoked?

tl;dr: Yes

@mike-kaufman I think it would probably be better to use existing language, there are a lot of terms here like active that can be confused with existing things. (i.e. confused with "active" handles.)

@mike-kaufman

think it would probably be better to use existing language, there are a lot of terms here like active that can be confused with existing things

@Fishrock123 - I'd love to hear your suggestions. I intentionally didn't use handle to refer to nodes in the tree, as they're different concepts with different life-cycles.

@AndreasMadsen
Member

thanks for the pointer. It looks like we've arrived at a very similar conceptual model?

Yes. But parallel execution is inherently a tree, I don't think there is that many ways of expressing it.

All the more reason to formalize its definition, no?

Honestly I find the language confusing and I see no reason to standardize a conceptual model. If whoever is going to use async wrap, can understand the API and create a mental model for themself that allows them to build the simple tools, such as CLS or a long-stack-trace tool, then communicating those ideas shouldn't be a challenge.

how would I build a graphical tool with my graph & your dprof data, display both, and link user's selection between the two?

The drop data structure is documented, you can interface with that.

@mike-kaufman

Honestly I find the language confusing and I see no reason to standardize a conceptual model. If whoever is going to use async wrap, can understand the API and create a mental model for themself that allows them to build the simple tools, such as CLS or a long-stack-trace tool, then communicating those ideas shouldn't be a challenge.

@AndreasMadsen - I think you're contradicting yourself. :) If you find my interpretation confusing, then that's a strong indication the conceptual model should be clearly defined, no?

Honestly, I'd appreciate any concrete feedback on what I've proposed. I'm happy to change names, or look at any alternative proposals. And, I'm happy to be wrong - just show me why. :) Reality is, from my point-of-view as a user of this API, the EPS hasn't articulated the conceptual model to degree of clarity or specificity, and this results in high degree of ambiguity about exactly how this will be used to achieve stated use cases.

@AndreasMadsen
Member

If you find my interpretation confusing, then that's a strong indication the conceptual model should be clearly defined, no?

You interpretation is fine, it is simply the language. Maybe it's because I'm not a native english speaker.

Reality is, from my point-of-view as a user of this API, the EPS hasn't articulated the conceptual model to degree of clarity or specificity.

I don't think the EP is meat to be read by users, it is simply for the CTC to read and use as a basis for their decision. The CTC is not the users.

I have tried to teach AsyncWrap at local meetups, and it is an exceptionally hard concept to grasp for newcomers. I don't think this is horrible, since AsyncWrap is mostly meant for the experienced users who want to develop there own diagnostic tools. That being said, it is very possible that the current documentation could benefit from using trees as a conceptual model. I would suggests that you try to improve that and not the EP.

@mike-kaufman

@AndreasMadsen - let me know if there are specific words that you find confusing - I'm happy to try and change them.

I have tried to teach AsyncWrap at local meetups, and it is an exceptionally hard concept to grasp for newcomers

:) This is just more support for the argument I'm making. If an API is "exceptionally hard" to explain, there's usually a problem with the API.

I don't think this is horrible, since AsyncWrap is mostly meant for the experienced users who want to develop there own diagnostic tools.

:( I really couldn't disagree more strongly. To suggest that an API is only for those experienced enough to use it is incredibly user-unfriendly.

I don't think the EP is meat to be read by users, it is simply for the CTC to read and use as a basis for their decision. The CTC is not the users.

While this isn't end-user documentation, the goals of the API and the conceptual model around it should be clear. I don't know how an API proposal can be effectively reviewed otherwise.

That being said, it is very possible that the current documentation could benefit from using trees as a conceptual model. I would suggests that you try to improve that and not the EP.

Yeah, but when you frame it around the Async Call Tree, the hook method names/signature become inconsistent. Namely,

  • init(id, type, parentId)
    • corresponds to a new deferment node in the tree. (aka, a low-level handle is created).
    • id is the ID of the new node
    • inconsistency: parentId is not necessarily the parent node in the Async Call Tree.
  • pre(id, entryPoint)
    • corresponds to a new invocation node in the tree. (aka, an callback is about to be invoked).
    • inconsistency: id is not the id of the new node (like in init), but is the parent's id in the tree
  • post(id, didThrow)
    • inconsistency: id is the parent's id.
  • destroy(id)
    • the low-level handle is being deleted.
    • inconsistency: the node in the tree is not necessarily "destroyable", as it may have triggered subsequent async operations that are still active.
@trevnorris
Contributor

:( I really couldn't disagree more strongly. To suggest that an API is only for those experienced enough to use it is incredibly user-unfriendly.

It's not the API that's not user-friendly, it's the complex model that it exposes. I've experienced the same teaching basic asynchronous programming to developers that aren't used to asynchronous anything. Let's not confuse the API with the concept it encapsulates.

While this isn't end-user documentation, the goals of the API and the conceptual model around it should be clear. I don't know how an API proposal can be effectively reviewed otherwise.

Clarity here is subjective. IMO I find the abstracted model you propose more complicated than simply referring to the operations themselves.

  • init(id, type, parentId)
    • inconsistency: parentId is not necessarily the parent node in the Async Call Tree.

This is one reason why I find your model of the Async Call Tree confusing. The parentId is well defined and introduces no inconsistency.

  • pre(id, entryPoint)
    • inconsistency: id is not the id of the new node (like in init), but is the parent's id in the tree
  • post(id, didThrow)
    • inconsistency: id is the parent's id.

The concept of an invocation node was introduced by you, and has no attachment to the reality of what's happening. There is no inconsistency in the way the id propagates to pre()/post().

  • destroy(id)
    • inconsistency: the node in the tree is not necessarily "destroyable", as it may have triggered subsequent async operations that are still active.

Can you please clarify "destroyable"? If by this you mean that it can't be removed from the call graph then you've misinterpreted why it exists.

Every inconsistency identified is only an inconsistency as far as the API relates to the Async Call Tree. If any model should be proposed it should match that of how the API functions today. Instead of creating a model and then attempt to change the API to match.

@AndreasMadsen
Member

@mike-kaufman I completely agree with @trevnorris.

To clarify, the API here exposes the lifetime of handle objects and sometimes that is all you want. For example if you just want to monitor a single TCP connection.

It is important to remember that handle objects exists in node.js, the nodes you suggests does not. A tree representation is thus an abstraction upon that information. The node.js filosofi is not to abstract too much, but let userland modules take care of making things easy.

@Fishrock123
Member
Fishrock123 commented Jun 28, 2016 edited

While this isn't end-user documentation, the goals of the API and the conceptual model around it should be clear. I don't know how an API proposal can be effectively reviewed otherwise.

The people reviewing it more or less already have the sort of "model" in their heads.

It's less some correct abstract structure and more How Things Actually Work.

It is important to remember that handle objects exists in node.js, the nodes you suggests does not. A tree representation is thus an abstraction upon that information.

Precisely.

@jfmengels jfmengels commented on an outdated diff Jun 29, 2016
XXX-asyncwrap-api.md
+tracking.
+
+It must be clarified that the `AsyncWrap` API is not meant to abstract away
+Node's implementation details, and is in fact intentional. Observing how
+operations are performed, not simply how they appear to perform from the public
+API, is a key point in its usability. For a real world example, a user was
+having difficulty figuring out why their `http.get()` calls were taking so
+long. Because `AsyncHooks` exposed the `dns.lookup()` request to resolve the
+host being made by the `net` module it was trivial to write a performance
+measurement that exposed the hidden culprit. Which was that DNS resolution was
+taking much longer than expected.
+
+A small amount of abstraction is necessary to accommodate the conceptual nature
+of the API. For example, the `HTTPParser` is not destructed but instead placed
+into an unused pool to be used later. Even so, at the time it is placed into
+the pool the `destroy()` callback will run and then the id assigned to that
@jfmengels
jfmengels Jun 29, 2016

nit: double space after the pool

@Trott Trott and 2 others commented on an outdated diff Jun 29, 2016
XXX-asyncwrap-api.md
+// time for requests (e.g. FSREQWRAP).
+function pre(id) { }
+
+// post() is called just after the handle's callback has finished, and will be
+// called regardless whether the handle's callback threw. If the handle's
+// callback did throw then hasThrown will be true.
+function post(id, hasThrown) { }
+
+// destroy() is called when an AsyncWrap instance is destroyed. In cases like
+// HTTPPARSER where the resource is reused, or timers where the handle is only
+// a JS object, destroy() will be triggered manually soon after post() has
+// completed.
+function destroy(id) { }
+
+// Create a new instance and add each callback to the corresponding hook.
+var asyncHook = new AsyncHook({ init, pre, post, destroy });
@Trott
Trott Jun 29, 2016 edited Member

Forgive me if this is naïve, as AsyncWrap has been in my peripheral vision mostly, but is AsyncHook a new global? Or is it require('async_wrap').AsyncHook? If the latter, should that be explicitly indicated?

@trevnorris
trevnorris Jun 30, 2016 Contributor

It's the latter. Swear I did show async_wrap.AsyncHook(). I'll add it.

@rvagg
rvagg Jul 6, 2016 Member

needs to be added here, there's an instance where it's done below but this one still needs fixing

@rvagg
Member
rvagg commented Jun 30, 2016

@nodejs/collaborators we'd like to try and move this to a final vote by the @nodejs/ctc but we'd prefer input from the broader group before that can happen. Please review the text of this PR, it's not too complex, and comment here if you have any questions or have any concerns that we should hold up the process to resolve. A -1 is an acceptable comment but please give some justification for that in case there are ways your concerns can be resolved and to help guide the CTC to a final decision. Collaborators are still the first gate in the decision-making process so please engage!

@rvagg
Member
rvagg commented Jun 30, 2016

Meant to say that we're aiming for a vote at next week's CTC meeting unless anything comes up here that needs resolution or stops this in its tracks.

@benjamingr
Member

Wait, doesn't AsyncWrap need to support promises before we ship it with docs? Did that change?

@trevnorris
Contributor

@benjamingr This is an EP, not a doc. The goal here is to agree on the API so work can begin on completing it. Our decision on what to do with Promises isn't a concern at this point.

@mcollina
Member
mcollina commented Jul 4, 2016

👍

cc @davidmarkclements you might want to review this, as you have you used it.

@rvagg rvagg commented on an outdated diff Jul 6, 2016
XXX-asyncwrap-api.md
+|--------|---------------|
+| Author | @trevnorris |
+| Status | DRAFT |
+| Date | 2016-04-05 |
+
+## Description
+
+Since its initial introduction along side the `AsyncListener` API, `AsyncWrap`
+has slowly evolved to ensure a generalized API that would serve as a solid base
+for module authors who wished to add listeners to the event loop's life cycle.
+Some of the use cases `AsyncWrap` has covered are long stack traces,
+continuation local storage, profiling of asynchronous requests and resource
+tracking.
+
+It must be clarified that the `AsyncWrap` API is not meant to abstract away
+Node's implementation details, and is in fact intentional. Observing how
@rvagg
rvagg Jul 6, 2016 Member

"and this is intentional"?

@rvagg rvagg commented on an outdated diff Jul 6, 2016
XXX-asyncwrap-api.md
+
+Performance impact of `AsyncWrap` should be zero if not being used, and near
+zero while being used. The performance overhead of `AsyncHook` callbacks
+supplied by the user should account for essentially all the performance
+overhead.
+
+
+## Terminology
+
+Because of the potential ambiguity for those not familiar with the terms "handle"
+and "request" they should be well defined.
+
+Handles are a reference to a system resource. The resource handle can be a
+simple identifier, such as a file descriptor, or it can be a pointer that
+allows access to further information, such as `uv_tcp_t`. Realize that the
+latter still relies on a file descriptor, but has been encapsulated in a data
@rvagg
rvagg Jul 6, 2016 Member

I'm not sure this strictly is true on Windows, is it? Perhaps that additional clarification will help explain why sometimes you don't get access to the underlying thing you might expect.

@rvagg rvagg commented on an outdated diff Jul 6, 2016
XXX-asyncwrap-api.md
+// is thrown.
+asyncHook.scope();
+
+// Allow callbacks of this AsyncHook instance to fire. This is not an implicit
+// action after running the constructor, and must be explicitly run to begin
+// executing callbacks.
+asyncHook.enable();
+
+// Disable listening for new asynchronous events. Though this will not prevent
+// callbacks from firing on asynchronous chains that have already run within
+// the scope of an enabled AsyncHook instance.
+asyncHook.disable();
+
+// Unlike disable(), prevent any hook callback from firing again in the future.
+// Future hooks can be added by running scope()/enable() again.
+// TODO: I am not sure all of this is possible. Some investigation is necessary.
@rvagg
rvagg Jul 6, 2016 Member

TODO needs clarification here before we can move on, this is kind of important

@rvagg rvagg commented on the diff Jul 6, 2016
XXX-asyncwrap-api.md
+simple identifier, such as a file descriptor, or it can be a pointer that
+allows access to further information, such as `uv_tcp_t`. Realize that the
+latter still relies on a file descriptor, but has been encapsulated in a data
+structure containing additional information that allows the application to
+work with it.
+
+Requests are short lived data structures created to accomplish one task. The
+callback for a request should always and only ever fire one time. When the
+assigned task has either completed or encountered an error. Requests are used
+by handles to perform work. Such as accepting a new connection or writing data
+to disk.
+
+
+## API
+
+### Overview
@rvagg
rvagg Jul 6, 2016 edited Member

Could you put a note in here somehow that mentions that further details are provided in the section below? Because you go into a fair bit of detail in the overview it feels like you're giving a complete picture (hence some of the questions you've been asked in this PR). Something like "Further details on each element of the API can be found in sections below this overview".

@trevnorris
trevnorris Sep 14, 2016 Contributor

Added comment below.

@rvagg rvagg commented on an outdated diff Jul 6, 2016
XXX-asyncwrap-api.md
+// Unlike disable(), prevent any hook callback from firing again in the future.
+// Future hooks can be added by running scope()/enable() again.
+// TODO: I am not sure all of this is possible. Some investigation is necessary.
+asyncHook.remove();
+```
+
+
+### `async_wrap`
+
+The object returned from `require('async_wrap')`.
+
+
+#### `async_wrap.subsystems`
+
+List of all subsystems that may trigger the `init` callback. These subsystems
+group relevant calls together. For example the `CRYPTO` provider encompasses
@rvagg
rvagg Jul 6, 2016 Member

use of "provider" here yet the ambiguity about subsystems remains in the next paragraph

@rvagg rvagg commented on an outdated diff Jul 6, 2016
XXX-asyncwrap-api.md
+all calls from the crypto library that only creates a request. Purposes of this
+is to allow quick filtering or reporting of incoming requests.
+
+**Note(trevnorris):** "provider" was originally intended to be "subsystem" then
+later essentially turned into the class name, unless the class being
+instantiated inherited directly from `AsyncWrap`. so should subsystems simply be
+changed to include the name of every class constructor? or should it properly
+group calls together, such as `TCPWRAP` and `TCPCONNECTWRAP` into a single TCP
+provider? or possibly both.
+
+
+#### `async_wrap.getHandleById(id)`
+
+Some handles are cleaned up by the garbage collector. Preventing the user from
+being able to store those handles for future reference. Instead node keeps
+internal references to each handle that aren't affected by GC. They can then be
@rvagg
rvagg Jul 6, 2016 Member

s/aren't/isn't

@rvagg rvagg commented on an outdated diff Jul 6, 2016
XXX-asyncwrap-api.md
+
+Callbacks are not implicitly enabled after an instance is created. The reason
+for this is to not make any assumptions about the user's use case. Since
+constructing the `asyncHook` during startup, but not using it until later is, a
+perfectly reasonable use case. This API is meant to err on the side of
+requiring explicit instructions from the user.
+
+
+#### `asyncHook.disable()`
+
+Disable the callbacks for a given `AsyncHook` instance. Doing this will prevent
+the `init()`, etc., calls from firing for any new roots of asynchronous call
+stacks, but will not prevent existing asynchronous call stacks that have
+already been captured by the `AsyncHook` instance from continuing to fire.
+
+While not part of the immediate development plan, it should be possible in the
@rvagg
rvagg Jul 6, 2016 Member

This is all nice, but what does it do now? What would the code sample below do with the current development plan? Disable the hook in its entirety rather than for just the current call stack? It would be good to clarify below the example what the current behaviour is. Also, what does the migration path look like to making this functionality work if it doesn't now? Would it be a breaking change to the API to make it work as is being proposed here for future work as opposed to what goes in now?

@rvagg rvagg and 1 other commented on an outdated diff Jul 6, 2016
XXX-asyncwrap-api.md
+
+* `id` {Number}
+* `entryPoint` {Function}
+
+Called just before the return callback is called after completing an
+asynchronous request. Or called on handles with events such as receiving a new
+connection. For requests, such as `fs.open()`, this should be called exactly
+once. For handles, such as a TCP server, this may be called 0-N times.
+
+The `entryPoint` is the callback that will be executed immediately after
+`pre()` returns. This is being passed as a way to track what work will be done.
+
+**Note(trevnorris):** I'm not sure passing only `entryPoint` is useful. Since
+the arguments of the function to be called will be available it may be useful
+to pass those to `pre()` as well. Though I have reservations about the user
+being able to mess with non-primitives.
@rvagg
rvagg Jul 6, 2016 Member

why? you're giving them access directly in to handles already, what else could you possibly give them access to that may cause more mess?

@trevnorris
trevnorris Aug 9, 2016 Contributor

along with this would be to pass the arguments that would have been passed to the callback. potentially allowing the user to change non-primitive argument values. but eh, you're right.

@rvagg rvagg commented on an outdated diff Jul 6, 2016
XXX-asyncwrap-api.md
+
+Called just before the return callback is called after completing an
+asynchronous request. Or called on handles with events such as receiving a new
+connection. For requests, such as `fs.open()`, this should be called exactly
+once. For handles, such as a TCP server, this may be called 0-N times.
+
+The `entryPoint` is the callback that will be executed immediately after
+`pre()` returns. This is being passed as a way to track what work will be done.
+
+**Note(trevnorris):** I'm not sure passing only `entryPoint` is useful. Since
+the arguments of the function to be called will be available it may be useful
+to pass those to `pre()` as well. Though I have reservations about the user
+being able to mess with non-primitives.
+
+
+#### `post(id, didThrow)`
@rvagg
rvagg Jul 6, 2016 Member

does this need a note about why no entryPoint is provided here? if people see a use for it in pre() then I'm guessing they'll see related uses in post()?

@rvagg
rvagg Jul 6, 2016 Member

also needs resolution from the discussion above about the possibility of passing the Error

@rvagg rvagg and 1 other commented on an outdated diff Jul 6, 2016
XXX-asyncwrap-api.md
+#### `post(id, didThrow)`
+
+* `id` {Number}
+* `didThrow` {Boolean}
+
+Called immediately after the return callback is completed. If the callback
+threw but was caught by a domain or `uncaughtException`, `didThrow` will be set
+to `true`. If the callback threw but was not caught then the process will exit
+immediately without calling `post()`.
+
+
+#### `destroy(id)`
+
+* `id` {Number}
+
+Called either when the class destructor is run, or if the resource is marked as
@rvagg
rvagg Jul 6, 2016 Member

does this need clarification about timing? does the resource disappear immediately before or after this call? does it matter? can we even know for sure?

@trevnorris
trevnorris Jul 6, 2016 Contributor

It's called during deconstruction of the C++ class. e.g. during the execution of delete. Or during the "conceptual" deconstruction of the resource if the resource is going to be placed in a queue for later use (e.g. HTTPParser).

Will clarify.

@rvagg rvagg commented on an outdated diff Jul 6, 2016
XXX-asyncwrap-api.md
+Resources like `HTTPParser` are reused throughout the lifetime of the
+application. This means node will have to synthesize the `init()` and
+`destroy()` calls. Also the id on the class instance will need to be changed
+every time the resource is acquired for use.
+
+For shared resources like `TimerWrap` this is not necessary since there is a
+unique JS handle that will contain the unique id necessary for the calls.
+
+
+## Notes
+
+### Promises
+
+Node currently doesn't have sufficient API to notify calls to Promise
+callbacks. In order to do so node would have to override the native
+implementation.
@rvagg
rvagg Jul 6, 2016 Member

... "we are working with the V8 team to gain access to enough internals to integrate Promises into the AsyncWrap API at a future date" or something to that effect. Current wording sounds like a brush-off which it's not.

@rvagg
Member
rvagg commented Jul 6, 2016

This is up for a vote by the CTC today if @trevnorris feels it's ready. If anyone in here has any reason to hold it up, please speak now because it may progress to ACCEPTED (it also may not of course).

@addaleax addaleax referenced this pull request in nodejs/node Jul 6, 2016
Closed

discuss: future of AsyncWrap #7565

@Fishrock123 Fishrock123 removed the ctc-agenda label Jul 7, 2016
@Fishrock123
Member

Should be moving to ACCEPTED, CTC voted in favor, though perhaps not at quorum.

@Trott
Member
Trott commented Jul 7, 2016

I don't know that we ever formally defined quorum but as I understand it, it's informally agreed to be 50% + 1. So 10 people in the current CTC.

If so, we had a quorum yesterday, although it was close. We had 11 CTC members and two observers.

@xjamundx
xjamundx commented Aug 5, 2016

Should this be merged, so that people can read it easily in the repo or does that wait until the language is nailed down?

@trevnorris
Contributor

@xjamundx I could land it now, with the understanding that the public API will need to be added later.

@xjamundx
xjamundx commented Aug 9, 2016 edited

I just mean landing this document, so people can see it in the index of the repo! Or does that only happen after the code is merged?

@trevnorris
Contributor

@xjamundx i mean the same. the public API hasn't been specified fully. though i'll update the wording in the document from the above comments and land it.

@xjamundx
xjamundx commented Aug 9, 2016

@trevnorris Awesome thanks! Was hanging out in react land for a while and now hoping to get back on top of what's going in node :)

@trevnorris
Contributor

@AndreasMadsen I've updated the EP. Mind taking a look?

@Fishrock123 Fishrock123 and 1 other commented on an outdated diff Aug 23, 2016
XXX-asyncwrap-api.md
+## API
+
+### Overview
+
+```js
+// Standard way of requiring a module. Snake case follows core module
+// convention.
+const async_wrap = require('async_wrap');
+
+// Return the id of the current execution context. Useful for tracking state
+// and retrieving the handle of the current parent without needing to use an
+// AsyncHook().
+const id = async_wrap.currentId();
+
+// Create a new instance and add each callback to the corresponding hook.
+var asyncHook = new AsyncHook({ init, before, after, destroy });
@Fishrock123
Fishrock123 Aug 23, 2016 Member

@trevnorris is this async_wrap.AsyncHook? maybe import that above?

@trevnorris
trevnorris Aug 24, 2016 Contributor

Curse. Thought I fixed that. Yes, it is.

@AndreasMadsen AndreasMadsen and 1 other commented on an outdated diff Aug 25, 2016
XXX-asyncwrap-api.md
+to disk.
+
+
+## API
+
+### Overview
+
+```js
+// Standard way of requiring a module. Snake case follows core module
+// convention.
+const async_wrap = require('async_wrap');
+
+// Return the id of the current execution context. Useful for tracking state
+// and retrieving the handle of the current parent without needing to use an
+// AsyncHook().
+const id = async_wrap.currentId();
@AndreasMadsen
AndreasMadsen Aug 25, 2016 edited Member

This is properly a good idea for performances reasons, but it does go against the goal:

In order to remain minimal, all potential features for initial release will first be judged on whether they can be achieved by the existing public API. If so then such a feature won't be included.

@trevnorris
trevnorris Aug 25, 2016 Contributor

Nice. Thanks for bringing this up. Reason I have it included is because 1) It's already necessary to exist for internal usage, and 2) For users of the JS embedder API. I don't feel like they should need to create hooks in order to have their application support hooks. Otherwise they'll practically always be enabled.

@AndreasMadsen
AndreasMadsen Aug 25, 2016 Member

Can you elaborate on when this is needed in the JS embedder API?

@AndreasMadsen AndreasMadsen and 1 other commented on an outdated diff Aug 25, 2016
XXX-asyncwrap-api.md
+### `async_wrap`
+
+The object returned from `require('async_wrap')`.
+
+
+#### `async_wrap.currentId()`
+
+Return the id of the current execution context. Useful to track the
+asynchronous execution chain. It can also be used to propagate state without
+needing to use the `AsyncHook` constructor. For example:
+
+```js
+const map = new Map();
+
+net.createServer((c) => {
+ // TODO: In order for this to be useful it needs to be possible to get the
@AndreasMadsen
AndreasMadsen Aug 25, 2016 Member

This example did not clarify the use for me. "In order for ..." what to be useful?

@trevnorris
trevnorris Aug 25, 2016 Contributor

That's a line item for and API that doesn't exist yet that I would like to figure out. Which is, to get the uid of any given handle. For example, the net API has some conflicting parts I'm reasoning with.

// 1) We can't get the id of the server handle here b/c no handle
//    is actually created until .listen() is called.
const server = net.createServer((c) => {
  // 3) It's noted at the bottom of the EP that this id would be
  //    manually made the connection id, instead of the server id.
  //    Which it technically should be. This is because there's no
  //    other way to retrieve the connection's id until after an event
  //    on it has fired.

  // 4) What would be most convenient is if the user could call
  //    server.asyncId(), instead of needing to retrieve it in .listen(),
  //    cache it somewhere then retrieve it here.
});

server.listen(8080, () => {
  // 2) This is the soonest the id of the server can be retrieved.
  //    Which then would need to be cached for later when the
  //    'connection' callback is called.
});

So this TODO is a note to myself to figure out a way to support something like server.asyncId() and the embedder API.

@AndreasMadsen AndreasMadsen and 1 other commented on an outdated diff Aug 25, 2016
XXX-asyncwrap-api.md
+const map = new Map();
+
+net.createServer((c) => {
+ // TODO: In order for this to be useful it needs to be possible to get the
+ // id of a given handle. Otherwise it'll be impossible to store state at
+ // moments like this for later usage using the handle's id.
+}).listen(8111);
+```
+
+
+### Constructor: `AsyncHook(callbacks)`
+
+The `AsyncHook` constructor returns an instance that contains information about
+the callbacks that are to fire during specific asynchronous events in the
+lifetime of the event loop. The focal point of these calls centers around the
+lifetime of `AsyncWrap`. These callbacks will also be called to emulate the
@AndreasMadsen
AndreasMadsen Aug 25, 2016 edited Member

... lifetime of the AsyncWrap C++ class.

I think we need to be clear about it, when referring to the C++ class.

@trevnorris
trevnorris Aug 25, 2016 Contributor

Thanks.

Side note: I've decided that the public API won't be require('async_wrap'). Feels like implementation details leaking through. Did a twitter poll (very authoritative) and I'm split between async_hooks and async_events. But hopefully this will help the topic be less confusing in the future.

@trevnorris
Contributor

@Qard Looking ahead, I doubt we'll have a proper API from V8 to get AsyncWrap working with Promises before async/await comes out from behind the flag. There's nothing we can do to support async/await except patch V8, since any runtime patch we apply to Promise won't be picked up by async/await.

@Qard
Member
Qard commented Aug 30, 2016

@trevnorris Does async/await not trigger then calls? I haven't tried the in-progress stuff yet.

@trevnorris
Contributor

@Qard It uses an internal reference to .then(), so even if the user overwrites it async/await will still use the internal pointer to the function. So there's currently no way for the user to override what async/await does.

@Qard
Member
Qard commented Aug 31, 2016

Hmm...that is problematic. Maybe we could float a patch on the JS bits of the promise impl in V8? I know we're generally averse to floating patches, but async_wrap is really not that useful if it loses track of the continuation every time a promise is used.

@Fishrock123
Member

We probably need to float a patch for the microtaskqueue and promises anyways. I'll start looking into that soon but it's becoming apparent that it's something we need to do outside of async_wrap too.

There is a thread here for implementing MTQ hooks into V8: https://bugs.chromium.org/p/v8/issues/detail?id=4643

but it's not going anywhere fast. I'll investigate floating hooks soonish.

@ofrobots

@Qard Looking ahead, I doubt we'll have a proper API from V8 to get AsyncWrap working with Promises before async/await comes out from behind the flag

Async/await aren't shipping in Node v7.x. I don't expect the above prediction to come true.

@trevnorris
Contributor

@ofrobots Thanks for clarifying.

@Fishrock123 Fishrock123 commented on an outdated diff Aug 31, 2016
XXX-asyncwrap-api.md
+## API
+
+### Overview
+
+```js
+// Standard way of requiring a module. Snake case follows core module
+// convention.
+const async_wrap = require('async_wrap');
+
+// Return the id of the current execution context. Useful for tracking state
+// and retrieving the handle of the current parent without needing to use an
+// AsyncHook.
+const id = async_wrap.currentId();
+
+// Return new AsyncEvent instance. Used to trigger the AsyncHook callbacks.
+const asyncEvent = async_wrap.createEvent(handle[, parent_id]);
@Fishrock123
Fishrock123 Aug 31, 2016 Member

unsure if this is the best naming... createEmitter or something might be better? unsure right now

@AndreasMadsen AndreasMadsen referenced this pull request in AndreasMadsen/async-hook Sep 2, 2016
Closed

If one of hook throws, where should it go? #8

@overlookmotel
overlookmotel commented Sep 3, 2016 edited

Sorry if this is a stupid question (and apologies also if you're really only inviting feedback from CTC members) but...

How do Event Emitters fit into the picture? I can't see any mention of them in the draft API.

I know Event Emitters don't involve any async in the sense that .on() callbacks are called synchronously as soon as .emit() is called. However, from the point of view of the user registering an .on() handler, they provide a callback function which will execute most likely in another cycle of the event loop - so in that sense it acts like an async callback.

In CLS, which is my particular interest, you might well want CLS context when the callback is called to be maintained from the context in which the handler was registered (same pattern as with callback-taking methods).

You could argue that as there's no I/O involved, this should be dealt with in userland. However, I feel that there's a danger attached to this. The current version of CLS is not in core and neither is async-listener which provides its backing. Consequently some module authors have felt no need to support CLS (notably bluebird where the author refused to support CLS petkaantonov/bluebird#583), which has made using CLS a bit of a battle.

I'm really pleased to see the addition of the Embedder API to async_wrap because that implies, as this will be part of node core, that all module authors should use the API to ensure async_wrap/CLS continues to work when a module uses e.g. batch queueing.

I feel it'd be ideal if there was a similar convention or API provided for Event Emitters, so that this is also part of node core and module authors can be reasonably expected to make async_wrap/CLS work sensibly when using Event Emitters.

Apologies if I'm missing the point here...

@RobinQu
RobinQu commented Sep 3, 2016

I am new here. What's Embedder API indeed?

@overlookmotel

@RobinQu Read XXX-asyncwrap-api.md in the files in this PR. That document outlines the proposed API for async_wrap.

@Fishrock123
Member

I'm really pleased to see the addition of the Embedder API to async_wrap because that implies, as this will be part of node core, that all module authors should use the API to ensure async_wrap/CLS continues to work when a module uses e.g. batch queueing.

Correct.

I feel it'd be ideal if there was a similar convention or API provided for Event Emitters, so that this is also part of node core and module authors can be reasonably expected to make async_wrap/CLS work sensibly when using Event Emitters.

I think usually an emitter is attached to some underlying resource? If so, that resource (even if only conceptual) should probably use the async_wrap embedder api, I think.

@overlookmotel perhaps I am misunderstanding, mind to give an example?

Keep in mind any significant new features are pretty out-of-scope right now, so it is unlikely much else will land in the initial public API, but things could be added later if they are necessary / make sense. :)

@estliberitas estliberitas commented on an outdated diff Sep 3, 2016
XXX-asyncwrap-api.md
+
+
+## Notes
+
+### Promises
+
+Node currently doesn't have sufficient API to notify calls to Promise
+callbacks. In order to do so node would have to override the native
+implementation.
+
+
+### Immediate Write Without Request
+
+When data is written through `StreamWrap` node first attempts to write as much
+to the kernel as possible. If all the data can be flushed to the kernel then
+the function exists without creating a `WriteWrap` and calls the user's
@estliberitas
estliberitas Sep 3, 2016 Member

s/exists/exits/ ?

@Qard
Member
Qard commented Sep 3, 2016

@Fishrock123 http.Agent pooling will no doubt play havoc on that assumption, among many other things.

@Fishrock123
Member

@Qard Doesn't the embedder API cover that?

The Agent will then be the parent of several connections, and it should act roughly as expected?

@Qard
Member
Qard commented Sep 3, 2016

@Fishrock123 Only if patches are applied in core. There are a bunch of emits that occur in http.Agent internals that'll get linked to the wrong request unless the embedder API automatically wraps event emitters or gets special-case used in a bunch of places in core that use event emitters.

@RobinQu RobinQu and 1 other commented on an outdated diff Sep 5, 2016
XXX-asyncwrap-api.md
+resources the underlying class may have been destructed.
+
+
+## API Exceptions
+
+### net Client connection Event
+
+Technically the `before()`/`after()` events of the `'connection'` event for
+`net.Server` would place the server as the active id. Problem is that this is
+not intuitive in how the asynchronous chain would propagate. So instead make
+the client the active id for the duration of the `'connection'` callback.
+
+
+### Reused Resources
+
+Resources like `HTTPParser` are reused throughout the lifetime of the
@RobinQu
RobinQu Sep 5, 2016

Do we have a complete list of reused resources?

@Fishrock123
Fishrock123 Sep 5, 2016 edited Member

The public docs will have a list.

The big ones that you would commonly run into would be HTTPParser and TimerWrap.

@RobinQu RobinQu and 1 other commented on an outdated diff Sep 7, 2016
XXX-asyncwrap-api.md
+// Unlike disable(), prevent any hook callback from firing again in the future.
+// Future hooks can be added by running syncScope()/enable() again.
+// TODO: Investigation is needed to know whether this is possible.
+asyncHook.remove();
+
+// init() is called during object construction. The handle may not have
+// completed construction when this callback runs. So all fields of the
+// handle referenced by "id" may not have been populated.
+function init(id, type, parentId, handle) { }
+
+// before() is called just before the handle's callback is called. It can be
+// called 0-N times for handles (e.g. TCPWrap), and should be called exactly 1
+// time for requests (e.g. FSReqWrap).
+function before(id) { }
+
+// after() is called just after the handle's callback has finished, and will be
@RobinQu
RobinQu Sep 7, 2016 edited

If the handle's callback throws, and there is no try-catch nor uncaughtException listener, should the process exits after these after and destroy hooks are executed?

@Fishrock123
Fishrock123 Sep 7, 2016 Member

@RobinQu It will propagate the error as normal, calling after() and probably exiting the process before destroy().

@RobinQu
RobinQu Sep 7, 2016

So no guarantee that after and destroy would be called if process shall exit?

@RobinQu RobinQu and 1 other commented on an outdated diff Sep 7, 2016
XXX-asyncwrap-api.md
+function init(id, type, parentId, handle) { }
+
+// before() is called just before the handle's callback is called. It can be
+// called 0-N times for handles (e.g. TCPWrap), and should be called exactly 1
+// time for requests (e.g. FSReqWrap).
+function before(id) { }
+
+// after() is called just after the handle's callback has finished, and will be
+// called regardless whether the handle's callback threw.
+function after(id) { }
+
+// destroy() is called when an AsyncWrap instance is destroyed. In cases like
+// HTTPParser where the resource is reused, or timers where the handle is only
+// a JS object, destroy() will be triggered manually soon after after() has
+// completed.
+function destroy(id) { }
@RobinQu
RobinQu Sep 7, 2016 edited

This is not the case for current implementation (4.5 and 6.5 as the time of writing).
init for HTTPParser is only called once in entire process, and uid for that handle is not changed after reuse.
The symptom can be easily observed by a simple script using http.createServer and http.request.

@Fishrock123
Fishrock123 Sep 7, 2016 Member

@RobinQu This EP reflects an (very soon) upcoming implementation and not the current, experimental one.

@RobinQu
RobinQu Sep 7, 2016 edited

I am working on a Zone implementation based on async_wrap and this problem forces me to do monkey patch on http.Server like cls.

@Fishrock123
Fishrock123 Sep 7, 2016 edited Member

@RobinQu You don't need Async_Wrap to implement Zones in your own code, though maybe from other APIs. (That being said, you'll be able to build something much better than Zones with this I think.)

If you do process.binding('async_wrap') it will not match this document.

The API described in this document is not yet in a Node release. Please wait.

@overlookmotel
overlookmotel commented Sep 7, 2016 edited

@Fishrock123 Sorry, been busy - will come back to you about Event Emitters soon as I can.

In the meantime, a quick question: what version of node is it planned for the full implementation of async_wrap to appear in? If node v7.x, will it also be back-ported to v6.x LTS?

@RobinQu RobinQu referenced this pull request in RobinQu/async-zone Sep 7, 2016
Open

HTTPParser reuse causes confusing contexts. #1

@Fishrock123
Member
Fishrock123 commented Sep 7, 2016 edited

a quick question: what version of node is it planned for the full implementation of async_wrap to appear in? If node v7.x, will it also be back-ported to v6.x LTS?

We're aiming for v7.0.0 with a backport to v6 and possibly even v4.

@RobinQu
RobinQu commented Sep 7, 2016

@Fishrock123 Is v7.0.0 scheduled in early October?

@Fishrock123
Member

Mid October, when v6 goes LTS.

@Fishrock123 Fishrock123 and 1 other commented on an outdated diff Sep 7, 2016
XXX-asyncwrap-api.md
-#### `event.emitBefore()`
+#### `async_hooks.emitBefore(id, event)`
@Fishrock123
Fishrock123 Sep 7, 2016 Member

what happens if you:

  • emit(<any positive number>, null)
  • emit(<any positive number>, {})
  • emit(-1, <anything>)
  • emit(0, <anything>)
@trevnorris
trevnorris Sep 8, 2016 Contributor
  • clears active hooks and sets current id
  • throws
  • throws
  • throws
@Fishrock123
Fishrock123 Sep 8, 2016 Member

clears active hooks and sets current id

Clarification on what this means would be helpful.

@Fishrock123 Fishrock123 commented on an outdated diff Sep 7, 2016
XXX-asyncwrap-api.md
-* Return: {Undefined}
+In the unusual circumstance that the embedder needs to define a different
+parent id than `currentId()`, they can pass in that id manually. Along with
+the `AsyncEventStor`, if there is one. If no stor is passed then it is assumed
@Fishrock123
Fishrock123 Sep 7, 2016 Member

Are all properties on AsyncEventStor hidden via Symbols?

@Fishrock123
Fishrock123 Sep 7, 2016 Member

Also what does this thing even need to propagate...?

@Fishrock123 Fishrock123 commented on an outdated diff Sep 8, 2016
XXX-asyncwrap-api.md
+constructor `AsyncHookStor`. Which will contain the state of all hooks that
+must propagate to future async calls. If there are no hooks that need to
+propagate then the return value will be `null`. Preventing the need of creating
+one additional object for every async constructor, even when hooks aren't being
+used.
+
+In the unusual circumstance that the embedder needs to define a different
+parent id than `currentId()`, they can pass in that id manually. Along with
+the `AsyncHookStor`, if there is one. If no stor is passed then it is assumed
+there are no hooks that need to propagate to the newly created stor.
+
+It is suggested to have `emitInit()` be the last call in the object's
+constructor.
+
+
+#### `async_hooks.emitBefore(id, event)`
@Fishrock123
Fishrock123 Sep 8, 2016 Member

emit(<any positive number>, null)

clears active hooks and sets current id

@trevnorris Clarification on what this means would be still be helpful.

@Fishrock123 Fishrock123 and 1 other commented on an outdated diff Sep 8, 2016
XXX-asyncwrap-api.md
+* Return: {Object|Null}
+
+Emit that a handle is being initialized. `id` should be a value returned by
+`async_hooks.newUid()`. Usage will probably be as follows:
+
+```js
+class Foo {
+ constructor() {
+ this.event_id = async_hooks.newUid();
+ this.hook_stor = async_hooks.emitInit(this.event_id, this, 'Foo');
+ }
+}
+```
+
+The return value from `emitInit()` is either an instance of the internal
+constructor `AsyncHookStor`. Which will contain the state of all hooks that
@Fishrock123
Fishrock123 Sep 8, 2016 Member

Are all properties on AsyncEventStor hidden via Symbols?

Also what does this thing even need to propagate...?

@trevnorris
trevnorris Sep 14, 2016 Contributor

The Stor doesn't exist anymore. Problem solved.

@trevnorris
Contributor

API has been updated to reflect the minimized API necessary to mitigate the performance impact. Hopefully this can be figured out, but unless it can I don't see how we can justify placing that burden on core.

@AndreasMadsen AndreasMadsen and 1 other commented on an outdated diff Sep 14, 2016
XXX-asyncwrap-api.md
+// a JS object, destroy() will be triggered manually soon after after() has
+// completed.
+function destroy(id) { }
+
+// The following calls are specific to the embedder API. If any hook emitter is
+// used then they must all be used to make sure state proceeds correctly.
+
+// Return new unique id for a constructing handle.
+const id = async_hooks.newUid();
+
+// Set the current global id.
+async_hooks.setCurrentId(id);
+
+// Call the init() callbacks. Returns an instance of AsyncEvent that will be
+// used in other emit calls.
+const event = async_hooks.emitInit(id, handle, type[, parentId]);
@AndreasMadsen
AndreasMadsen Sep 14, 2016 Member

The emitInit documentation says it returns undefined.

@trevnorris
trevnorris Sep 14, 2016 Contributor

Good catch. Fixed.

@trevnorris trevnorris referenced this pull request in nodejs/node Sep 14, 2016
Open

async_hooks initial implementation #8531

1 of 7 tasks complete
@Trott Trott commented on an outdated diff Sep 14, 2016
XXX-asyncwrap-api.md
@@ -0,0 +1,540 @@
+| Title | AsyncHook API |
+|--------|---------------|
+| Author | @trevnorris |
+| Status | DRAFT |
+| Date | 2016-09-14 |
+
+## Description
+
+Since its initial introduction along side the `AsyncListener` API, the internal
+class `AsyncWrap` has slowly evolved to ensure a generalized API that would
+serve as a solid base for module authors who wished to add listeners to the
+event loop's life cycle. Some of the use cases `AsyncWrap` has covered are long
+stack traces, continuation local storage, profiling of asynchronous requests
+and resource tracking. Though the public API is now exposed as `'async_hooks'`.
@Trott
Trott Sep 14, 2016 Member

Nit: remove Though

@Trott
Member
Trott commented Sep 14, 2016

At the CTC meeting today, 7 members voted in favor of moving this version to ACCEPTED status and 1 member abstained. We need at least two more votes to get to a resolution. If you are on @nodejs/ctc, please leave a vote (yes, no, or abstain) here. Thanks!

@rvagg
Member
rvagg commented Sep 15, 2016

Thanks @trott, I wrote basically the same thing as you in here but just discovered it unsubmitted in one of my browser tabs ...

Also note that the impl for these changes an be found @ nodejs/node#8531, those changes need a separate approval process to this EP but please review if you have the time.

@Qard
Member
Qard commented Sep 15, 2016 edited

@trevnorris What do you think of the idea of splitting the create event into separate create and queue events? The create would always happen once, at resource creation time, while the queue would happen when a request is queued to use the handle (or handle-like thing). This would make handle reuse much clearer. To track resource usage, you would simply need to compare create and destroy events. For tracing or long stack traces, you could rely on queue events to indicate exactly where the hop to the async barrier occurred, but still know what resource/call to attribute it to.

@Trott
Member
Trott commented Sep 16, 2016

@nodejs/ctc We still need a few more votes to get resolution on whether this should be moved out of Draft status and merged as Accepted status or not. The following CTC folks have not voted. Please indicate a yes, no, or abstain when you get a chance. Thanks. @jasnell @ofrobots @addaleax @cjihrig @bnoordhuis @chrisdickinson @indutny @mhdawson @shigeki @TheAlphaNerd

XXX-asyncwrap-api.md
+
+Current reason for this is because of the performance impact of tracking hooks
+individually against every handle. Combined with the fact that we're now
+tracking `process.nextTick()` calls. Which are used liberal throughout core.
@cjihrig
cjihrig Sep 16, 2016 Member

calls, which are used liberally throughout core.

@cjihrig
Member
cjihrig commented Sep 16, 2016

Please indicate a yes, no, or abstain when you get a chance.

yes

@trevnorris
Contributor

@Qard My reasoning is once the resource (e.g. HTTPParser) is no longer used by the asynchronous request it's no longer in the purview of the user in terms of asynchronous operations. It's simply a blank data structure that'll be reported in a snapshot. Then when the resource is reused it's re-initialized with all the data necessary to perform its task for the new asynchronous operation. Thus init() would again apply. I see it like this:

// We're not going to track this allocated memory
char* const storage = new char[sizeof(MyClass)];
// Instead we're going to track the constructor itself
MyClass* my = new(storage) MyClass();

and once the handle is no longer in use what's placed in the queue is nothing more than a husk of memory waiting to be reused. And keeping track of the fact that the memory hasn't been free'd doesn't fall in the scope of async_hooks. Thoughts?

@Qard
Member
Qard commented Sep 16, 2016

It's perhaps not a usable thing while blank, but still occupying memory, so I'd think it'd be something on would want to be able to track.

I feel like having both create and queue events would also help to quell the debate about whether promises should be recorded when the promise is created or when it's resolved/rejected. By reporting both create and queue, we can capture both points and users can do whichever one they want.

In discussion with @ofrobots, we came to the conclusion that the tracing need and the long stack traces need are a bit at odds with each other in terms of what points in the call graph are useful to them. Some want the call point, some want to queue point.

@AndreasMadsen
Member

conclusion that the tracing need and the long stack traces need are a bit at odds

I'm a bit out of the loop in this discussion. But as the author of a long stack trace module (trace), I can say that my users have diffrent opinions regarding what is the natural stack trace. But the current implementation, which treats .then() as a new handle, appears to be the most popular one (not that I have specific data on it). If I understand correctly that is also what CLS/tracing needs.

@Qard
Member
Qard commented Sep 16, 2016

CLS/tracing needs the before/after to point to the resolve of the promise the then call is chained from. Not from the create of the then call. Which is to say, CLS/tracing needs the queue point, not the call point.

@AndreasMadsen
Member
AndreasMadsen commented Sep 16, 2016 edited

CLS/tracing needs the before/after to point to the resolve of the promise the then call is chained from. Not from the create of the then call. Which is to say, CLS/tracing needs the queue point, not the call point.

Could you show an example? From previous conversations I think we are talking about the same thing, it is just the terminology that is confusion.

@Qard
Member
Qard commented Sep 16, 2016 edited

Often long stack trace libraries chose to have the callback point directly back to where it was passed into the then(...) call, so the create event of the promise should be your init point. If you want to trace through the internal mechanics however, you want to move the last point of reference to right before the resolve or reject is queued to the microtask, so you can capture the most surface area of what actually happened in the full context of the request.

Tracing cares about what happens inside a promise. So we need CLS access to look like this:

cls.set('last', 'before new promise')
var p = new Promise(done => {
  setImmediate(() => {
    cls.set('last', 'in setimmediate')
    done()
  })
})

cls.set('last', 'before promise then')
p.then(() => {
  assert.equal(cls.get('last'), 'in setimmediate')
})

There's also the user-facing context use-case though, which would expect the opposite. By supporting both create and queue events, we could have a CLS that supports multiple context propagation strategies, serving both needs.

@trevnorris
Contributor

Everyone, I've made an important clarification to the EP. Which is to clarify what parentId is meant to do. Previously it was partially dual purpose, but now that's not the case. Short of it is that currentId() gives us the resource creation tree and parentId gives us the "reason the resource was created" tree.

@Trott
Member
Trott commented Sep 19, 2016

@jasnell @ofrobots @addaleax @bnoordhuis @chrisdickinson @indutny @mhdawson @shigeki @TheAlphaNerd

We need one more "yes" vote or two more "abstain" votes or 9 "no" votes to move this forward. Consider this a test of whether @nodejs/ctc can realistically do more stuff asynchronously. :-D Please review and vote!

Currently we have 8 "yes" votes and 1 abstention.

@Trott
Mem