node: change AsyncListener API

There was a flaw in the old API that has been fixed. Now the
asyncListener callback is now the "create" object property in the
callback object, and is optional.
This commit is contained in:
Trevor Norris 2014-01-02 16:40:30 -08:00
parent 13eb17f412
commit d9fc6af32a
22 changed files with 138 additions and 143 deletions

View File

@ -688,68 +688,68 @@ a diff reading, useful for benchmarks and measuring intervals:
The `AsyncListener` API is the JavaScript interface for the `AsyncWrap` The `AsyncListener` API is the JavaScript interface for the `AsyncWrap`
class which allows developers to be notified about key events in the class which allows developers to be notified about key events in the
lifetime of an asynchronous event. Node performs a lot of asynchronous lifetime of an asynchronous event. Node performs a lot of asynchronous
events internally, and significant use of this API will have a **dramatic events internally, and significant use of this API may have a
performance impact** on your application. **significant performance impact** on your application.
## process.createAsyncListener(asyncListener[, callbacksObj[, storageValue]]) ## process.createAsyncListener(callbacksObj[, userData])
* `asyncListener` {Function} callback fired when an asynchronous event is * `callbacksObj` {Object} Contains optional callbacks that will fire at
instantiated. specific times in the life cycle of the asynchronous event.
* `callbacksObj` {Object} optional callbacks that will fire at specific * `userData` {Value} a value that will be passed to all callbacks.
times in the lifetime of the asynchronous event.
* `storageValue` {Value} a value that will be passed as the first argument
when the `asyncListener` callback is run, and to all subsequent callback.
Returns a constructed `AsyncListener` object. Returns a constructed `AsyncListener` object.
To begin capturing asynchronous events pass the object to To begin capturing asynchronous events pass either the `callbacksObj` or
[`process.addAsyncListener()`][]. The same `AsyncListener` instance can and existing `AsyncListener` instance to [`process.addAsyncListener()`][].
only be added once to the active queue, and subsequent attempts to add the The same `AsyncListener` instance can only be added once to the active
instance will be ignored. queue, and subsequent attempts to add the instance will be ignored.
To stop capturing pass the object to [`process.removeAsyncListener()`][]. To stop capturing pass the `AsyncListener` instance to
This does _not_ mean the `AsyncListener` previously added will stop [`process.removeAsyncListener()`][]. This does _not_ mean the
triggering callbacks. Once attached to an asynchronous event it will `AsyncListener` previously added will stop triggering callbacks. Once
persist with the lifetime of the asynchronous call stack. attached to an asynchronous event it will persist with the lifetime of the
asynchronous call stack.
Explanation of function parameters: Explanation of function parameters:
`asyncListener(storageValue)`: 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
`process.createAsyncListener()`'s `storageValue` argument. If an initial
`storageValue` was passed when created, then `asyncListener()` will
receive that as a function argument.
`callbacksObj`: An `Object` which may contain three optional fields: `callbacksObj`: An `Object` which may contain three optional fields:
* `before(context, storageValue)`: A `Function` that is called immediately * `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
`process.createAsyncListener()`'s `userData` argument. If an initial
`userData` was passed when created, then `create()` will
receive that as a function argument.
* `before(context, userData)`: A `Function` that is called immediately
before the asynchronous callback is about to run. It will be passed both before the asynchronous callback is about to run. It will be passed both
the `context` (i.e. `this`) of the calling function and the `storageValue` the `context` (i.e. `this`) of the calling function and the `userData`
either returned from `asyncListener` or passed during construction (if either returned from `create()` or passed during construction (if
either occurred). either occurred).
* `after(context, storageValue)`: A `Function` called immediately after * `after(context, userData)`: A `Function` called immediately after
the asynchronous event's callback has run. Note this will not be called the asynchronous event's callback has run. Note this will not be called
if the callback throws and the error is not handled. if the callback throws and the error is not handled.
* `error(storageValue, error)`: A `Function` called if the event's * `error(userData, error)`: A `Function` called if the event's
callback threw. If `error` returns `true` then Node will assume the error callback threw. If this registered callback returns `true` then Node will
has been properly handled and resume execution normally. When multiple assume the error has been properly handled and resume execution normally.
`error()` callbacks have been registered, only **one** of those callbacks When multiple `error()` callbacks have been registered only **one** of
needs to return `true` for `AsyncListener` to accept that the error has those callbacks needs to return `true` for `AsyncListener` to accept that
been handled. the error has been handled, but all `error()` callbacks will always be run.
`storageValue`: A `Value` (i.e. anything) that will be, by default, `userData`: A `Value` (i.e. anything) that will be, by default,
attached to all new event instances. This will be overwritten if a `Value` attached to all new event instances. This will be overwritten if a `Value`
is returned by `asyncListener()`. is returned by `create()`.
Here is an example of overwriting the `storageValue`: Here is an example of overwriting the `userData`:
process.createAsyncListener(function listener(value) { process.createAsyncListener({
// value === true create: function listener(value) {
return false; // value === true
return false;
}, { }, {
before: function before(context, value) { before: function before(context, value) {
// value === false // value === false
@ -757,12 +757,12 @@ Here is an example of overwriting the `storageValue`:
}, true); }, true);
**Note:** The [EventEmitter][], while used to emit status of an asynchronous **Note:** The [EventEmitter][], while used to emit status of an asynchronous
event, is not itself asynchronous. So `asyncListener()` will not fire when event, is not itself asynchronous. So `create()` will not fire when
an event is added, and `before`/`after` will not fire when emitted an event is added, and `before`/`after` will not fire when emitted
callbacks are called. callbacks are called.
## process.addAsyncListener(asyncListener[, callbacksObj[, storageValue]]) ## process.addAsyncListener(callbacksObj[, userData])
## process.addAsyncListener(asyncListener) ## process.addAsyncListener(asyncListener)
Returns a constructed `AsyncListener` object and immediately adds it to Returns a constructed `AsyncListener` object and immediately adds it to
@ -774,36 +774,32 @@ object.
Example usage for capturing errors: Example usage for capturing errors:
var fs = require('fs');
var cntr = 0; var cntr = 0;
var key = process.addAsyncListener(function() { var key = process.addAsyncListener({
return { uid: cntr++ }; create: function onCreate() {
}, { return { uid: cntr++ };
},
before: function onBefore(context, storage) { before: function onBefore(context, storage) {
// Need to remove the listener while logging or will end up // Write directly to stdout or we'll enter a recursive loop
// with an infinite call loop. fs.writeSync(1, 'uid: ' + storage.uid + ' is about to run\n');
process.removeAsyncListener(key);
console.log('uid: %s is about to run', storage.uid);
process.addAsyncListener(key);
}, },
after: function onAfter(context, storage) { after: function onAfter(context, storage) {
process.removeAsyncListener(key); fs.writeSync(1, 'uid: ' + storage.uid + ' is about to run\n');
console.log('uid: %s is about to run', storage.uid);
process.addAsyncListener(key);
}, },
error: function onError(storage, err) { error: function onError(storage, err) {
// Handle known errors // Handle known errors
if (err.message === 'really, it\'s ok') { if (err.message === 'everything is fine') {
process.removeAsyncListener(key); fs.writeSync(1, 'handled error just threw:\n');
console.log('handled error just threw:'); fs.writeSync(1, err.stack + '\n');
console.log(err.stack);
process.addAsyncListener(key);
return true; return true;
} }
} }
}); });
process.nextTick(function() { process.nextTick(function() {
throw new Error('really, it\'s ok'); throw new Error('everything is fine');
}); });
// Output: // Output:
@ -820,16 +816,19 @@ Example usage for capturing errors:
Removes the `AsyncListener` from the listening queue. Removes the `AsyncListener` from the listening queue.
Removing the `AsyncListener` from the queue does _not_ mean asynchronous Removing the `AsyncListener` from the active queue does _not_ mean the
events called during its execution scope will stop firing callbacks. Once `asyncListener` callbacks will cease to fire on the events they've been
attached to an event it will persist for the entire asynchronous call registered. Subsequently, any asynchronous events fired during the
stack. For example: execution of a callback will also have the same `asyncListener` callbacks
attached for future execution. For example:
var key = process.createAsyncListener(function asyncListener() { var fs = require('fs');
// To log we must stop listening or we'll enter infinite recursion.
process.removeAsyncListener(key); var key = process.createAsyncListener({
console.log('You summoned me?'); create: function asyncListener() {
process.addAsyncListener(key); // 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. // We want to begin capturing async events some time in the future.
@ -861,11 +860,13 @@ To stop capturing from a specific asynchronous event stack
`process.removeAsyncListener()` must be called from within the call `process.removeAsyncListener()` must be called from within the call
stack itself. For example: stack itself. For example:
var key = process.createAsyncListener(function asyncListener() { var fs = require('fs');
// To log we must stop listening or we'll enter infinite recursion.
process.removeAsyncListener(key); var key = process.createAsyncListener({
console.log('You summoned me?'); create: function asyncListener() {
process.addAsyncListener(key); // 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. // We want to begin capturing async events some time in the future.

View File

@ -42,9 +42,6 @@ exports._stack = stack;
exports.active = null; exports.active = null;
function noop() { }
var listenerObj = { var listenerObj = {
error: function errorHandler(domain, er) { error: function errorHandler(domain, er) {
var caught = false; var caught = false;
@ -104,7 +101,7 @@ inherits(Domain, EventEmitter);
function Domain() { function Domain() {
EventEmitter.call(this); EventEmitter.call(this);
this.members = []; this.members = [];
this._listener = process.createAsyncListener(noop, listenerObj, this); this._listener = process.createAsyncListener(listenerObj, this);
} }
Domain.prototype.members = undefined; Domain.prototype.members = undefined;

View File

@ -311,18 +311,21 @@
inAsyncTick = true; inAsyncTick = true;
for (i = 0; i < asyncQueue.length; i++) { for (i = 0; i < asyncQueue.length; i++) {
queueItem = asyncQueue[i]; queueItem = asyncQueue[i];
if (!queueItem.callbacks.create) {
queue[i] = queueItem;
continue;
}
// Not passing "this" context because it hasn't actually been // Not passing "this" context because it hasn't actually been
// instantiated yet, so accessing some of the object properties // instantiated yet, so accessing some of the object properties
// can cause a segfault. // can cause a segfault.
// Passing the original value will allow users to manipulate the // Passing the original value will allow users to manipulate the
// original value object, while also allowing them to return a // original value object, while also allowing them to return a
// new value for current async call tracking. // new value for current async call tracking.
value = queueItem.listener(queueItem.value); value = queueItem.callbacks.create(queueItem.value);
if (typeof value !== 'undefined') { if (typeof value !== 'undefined') {
item = { item = {
callbacks: queueItem.callbacks, callbacks: queueItem.callbacks,
value: value, value: value,
listener: queueItem.listener,
uid: queueItem.uid uid: queueItem.uid
}; };
} else { } else {
@ -388,22 +391,19 @@
// Create new async listener object. Useful when instantiating a new // Create new async listener object. Useful when instantiating a new
// object and want the listener instance, but not add it to the stack. // object and want the listener instance, but not add it to the stack.
function createAsyncListener(listener, callbacks, value) { function createAsyncListener(callbacks, value) {
return { return {
callbacks: callbacks, callbacks: callbacks,
value: value, value: value,
listener: listener,
uid: uid++ uid: uid++
}; };
} }
// Add a listener to the current queue. // Add a listener to the current queue.
function addAsyncListener(listener, callbacks, value) { function addAsyncListener(callbacks, value) {
// Accept new listeners or previous created listeners. // Accept new listeners or previous created listeners.
if (typeof listener === 'function') if (typeof callbacks.uid !== 'number')
callbacks = createAsyncListener(listener, callbacks, value); callbacks = createAsyncListener(callbacks, value);
else
callbacks = listener;
var inQueue = false; var inQueue = false;
// The asyncQueue will be small. Probably always <= 3 items. // The asyncQueue will be small. Probably always <= 3 items.

View File

@ -30,8 +30,6 @@ var caught = 0;
var expectCaught = 0; var expectCaught = 0;
var exitCbRan = false; var exitCbRan = false;
function asyncL() { }
var callbacksObj = { var callbacksObj = {
error: function(value, er) { error: function(value, er) {
var idx = errorMsgs.indexOf(er.message); var idx = errorMsgs.indexOf(er.message);
@ -48,7 +46,7 @@ var callbacksObj = {
} }
}; };
var listener = process.createAsyncListener(asyncL, callbacksObj); var listener = process.createAsyncListener(callbacksObj);
process.on('exit', function(code) { process.on('exit', function(code) {
// Just in case. // Just in case.

View File

@ -33,17 +33,24 @@ function onAsync1() {
return 1; return 1;
} }
function onError(stor) {
results.push(stor);
return true;
}
var results = []; var results = [];
var asyncNoHandleError = { var asyncNoHandleError0 = {
error: function(stor) { create: onAsync0,
results.push(stor); error: onError
return true; };
} var asyncNoHandleError1 = {
create: onAsync1,
error: onError
}; };
var listeners = [ var listeners = [
process.addAsyncListener(onAsync0, asyncNoHandleError), process.addAsyncListener(asyncNoHandleError0),
process.addAsyncListener(onAsync1, asyncNoHandleError) process.addAsyncListener(asyncNoHandleError1)
]; ];
process.nextTick(function() { process.nextTick(function() {

View File

@ -22,8 +22,6 @@
var common = require('../common'); var common = require('../common');
var assert = require('assert'); var assert = require('assert');
function onAsync() {}
var results = []; var results = [];
var asyncNoHandleError = { var asyncNoHandleError = {
error: function(stor) { error: function(stor) {
@ -39,8 +37,8 @@ var asyncHandleError = {
}; };
var listeners = [ var listeners = [
process.addAsyncListener(onAsync, asyncHandleError), process.addAsyncListener(asyncHandleError),
process.addAsyncListener(onAsync, asyncNoHandleError) process.addAsyncListener(asyncNoHandleError)
]; ];
// Even if an error handler returns true, both should fire. // Even if an error handler returns true, both should fire.

View File

@ -30,16 +30,23 @@ function onAsync1() {
return 1; return 1;
} }
function onError(stor) {
results.push(stor);
}
var results = []; var results = [];
var asyncNoHandleError = { var asyncNoHandleError0 = {
error: function(stor) { create: onAsync0,
results.push(stor); error: onError
} };
var asyncNoHandleError1 = {
create: onAsync1,
error: onError
}; };
var listeners = [ var listeners = [
process.addAsyncListener(onAsync0, asyncNoHandleError), process.addAsyncListener(asyncNoHandleError0),
process.addAsyncListener(onAsync1, asyncNoHandleError) process.addAsyncListener(asyncNoHandleError1)
]; ];
var uncaughtFired = false; var uncaughtFired = false;

View File

@ -29,8 +29,6 @@ var errorMsgs = [];
var caught = 0; var caught = 0;
var expectCaught = 0; var expectCaught = 0;
function asyncL() { }
var callbacksObj = { var callbacksObj = {
error: function(value, er) { error: function(value, er) {
var idx = errorMsgs.indexOf(er.message); var idx = errorMsgs.indexOf(er.message);
@ -47,7 +45,7 @@ var callbacksObj = {
} }
}; };
var listener = process.addAsyncListener(asyncL, callbacksObj); var listener = process.addAsyncListener(callbacksObj);
process.on('exit', function(code) { process.on('exit', function(code) {
process.removeAsyncListener(listener); process.removeAsyncListener(listener);

View File

@ -23,7 +23,6 @@ var common = require('../common');
var assert = require('assert'); var assert = require('assert');
var once = 0; var once = 0;
function onAsync0() { }
var results = []; var results = [];
var handlers = { var handlers = {
@ -38,7 +37,7 @@ var handlers = {
} }
} }
var key = process.addAsyncListener(onAsync0, handlers); var key = process.addAsyncListener(handlers);
var uncaughtFired = false; var uncaughtFired = false;
process.on('uncaughtException', function(err) { process.on('uncaughtException', function(err) {

View File

@ -23,8 +23,6 @@ var common = require('../common');
var assert = require('assert'); var assert = require('assert');
var once = 0; var once = 0;
function onAsync0() { }
function onAsync1() { }
var results = []; var results = [];
var handlers = { var handlers = {
@ -52,8 +50,8 @@ var handlers1 = {
} }
var listeners = [ var listeners = [
process.addAsyncListener(onAsync0, handlers), process.addAsyncListener(handlers),
process.addAsyncListener(onAsync1, handlers1) process.addAsyncListener(handlers1)
]; ];
var uncaughtFired = false; var uncaughtFired = false;

View File

@ -23,7 +23,6 @@ var common = require('../common');
var assert = require('assert'); var assert = require('assert');
var once = 0; var once = 0;
function onAsync0() {}
var results = []; var results = [];
var handlers = { var handlers = {
@ -38,7 +37,7 @@ var handlers = {
} }
} }
var key = process.addAsyncListener(onAsync0, handlers); var key = process.addAsyncListener(handlers);
var uncaughtFired = false; var uncaughtFired = false;
process.on('uncaughtException', function(err) { process.on('uncaughtException', function(err) {

View File

@ -34,7 +34,7 @@ else
function runChild() { function runChild() {
var cntr = 0; var cntr = 0;
var key = process.addAsyncListener(function() { }, { var key = process.addAsyncListener({
error: function onError() { error: function onError() {
cntr++; cntr++;
throw new Error('onError'); throw new Error('onError');

View File

@ -33,8 +33,6 @@ var caught = 0;
var expectCaught = 0; var expectCaught = 0;
var exitCbRan = false; var exitCbRan = false;
function asyncL() { }
var callbacksObj = { var callbacksObj = {
error: function(value, er) { error: function(value, er) {
var idx = errorMsgs.indexOf(er.message); var idx = errorMsgs.indexOf(er.message);
@ -48,7 +46,7 @@ var callbacksObj = {
} }
}; };
var listener = process.createAsyncListener(asyncL, callbacksObj); var listener = process.createAsyncListener(callbacksObj);
process.on('exit', function(code) { process.on('exit', function(code) {
removeListener(listener); removeListener(listener);

View File

@ -27,8 +27,6 @@ var removeListener = process.removeAsyncListener;
var caught = []; var caught = [];
var expect = []; var expect = [];
function asyncL(a) {}
var callbacksObj = { var callbacksObj = {
error: function(value, er) { error: function(value, er) {
process._rawDebug('caught', er.message); process._rawDebug('caught', er.message);
@ -37,7 +35,7 @@ var callbacksObj = {
} }
}; };
var listener = process.createAsyncListener(asyncL, callbacksObj); var listener = process.createAsyncListener(callbacksObj);
process.on('exit', function(code) { process.on('exit', function(code) {
removeListener(listener); removeListener(listener);

View File

@ -23,6 +23,9 @@ var common = require('../common');
var assert = require('assert'); var assert = require('assert');
var val; var val;
var callbacks = { var callbacks = {
create: function() {
return 42;
},
before: function() { before: function() {
process.removeAsyncListener(listener); process.removeAsyncListener(listener);
process.addAsyncListener(listener); process.addAsyncListener(listener);
@ -32,14 +35,12 @@ var callbacks = {
} }
}; };
var listener = process.addAsyncListener(function() { var listener = process.addAsyncListener(callbacks);
return 66;
}, callbacks);
process.nextTick(function() {}); process.nextTick(function() {});
process.on('exit', function(status) { process.on('exit', function(status) {
process.removeAsyncListener(listener); process.removeAsyncListener(listener);
assert.equal(status, 0); assert.equal(status, 0);
assert.equal(val, 66); assert.equal(val, 42);
}); });

View File

@ -27,7 +27,7 @@ var net = require('net');
// TODO(trevnorris): Test has the flaw that it's not checking if the async // TODO(trevnorris): Test has the flaw that it's not checking if the async
// flag has been removed on the class instance. Though currently there's // flag has been removed on the class instance. Though currently there's
// no way to do that. // no way to do that.
var listener = process.addAsyncListener(function() { }); var listener = process.addAsyncListener({ create: function() { }});
// Test timers // Test timers

View File

@ -23,8 +23,6 @@ var common = require('../common');
var assert = require('assert'); var assert = require('assert');
var set = 0; var set = 0;
function onAsync0() { }
var asyncNoHandleError = { var asyncNoHandleError = {
before: function() { before: function() {
set++; set++;
@ -34,7 +32,7 @@ var asyncNoHandleError = {
} }
} }
var key = process.addAsyncListener(onAsync0, asyncNoHandleError); var key = process.addAsyncListener(asyncNoHandleError);
process.removeAsyncListener(key); process.removeAsyncListener(key);

View File

@ -31,7 +31,7 @@ var callbacks = {
} }
}; };
var listener = process.addAsyncListener(function() {}, callbacks); var listener = process.addAsyncListener(callbacks);
process.nextTick(function() {}); process.nextTick(function() {});

View File

@ -22,8 +22,6 @@
var common = require('../common'); var common = require('../common');
var assert = require('assert'); var assert = require('assert');
function onAsync0() { }
var set = 0; var set = 0;
var asyncNoHandleError = { var asyncNoHandleError = {
error: function() { error: function() {
@ -31,7 +29,7 @@ var asyncNoHandleError = {
} }
} }
var key = process.addAsyncListener(onAsync0, asyncNoHandleError); var key = process.addAsyncListener(asyncNoHandleError);
process.nextTick(function() { process.nextTick(function() {
throw 1; throw 1;

View File

@ -22,8 +22,6 @@
var common = require('../common'); var common = require('../common');
var assert = require('assert'); var assert = require('assert');
function onAsync0() { }
var set = 0; var set = 0;
var asyncNoHandleError = { var asyncNoHandleError = {
before: function() { before: function() {
@ -34,7 +32,7 @@ var asyncNoHandleError = {
} }
} }
var key = process.addAsyncListener(onAsync0, asyncNoHandleError); var key = process.addAsyncListener(asyncNoHandleError);
process.nextTick(function() { }); process.nextTick(function() { });

View File

@ -31,7 +31,7 @@ var assert = require('assert');
var cntr = 0; var cntr = 0;
process.addAsyncListener(function() { }, { process.addAsyncListener({
before: function() { before: function() {
if (++cntr > 1) { if (++cntr > 1) {
// Can't throw since uncaughtException will also catch that. // Can't throw since uncaughtException will also catch that.

View File

@ -30,11 +30,13 @@ var removeListener = process.removeAsyncListener;
var actualAsync = 0; var actualAsync = 0;
var expectAsync = 0; var expectAsync = 0;
function onAsync() { var callbacks = {
actualAsync++; create: function onAsync() {
} actualAsync++;
}
};
var listener = process.createAsyncListener(onAsync); var listener = process.createAsyncListener(callbacks);
process.on('exit', function() { process.on('exit', function() {
process._rawDebug('expected', expectAsync); process._rawDebug('expected', expectAsync);