Skip to content
This repository has been archived by the owner on Apr 22, 2023. It is now read-only.

Allow passing specific PROVIDERs to AsyncListener callbacks #7145

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 180 additions & 99 deletions doc/api/tracing.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -59,45 +59,41 @@ Returns an object with the following properties
```


# Async Listeners
## Async Listeners

The `AsyncListener` API is the JavaScript interface for the `AsyncWrap`
class which allows developers to be notified about key events in the
class. This allows developers to be notified about key events in the
lifetime of an asynchronous event. Node performs a lot of asynchronous
events internally, and significant use of this API may have a
**significant performance impact** on your application.

**Note**: Running any of these functions within any `AsyncListener`
callback is *undefined behavior*.

## tracing.createAsyncListener(callbacksObj[, userData])

### tracing.createAsyncListener(callbacksObj[, userData[, provider]])

* `callbacksObj` {Object} Contains optional callbacks that will fire at
specific times in the life cycle of the asynchronous event.
* `userData` {Value} a value that will be passed to all callbacks.
* `userData` {Value} a default value (i.e. primitive or object) that will be
passed to all callbacks. It can be overwritten by returning a new value from
the `create()` callback.
* `provider` {Number} Types of providers the user wishes to track. These are
provided by `tracing.ASYNC_PROVIDERS`. If the user wishes to pass in a custom
set of providers but no `userData`, then pass `null` to `userData`.

Returns a constructed `AsyncListener` object.

To begin capturing asynchronous events pass either the `callbacksObj` or
pass an existing `AsyncListener` instance to [`tracing.addAsyncListener()`][].
The same `AsyncListener` instance can only be added once to the active
queue, and subsequent attempts to add the instance will be ignored.

To stop capturing pass the `AsyncListener` instance to
[`tracing.removeAsyncListener()`][]. This does _not_ mean the
`AsyncListener` previously added will stop triggering callbacks. Once
attached to an asynchronous event it will persist with the lifetime of the
asynchronous call stack.

Explanation of function parameters:

**callbacksObj**: An `Object` which may contain several optional fields:

`callbacksObj`: An `Object` which may contain several optional fields:

* `create(userData)`: A `Function` called when an asynchronous
event is instantiated. If a `Value` is returned then it will be attached
to the event and overwrite any value that had been passed to
`tracing.createAsyncListener()`'s `userData` argument. If an initial
`userData` was passed when created, then `create()` will
receive that as a function argument.
* `create(userData, type)`: A `Function` called when an asynchronous
event is instantiated. Any `userData` value passed when the `AsyncListener`
was created will be passed as the first argument. If a value is returned then
it will overwrite the `userData` value for the specific instance in which it
was run. The `type` argument is the type of `ASYNC_PROVIDERS` where the call
originated.

* `before(context, userData)`: A `Function` that is called immediately
before the asynchronous callback is about to run. It will be passed both
Expand All @@ -107,23 +103,31 @@ either occurred).

* `after(context, userData)`: A `Function` called immediately after
the asynchronous event's callback has run. Note this will not be called
if the callback throws and the error is not handled.

* `error(userData, error)`: A `Function` called if the event's
callback threw. If this registered callback returns `true` then Node will
assume the error has been properly handled and resume execution normally.
When multiple `error()` callbacks have been registered only **one** of
those callbacks needs to return `true` for `AsyncListener` to accept that
the error has been handled, but all `error()` callbacks will always be run.

`userData`: A `Value` (i.e. anything) that will be, by default,
attached to all new event instances. This will be overwritten if a `Value`
is returned by `create()`.
if the callback throws.

* `error(context, userData, error, handled)`: A `Function` called if any call
threw within the synchronous execution stack of the originating asynchronous
callback. The `context` is that of the original asynchronous function. Not
that of the originating function in the case something threw within a
synchronous call stack. If the `error()` callback returns `true` then Node
will assume the error has been properly handled and resume execution normally.
When multiple `error()` callbacks have been registered only **one** of those
callbacks needs to return `true` for `AsyncListener` to accept that the
error has been handled, but all `error()` callbacks will always be run.
The `context` passed to `error()` is that of the originating function in
the call stack. `handled` is whether a previous AL has already returned that
the error has been handled.

**userData**: A `Value` (i.e. anything) that will, by default, be attached
to all new event instances. This will be overwritten if a `Value` is
returned by `create()`.

Here is an example of overwriting the `userData`:

var tracing = require('tracing');

tracing.createAsyncListener({
create: function listener(value) {
create: function listener(value, type) {
// value === true
return false;
}, {
Expand All @@ -132,44 +136,66 @@ Here is an example of overwriting the `userData`:
}
}, true);

**provider**: A provider, or combination of providers, from `ASYNC_PROVIDERS`.
Here's an example:

var tracing = require('tracing');
var tap = tracing.ASYNC_PROVIDERS;

tracing.createAsyncListener(callbacksObj, null, tap.TCP | tap.PIPE);

Currently callbacks will fire for all `NEXTTICK` providers. As it is currently
impossible to correctly determine their caller's provider. As such `NEXTTICK`
should never be passed as a provider type.

**Note:** The [EventEmitter][], while used to emit status of an asynchronous
event, is not itself asynchronous. So `create()` will not fire when
an event is added, and `before()`/`after()` will not fire when emitted
callbacks are called.
an EventEmitter is instantiated, and `before()`/`after()` will not fire when
a callback is emitted.


## tracing.addAsyncListener(callbacksObj[, userData])
## tracing.addAsyncListener(asyncListener)
### tracing.addAsyncListener(callbacksObj[, userData[, provider]])
### tracing.addAsyncListener(asyncListener)

Returns a constructed `AsyncListener` object and immediately adds it to
Returns a constructed `AsyncListener` instance and immediately adds it to
the listening queue to begin capturing asynchronous events.

To begin capturing asynchronous events pass either the `callbacksObj` or
pass an existing `AsyncListener` instance. The same `AsyncListener` instance
can only be added once to the active queue, and subsequent attempts to add
the instance will be ignored. If a previously instantiated `AsyncListener`
is passed than any value passed to `userData` or `provider` will be ignored.

To stop capturing pass the returned `AsyncListener` instance to
[`tracing.removeAsyncListener()`][]. This does _not_ mean the
`AsyncListener` previously added will stop triggering callbacks. Once
attached to an asynchronous event it will persist with the lifetime of the
asynchronous call stack.

Function parameters can either be the same as
[`tracing.createAsyncListener()`][], or a constructed `AsyncListener`
object.
[`tracing.createAsyncListener()`][], or a constructed `AsyncListener`.

Example usage for capturing errors:

var fs = require('fs');
var tracing = require('tracing');

var cntr = 0;
var key = tracing.addAsyncListener({
create: function onCreate() {
return { uid: cntr++ };
return cntr++;
},
before: function onBefore(context, storage) {
// Write directly to stdout or we'll enter a recursive loop
fs.writeSync(1, 'uid: ' + storage.uid + ' is about to run\n');
before: function onBefore(context, uid) {
console.log('uid: ' + uid + ' is about to run\n');
},
after: function onAfter(context, storage) {
fs.writeSync(1, 'uid: ' + storage.uid + ' ran\n');
after: function onAfter(context, uid) {
console.log('uid: ' + uid + ' ran\n');
},
error: function onError(storage, err) {
error: function onError(context, uid, err) {
// Handle known errors
if (err.message === 'everything is fine') {
// Writing to stderr this time.
fs.writeSync(2, 'handled error just threw:\n');
fs.writeSync(2, err.stack + '\n');
console.error('handled error just threw:\n');
console.error(err.stack + '\n');
return true;
}
}
Expand All @@ -182,29 +208,113 @@ Example usage for capturing errors:
// Output:
// uid: 0 is about to run
// handled error just threw:
// Error: really, it's ok
// at /tmp/test2.js:27:9
// at process._tickCallback (node.js:583:11)
// Error: everything is fine
// at /tmp/test.js:28:9
// at process._tickCallback (node.js:339:11)
// at Function.Module.runMain (module.js:492:11)
// at startup (node.js:123:16)
// at node.js:1012:3
// at startup (node.js:124:16)
// at node.js:803:3

When a `ASYNC_PROVIDERS` is passed the callbacks will only be fired when that
provider type is triggered. Again with the exception that all `NEXTTICK`
providers will fire. Here is an example usage:

var fs = require('fs');
var tracing = require('tracing');
var tap = tracing.ASYNC_PROVIDERS;

tracing.addAsyncListener(tap.TIMER, {
create: function(stor, type) { }
});

// create() will fire for this setImmediate().
setImmediate(function() {
// But won't fire for this fs operation.
fs.stat(__filename, function() {
// Even so, the stack has been kept in the background and
// create() will fire again for this setTimeout().
setTimeout(function() { });
});
});

The `error()` callback is unique because it will still fire anywhere
within the call stack, regardless whether the error occurred while in
the stack of the specified provider.

var fs = require('fs');
var net = require('net');
var tracing = require('tracing');

tracing.addAsyncListener(tracing.ASYNC_PROVIDERS.TCP, {
create: function(stor, type) { },
error: function(context, stor, err) {
// Continue normally
return true;
}
});

## tracing.removeAsyncListener(asyncListener)
// The TCP connections made from the net module will trigger
// any create/before/after callbacks.
net.createServer(function(c) {
// This FS operation will not.
fs.stat(__filename, function() {
// Regardless, this error will still be caught.
throw new Error('from fs');
});
}).listen(8080);

If an `error()` callback throws then there is no allowed recovery. Any
callbacks placed in the [`process` exit event][] will still fire. Though
the user should be careful to check the exit status before doing much
because it's possible to override the actual reason for triggering the
exit. For example:

var assert = require('assert');
var tracing = require('tracing');

process.on('exit', function(status) {
// Need to check if the exit status is fine before continuing.
if (status > 0)
return;

// Or else a somthing like this will not display why the code
// actually exited.
assert.ok(false);
});

tracing.addAsyncListener({
error: function(context, data, err) {
// Check if we can recover from this error or not.
if (err.message === 'expected message')
return true;
}
});

process.nextTick(function() {
// This error will bubble through because it doesn't match
// the criterion necessary to recover properly.
throw new Error('random message');
});

Note that the `NEXTTICK` `ASYNC_PROVIDERS` will always fire. Because of
the implicit way [`process.nextTick()`][] is called it is impossible to
properly determine the actual provider that initiated the call.

### tracing.removeAsyncListener(asyncListener)

Removes the `AsyncListener` from the listening queue.

Removing the `AsyncListener` from the active queue does _not_ mean the
`asyncListener` callbacks will cease to fire on the events they've been
registered. Subsequently, any asynchronous events fired during the
execution of a callback will also have the same `asyncListener` callbacks
attached for future execution. For example:
callbacks will cease to fire on the events they've been registered.
Subsequently, any asynchronous events fired during the execution of a
callback will also have the same `AsyncListener` callbacks attached for
future execution. For example:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

Callbacks are associated with handles upon their creation, deregistering callbacks does not remove them from handles in flight, only they won't be added to any newly created handles. Therefore after deregistration your callbacks may fire.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think by:

[...] deregistering callbacks [...]

You meant:

[...] deregistering an AsyncListener [...]

Since "callbacks" feels like the callbacks passed to the asynchronous events that will fire in the future.

Feel slight confusion on the line:

[...] does not remove them from handles in flight, only they won't be added to any newly created handles.

Because I feel like that implies that the AsyncListener callbacks will not fire on any further creations of the asynchronous call stack on the handles where it was initially registered. When in fact the callbacks will continue to fire for all asynchronous "events" that originate from the handle which the AsyncListener had been added, before having been removed from the activeContext and the contextStack.

Maybe something like the following:

An active AsyncListener (i.e. passed to tracing.addAsyncListener()) is attached to an asynchronous handle, when the handle is created. Removing the AsyncListener simply prevents it from being added to any additional handles within the asynchronous scope where it was removed. Though this does not prevent the AsyncListener from propagating along the asynchronous call stack along the handle on which it was attached. For example:

var tracing = require('tracing');
var addAL = tracing.addAsyncListener;
var removeAL = tracing.removeAsyncListener;

// The AsyncListener will now begin to attach to any
var listener = addAL(callbacksObj);
setTimeout(function() {
  process.nextTick(function() {
    throw new Error('this will be caught');
  });
  setImmediate(function() {
    removeAL(listener);
    process.nextTick(function() {
      throw new Error('this will not be caught');
    });
    throw new Error('this will not be caught');
  });
  throw new Error('this will be caught');
});


var fs = require('fs');
var tracing = require('tracing');

var key = tracing.createAsyncListener({
create: function asyncListener() {
// Write directly to stdout or we'll enter a recursive loop
fs.writeSync(1, 'You summoned me?\n');
create: function() {
console.log('You summoned me?');
}
});

Expand Down Expand Up @@ -233,41 +343,12 @@ attached for future execution. For example:
The fact that we logged 4 asynchronous events is an implementation detail
of Node's [Timers][].

To stop capturing from a specific asynchronous event stack
`tracing.removeAsyncListener()` must be called from within the call
stack itself. For example:

var fs = require('fs');

var key = tracing.createAsyncListener({
create: function asyncListener() {
// Write directly to stdout or we'll enter a recursive loop
fs.writeSync(1, 'You summoned me?\n');
}
});

// We want to begin capturing async events some time in the future.
setTimeout(function() {
tracing.addAsyncListener(key);

// Perform a few additional async events.
setImmediate(function() {
// Stop capturing from this call stack.
tracing.removeAsyncListener(key);

process.nextTick(function() { });
});
}, 100);

// Output:
// You summoned me?

The user must be explicit and always pass the `AsyncListener` they wish
to remove. It is not possible to simply remove all listeners at once.


[EventEmitter]: events.html#events_class_events_eventemitter
[Timers]: timers.html
[`tracing.createAsyncListener()`]: #tracing_tracing_createasynclistener_asynclistener_callbacksobj_storagevalue
[`tracing.addAsyncListener()`]: #tracing_tracing_addasynclistener_asynclistener
[`tracing.removeAsyncListener()`]: #tracing_tracing_removeasynclistener_asynclistener
[`process.nextTick()`]: process.html#process_process_nexttick_callback
[`util.inspect()`]: util.html#util_util_inspect_object_options
[`process` exit event]: process.html#process_event_exit
Loading