summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorisaacs <i@izs.me>2012-04-06 16:26:18 -0700
committerisaacs <i@izs.me>2012-04-17 13:14:55 -0700
commit963459d736d6594de641aff4d8767da113359457 (patch)
tree38e18f82599ebed15fbed527c16f392efaceb056
parenta26bee8fa16bcbdaafdee516288c6f59a43376f5 (diff)
downloadnodejs-963459d736d6594de641aff4d8767da113359457.tar.gz
nodejs-963459d736d6594de641aff4d8767da113359457.tar.bz2
nodejs-963459d736d6594de641aff4d8767da113359457.zip
Domain feature
This is a squashed commit of the main work done on the domains-wip branch. The original commit messages are preserved for posterity: * Implicitly add EventEmitters to active domain * Implicitly add timers to active domain * domain: add members, remove ctor cb * Don't hijack bound callbacks for Domain error events * Add dispose method * Add domain.remove(ee) method * A test of multiple domains in process at once * Put the active domain on the process object * Only intercept error arg if explicitly requested * Typo * Don't auto-add new domains to the current domain While an automatic parent/child relationship is sort of neat, and leads to some nice error-bubbling characteristics, it also results in keeping a reference to every EE and timer created, unless domains are explicitly disposed of. * Explicitly adding one domain to another is still fine, of course. * Don't allow circular domain->domain memberships * Disposing of a domain removes it from its parent * Domain disposal turns functions into no-ops * More documentation of domains * More thorough dispose() semantics * An example using domains in an HTTP server * Don't handle errors on a disposed domain * Need to push, even if the same domain is entered multiple times * Array.push is too slow for the EE Ctor * lint domain * domain: docs * Also call abort and destroySoon to clean up event emitters * domain: Wrap destroy methods in a try/catch * Attach tick callbacks to active domain * domain: Only implicitly bind timers, not explicitly * domain: Don't fire timers when disposed. * domain: Simplify naming so that MakeCallback works on Timers * Add setInterval and nextTick to domain test * domain: Make stack private
-rw-r--r--doc/api/_toc.markdown1
-rw-r--r--doc/api/all.markdown1
-rw-r--r--doc/api/domain.markdown181
-rw-r--r--lib/domain.js233
-rw-r--r--lib/events.js31
-rw-r--r--lib/timers.js9
-rw-r--r--node.gyp1
-rw-r--r--src/node.js12
-rw-r--r--test/simple/test-domain-http-server.js115
-rw-r--r--test/simple/test-domain-multi.js100
-rw-r--r--test/simple/test-domain.js198
11 files changed, 879 insertions, 3 deletions
diff --git a/doc/api/_toc.markdown b/doc/api/_toc.markdown
index 73e6b9bd5..0e90fe6c7 100644
--- a/doc/api/_toc.markdown
+++ b/doc/api/_toc.markdown
@@ -8,6 +8,7 @@
* [Process](process.html)
* [Utilities](util.html)
* [Events](events.html)
+* [Domain](domain.html)
* [Buffer](buffer.html)
* [Stream](stream.html)
* [Crypto](crypto.html)
diff --git a/doc/api/all.markdown b/doc/api/all.markdown
index 7b7f73695..c62526713 100644
--- a/doc/api/all.markdown
+++ b/doc/api/all.markdown
@@ -8,6 +8,7 @@
@include process
@include util
@include events
+@include domain
@include buffer
@include stream
@include crypto
diff --git a/doc/api/domain.markdown b/doc/api/domain.markdown
new file mode 100644
index 000000000..7e546fd53
--- /dev/null
+++ b/doc/api/domain.markdown
@@ -0,0 +1,181 @@
+# Domain
+
+ Stability: 1 - Experimental
+
+Domains provide a way to handle multiple different IO operations as a
+single group. If any of the event emitters or callbacks registered to a
+domain emit an `error` event, or throw an error, then the domain object
+will be notified, rather than losing the context of the error in the
+`process.on('uncaughtException')` handler, or causing the program to
+exit with an error code.
+
+This feature is new in Node version 0.8. It is a first pass, and is
+expected to change significantly in future versions. Please use it and
+provide feedback.
+
+Due to their experimental nature, the Domains features are disabled unless
+the `domain` module is loaded at least once. No domains are created or
+registered by default. This is by design, to prevent adverse effects on
+current programs. It is expected to be enabled by default in future
+Node.js versions.
+
+## Additions to Error objects
+
+<!-- type=misc -->
+
+Any time an Error object is routed through a domain, a few extra fields
+are added to it.
+
+* `error.domain` The domain that first handled the error.
+* `error.domain_emitter` The event emitter that emitted an 'error' event
+ with the error object.
+* `error.domain_bound` The callback function which was bound to the
+ domain, and passed an error as its first argument.
+* `error.domain_thrown` A boolean indicating whether the error was
+ thrown, emitted, or passed to a bound callback function.
+
+## Implicit Binding
+
+<!--type=misc-->
+
+If domains are in use, then all new EventEmitter objects (including
+Stream objects, requests, responses, etc.) will be implicitly bound to
+the active domain at the time of their creation.
+
+Additionally, callbacks passed to lowlevel event loop requests (such as
+to fs.open, or other callback-taking methods) will automatically be
+bound to the active domain. If they throw, then the domain will catch
+the error.
+
+In order to prevent excessive memory usage, Domain objects themselves
+are not implicitly added as children of the active domain. If they
+were, then it would be too easy to prevent request and response objects
+from being properly garbage collected.
+
+If you *want* to nest Domain objects as children of a parent Domain,
+then you must explicitly add them, and then dispose of them later.
+
+Implicit binding routes thrown errors and `'error'` events to the
+Domain's `error` event, but does not register the EventEmitter on the
+Domain, so `domain.dispose()` will not shut down the EventEmitter.
+Implicit binding only takes care of thrown errors and `'error'` events.
+
+## domain.create()
+
+* return: {Domain}
+
+Returns a new Domain object.
+
+## Class: Domain
+
+The Domain class encapsulates the functionality of routing errors and
+uncaught exceptions to the active Domain object.
+
+Domain is a child class of EventEmitter. To handle the errors that it
+catches, listen to its `error` event.
+
+### domain.members
+
+* {Array}
+
+An array of timers and event emitters that have been explicitly added
+to the domain.
+
+### domain.add(emitter)
+
+* `emitter` {EventEmitter | Timer} emitter or timer to be added to the domain
+
+Explicitly adds an emitter to the domain. If any event handlers called by
+the emitter throw an error, or if the emitter emits an `error` event, it
+will be routed to the domain's `error` event, just like with implicit
+binding.
+
+This also works with timers that are returned from `setInterval` and
+`setTimeout`. If their callback function throws, it will be caught by
+the domain 'error' handler.
+
+If the Timer or EventEmitter was already bound to a domain, it is removed
+from that one, and bound to this one instead.
+
+### domain.remove(emitter)
+
+* `emitter` {EventEmitter | Timer} emitter or timer to be removed from the domain
+
+The opposite of `domain.add(emitter)`. Removes domain handling from the
+specified emitter.
+
+### domain.bind(cb)
+
+* `cb` {Function} The callback function
+* return: {Function} The bound function
+
+The returned function will be a wrapper around the supplied callback
+function. When the returned function is called, any errors that are
+thrown will be routed to the domain's `error` event.
+
+#### Example
+
+ var d = domain.create();
+
+ function readSomeFile(filename, cb) {
+ fs.readFile(filename, d.bind(function(er, data) {
+ // if this throws, it will also be passed to the domain
+ return cb(er, JSON.parse(data));
+ }));
+ }
+
+ d.on('error', function(er) {
+ // an error occurred somewhere.
+ // if we throw it now, it will crash the program
+ // with the normal line number and stack message.
+ });
+
+### domain.intercept(cb)
+
+* `cb` {Function} The callback function
+* return: {Function} The intercepted function
+
+This method is almost identical to `domain.bind(cb)`. However, in
+addition to catching thrown errors, it will also intercept `Error`
+objects sent as the first argument to the function.
+
+In this way, the common `if (er) return cb(er);` pattern can be replaced
+with a single error handler in a single place.
+
+#### Example
+
+ var d = domain.create();
+
+ function readSomeFile(filename, cb) {
+ fs.readFile(filename, d.intercept(function(er, data) {
+ // if this throws, it will also be passed to the domain
+ // additionally, we know that 'er' will always be null,
+ // so the error-handling logic can be moved to the 'error'
+ // event on the domain instead of being repeated throughout
+ // the program.
+ return cb(er, JSON.parse(data));
+ }));
+ }
+
+ d.on('error', function(er) {
+ // an error occurred somewhere.
+ // if we throw it now, it will crash the program
+ // with the normal line number and stack message.
+ });
+
+### domain.dispose()
+
+The dispose method destroys a domain, and makes a best effort attempt to
+clean up any and all IO that is associated with the domain. Streams are
+aborted, ended, closed, and/or destroyed. Timers are cleared.
+Explicitly bound callbacks are no longer called. Any error events that
+are raised as a result of this are ignored.
+
+The intention of calling `dispose` is generally to prevent cascading
+errors when a critical part of the Domain context is found to be in an
+error state.
+
+Note that IO might still be performed. However, to the highest degree
+possible, once a domain is disposed, further errors from the emitters in
+that set will be ignored. So, even if some remaining actions are still
+in flight, Node.js will not communicate further about them.
diff --git a/lib/domain.js b/lib/domain.js
new file mode 100644
index 000000000..d7a71ed1f
--- /dev/null
+++ b/lib/domain.js
@@ -0,0 +1,233 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var util = require('util');
+var events = require('events');
+var EventEmitter = events.EventEmitter;
+var inherits = util.inherits;
+
+// methods that are called when trying to shut down expliclitly bound EEs
+var endMethods = [ 'end', 'abort', 'destroy', 'destroySoon' ];
+
+// communicate with events module, but don't require that
+// module to have to load this one, since this module has
+// a few side effects.
+events.usingDomains = true;
+
+exports.Domain = Domain;
+
+exports.create = exports.createDomain = function(cb) {
+ return new Domain(cb);
+};
+
+// it's possible to enter one domain while already inside
+// another one. the stack is each entered domain.
+var stack = [];
+// the active domain is always the one that we're currently in.
+exports.active = null;
+
+
+// loading this file the first time sets up the global
+// uncaughtException handler.
+process.on('uncaughtException', uncaughtHandler);
+
+function uncaughtHandler(er) {
+ // if there's an active domain, then handle this there.
+ // Note that if this error emission throws, then it'll just crash.
+ if (exports.active && !exports.active._disposed) {
+ decorate(er, {
+ domain: exports.active,
+ domain_thrown: true
+ });
+ exports.active.emit('error', er);
+ } else if (process.listeners('uncaughtException').length === 1) {
+ // if there are other handlers, then they'll take care of it.
+ // but if not, then we need to crash now.
+ throw er;
+ }
+}
+
+inherits(Domain, EventEmitter);
+
+function Domain() {
+ EventEmitter.apply(this);
+
+ this.members = [];
+}
+
+Domain.prototype.enter = function() {
+ if (this._disposed) return;
+
+ // note that this might be a no-op, but we still need
+ // to push it onto the stack so that we can pop it later.
+ exports.active = process.domain = this;
+ stack.push(this);
+};
+
+Domain.prototype.exit = function() {
+ if (this._disposed) return;
+
+ // exit all domains until this one.
+ var d;
+ do {
+ d = stack.pop();
+ } while (d && d !== this);
+
+ exports.active = stack[stack.length - 1];
+ process.domain = exports.active;
+};
+
+// note: this works for timers as well.
+Domain.prototype.add = function(ee) {
+ // disposed domains can't be used for new things.
+ if (this._disposed) return;
+
+ // already added to this domain.
+ if (ee.domain === this) return;
+
+ // has a domain already - remove it first.
+ if (ee.domain) {
+ ee.domain.remove(ee);
+ }
+
+ // check for circular Domain->Domain links.
+ // This causes bad insanity!
+ //
+ // For example:
+ // var d = domain.create();
+ // var e = domain.create();
+ // d.add(e);
+ // e.add(d);
+ // e.emit('error', er); // RangeError, stack overflow!
+ if (this.domain && (ee instanceof Domain)) {
+ for (var d = this.domain; d; d = d.domain) {
+ if (ee === d) return;
+ }
+ }
+
+ ee.domain = this;
+ this.members.push(ee);
+};
+
+Domain.prototype.remove = function(ee) {
+ ee.domain = null;
+ var index = this.members.indexOf(ee);
+ if (index !== -1) {
+ this.members.splice(index, 1);
+ }
+};
+
+Domain.prototype.run = function(fn) {
+ this.bind(fn)();
+};
+
+Domain.prototype.intercept = function(cb) {
+ return this.bind(cb, true);
+};
+
+Domain.prototype.bind = function(cb, interceptError) {
+ // if cb throws, catch it here.
+ var self = this;
+ var b = function() {
+ // disposing turns functions into no-ops
+ if (self._disposed) return;
+
+ if (this instanceof Domain) {
+ return cb.apply(this, arguments);
+ }
+
+ // only intercept first-arg errors if explicitly requested.
+ if (interceptError && arguments[0] &&
+ (arguments[0] instanceof Error)) {
+ var er = arguments[0];
+ decorate(er, {
+ domain_bound: cb,
+ domain_thrown: false,
+ domain: self
+ });
+ self.emit('error', er);
+ return;
+ }
+
+ self.enter();
+ var ret = cb.apply(this, arguments);
+ self.exit();
+ return ret;
+ };
+ b.domain = this;
+ return b;
+};
+
+Domain.prototype.dispose = function() {
+ if (this._disposed) return;
+
+ this.emit('dispose');
+
+ // remove error handlers.
+ this.removeAllListeners();
+ this.on('error', function() {});
+
+ // try to kill all the members.
+ // XXX There should be more consistent ways
+ // to shut down things!
+ this.members.forEach(function(m) {
+ // if it's a timeout or interval, cancel it.
+ clearTimeout(m);
+
+ // drop all event listeners.
+ if (m instanceof EventEmitter) {
+ m.removeAllListeners();
+ // swallow errors
+ m.on('error', function() {});
+ }
+
+ // Be careful!
+ // By definition, we're likely in error-ridden territory here,
+ // so it's quite possible that calling some of these methods
+ // might cause additional exceptions to be thrown.
+ endMethods.forEach(function(method) {
+ if (typeof m[method] === 'function') {
+ try {
+ m[method]();
+ } catch (er) {}
+ }
+ });
+
+ });
+
+ // remove from parent domain, if there is one.
+ if (this.domain) this.domain.remove(this);
+
+ // kill the references so that they can be properly gc'ed.
+ this.members.length = 0;
+
+ // finally, mark this domain as 'no longer relevant'
+ // so that it can't be entered or activated.
+ this._disposed = true;
+};
+
+
+function decorate(er, props) {
+ Object.keys(props).forEach(function(k, _, __) {
+ if (er.hasOwnProperty(k)) return;
+ er[k] = props[k];
+ });
+}
diff --git a/lib/events.js b/lib/events.js
index 05255ac3d..c4ab9d80a 100644
--- a/lib/events.js
+++ b/lib/events.js
@@ -21,7 +21,15 @@
var isArray = Array.isArray;
-function EventEmitter() { }
+function EventEmitter() {
+ if (exports.usingDomains) {
+ // if there is an active domain, then attach to it.
+ var domain = require('domain');
+ if (domain.active && !(this instanceof domain.Domain)) {
+ this.domain = domain.active;
+ }
+ }
+}
exports.EventEmitter = EventEmitter;
// By default EventEmitters will print a warning if more than
@@ -44,6 +52,15 @@ EventEmitter.prototype.emit = function() {
if (!this._events || !this._events.error ||
(isArray(this._events.error) && !this._events.error.length))
{
+ if (this.domain) {
+ var er = arguments[1];
+ er.domain_emitter = this;
+ er.domain = this.domain;
+ er.domain_thrown = false;
+ this.domain.emit('error', er);
+ return false;
+ }
+
if (arguments[1] instanceof Error) {
throw arguments[1]; // Unhandled 'error' event
} else {
@@ -58,6 +75,9 @@ EventEmitter.prototype.emit = function() {
if (!handler) return false;
if (typeof handler == 'function') {
+ if (this.domain) {
+ this.domain.enter();
+ }
switch (arguments.length) {
// fast cases
case 1:
@@ -76,9 +96,15 @@ EventEmitter.prototype.emit = function() {
for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
handler.apply(this, args);
}
+ if (this.domain) {
+ this.domain.exit();
+ }
return true;
} else if (isArray(handler)) {
+ if (this.domain) {
+ this.domain.enter();
+ }
var l = arguments.length;
var args = new Array(l - 1);
for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
@@ -87,6 +113,9 @@ EventEmitter.prototype.emit = function() {
for (var i = 0, l = listeners.length; i < l; i++) {
listeners[i].apply(this, args);
}
+ if (this.domain) {
+ this.domain.exit();
+ }
return true;
} else {
diff --git a/lib/timers.js b/lib/timers.js
index 97e5830e6..9e2336d05 100644
--- a/lib/timers.js
+++ b/lib/timers.js
@@ -93,12 +93,17 @@ function insert(item, msecs) {
// hack should be removed.
//
// https://github.com/joyent/node/issues/2631
+ if (first.domain) {
+ if (first.domain._disposed) continue;
+ first.domain.enter();
+ }
try {
first._onTimeout();
} catch (e) {
if (!process.listeners('uncaughtException').length) throw e;
process.emit('uncaughtException', e);
}
+ if (first.domain) first.domain.exit();
}
}
@@ -192,6 +197,8 @@ exports.setTimeout = function(callback, after) {
}
}
+ if (process.domain) timer.domain = process.domain;
+
exports.active(timer);
return timer;
@@ -213,6 +220,8 @@ exports.clearTimeout = function(timer) {
exports.setInterval = function(callback, repeat) {
var timer = new Timer();
+ if (process.domain) timer.domain = process.domain;
+
repeat = ~~repeat;
if (repeat < 1 || repeat > TIMEOUT_MAX) {
repeat = 1; // schedule on next tick, follows browser behaviour
diff --git a/node.gyp b/node.gyp
index 7fbf17532..9128041ab 100644
--- a/node.gyp
+++ b/node.gyp
@@ -23,6 +23,7 @@
'lib/cluster.js',
'lib/dgram.js',
'lib/dns.js',
+ 'lib/domain.js',
'lib/events.js',
'lib/freelist.js',
'lib/fs.js',
diff --git a/src/node.js b/src/node.js
index 3322df6d2..05c0dd8ec 100644
--- a/src/node.js
+++ b/src/node.js
@@ -235,7 +235,13 @@
nextTickQueue = [];
try {
- for (var i = 0; i < l; i++) q[i]();
+ for (var i = 0; i < l; i++) {
+ var tock = q[i];
+ var callback = tock.callback;
+ if (tock.domain) tock.domain.enter();
+ callback();
+ if (tock.domain) tock.domain.exit();
+ }
}
catch (e) {
if (i + 1 < l) {
@@ -249,7 +255,9 @@
};
process.nextTick = function(callback) {
- nextTickQueue.push(callback);
+ var tock = { callback: callback };
+ if (process.domain) tock.domain = process.domain;
+ nextTickQueue.push(tock);
process._needTickCallback();
};
};
diff --git a/test/simple/test-domain-http-server.js b/test/simple/test-domain-http-server.js
new file mode 100644
index 000000000..bd6336b57
--- /dev/null
+++ b/test/simple/test-domain-http-server.js
@@ -0,0 +1,115 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var domain = require('domain');
+var http = require('http');
+var assert = require('assert');
+var common = require('../common.js');
+
+var objects = { foo: 'bar', baz: {}, num: 42, arr: [1,2,3] };
+objects.baz.asdf = objects;
+
+var serverCaught = 0;
+var clientCaught = 0
+
+var server = http.createServer(function(req, res) {
+ var dom = domain.create();
+ dom.add(req);
+ dom.add(res);
+
+ dom.on('error', function(er) {
+ serverCaught++;
+ console.log('server error', er);
+ // try to send a 500. If that fails, oh well.
+ res.writeHead(500, {'content-type':'text/plain'});
+ res.end(er.stack || er.message || 'Unknown error');
+ });
+
+ var data;
+ dom.run(function() {
+ // Now, an action that has the potential to fail!
+ // if you request 'baz', then it'll throw a JSON circular ref error.
+ data = JSON.stringify(objects[req.url.replace(/[^a-z]/g, '')]);
+
+ // this line will throw if you pick an unknown key
+ assert(data !== undefined, 'Data should not be undefined');
+
+ res.writeHead(200);
+ res.end(data);
+ });
+});
+
+server.listen(common.PORT, next);
+
+function next() {
+ console.log('listening on localhost:%d', common.PORT);
+
+ // now hit it a few times
+ var dom = domain.create();
+ var requests = 0;
+ var responses = 0;
+
+ makeReq('/');
+ makeReq('/foo');
+ makeReq('/arr');
+ makeReq('/baz');
+ makeReq('/num');
+
+ function makeReq(p) {
+ requests++;
+
+ var dom = domain.create();
+ dom.on('error', function(er) {
+ clientCaught++;
+ console.log('client error', er);
+ // kill everything.
+ dom.dispose();
+ });
+
+ var req = http.get({ host: 'localhost', port: common.PORT, path: p });
+ dom.add(req);
+ req.on('response', function(res) {
+ responses++;
+ console.error('requests=%d responses=%d', requests, responses);
+ if (responses === requests) {
+ console.error('done, closing server');
+ // no more coming.
+ server.close();
+ }
+
+ dom.add(res);
+ var d = '';
+ res.on('data', function(c) {
+ d += c;
+ });
+ res.on('end', function() {
+ d = JSON.parse(d);
+ console.log('json!', d);
+ });
+ });
+ }
+}
+
+process.on('exit', function() {
+ assert.equal(serverCaught, 2);
+ assert.equal(clientCaught, 2);
+ console.log('ok');
+});
diff --git a/test/simple/test-domain-multi.js b/test/simple/test-domain-multi.js
new file mode 100644
index 000000000..f097c0652
--- /dev/null
+++ b/test/simple/test-domain-multi.js
@@ -0,0 +1,100 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+// Tests of multiple domains happening at once.
+
+var common = require('../common');
+var assert = require('assert');
+var domain = require('domain');
+var events = require('events');
+
+var caughtA = false;
+var caughtB = false;
+var caughtC = false;
+
+
+var a = domain.create();
+a.enter(); // this will be our "root" domain
+a.on('error', function(er) {
+ caughtA = true;
+ console.log('This should not happen');
+ throw er;
+});
+
+
+var http = require('http');
+var server = http.createServer(function (req, res) {
+ // child domain.
+ // implicitly added to a, because we're in a when
+ // it is created.
+ var b = domain.create();
+
+ // treat these EE objects as if they are a part of the b domain
+ // so, an 'error' event on them propagates to the domain, rather
+ // than being thrown.
+ b.add(req);
+ b.add(res);
+
+ b.on('error', function (er) {
+ caughtB = true;
+ console.error('Error encountered', er)
+ if (res) {
+ res.writeHead(500);
+ res.end('An error occurred');
+ }
+ // res.writeHead(500), res.destroy, etc.
+ server.close();
+ });
+
+ // XXX this bind should not be necessary.
+ // the write cb behavior in http/net should use an
+ // event so that it picks up the domain handling.
+ res.write('HELLO\n', b.bind(function() {
+ throw new Error('this kills domain B, not A');
+ }));
+
+}).listen(common.PORT);
+
+var c = domain.create();
+var req = http.get({ host: 'localhost', port: common.PORT })
+
+// add the request to the C domain
+c.add(req);
+
+req.on('response', function(res) {
+ console.error('got response');
+ // add the response object to the C domain
+ c.add(res);
+ res.pipe(process.stdout);
+});
+
+c.on('error', function(er) {
+ caughtC = true;
+ console.error('Error on c', er.message);
+});
+
+process.on('exit', function() {
+ assert.equal(caughtA, false);
+ assert.equal(caughtB, true)
+ assert.equal(caughtC, true)
+ console.log('ok - Errors went where they were supposed to go');
+});
diff --git a/test/simple/test-domain.js b/test/simple/test-domain.js
new file mode 100644
index 000000000..e20868ed0
--- /dev/null
+++ b/test/simple/test-domain.js
@@ -0,0 +1,198 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+// Simple tests of most basic domain functionality.
+
+var common = require('../common');
+var assert = require('assert');
+var domain = require('domain');
+var events = require('events');
+var caught = 0;
+var expectCaught = 8;
+
+var d = new domain.Domain();
+var e = new events.EventEmitter();
+
+d.on('error', function(er) {
+ console.error('caught', er);
+ switch (er.message) {
+ case 'emitted':
+ assert.equal(er.domain, d);
+ assert.equal(er.domain_emitter, e);
+ assert.equal(er.domain_thrown, false);
+ break;
+
+ case 'bound':
+ assert.ok(!er.domain_emitter);
+ assert.equal(er.domain, d);
+ assert.equal(er.domain_bound, fn);
+ assert.equal(er.domain_thrown, false);
+ break;
+
+ case 'thrown':
+ assert.ok(!er.domain_emitter);
+ assert.equal(er.domain, d);
+ assert.equal(er.domain_thrown, true);
+ break;
+
+ case "ENOENT, open 'this file does not exist'":
+ assert.equal(er.domain, d);
+ assert.equal(er.domain_thrown, false);
+ assert.equal(typeof er.domain_bound, 'function');
+ assert.ok(!er.domain_emitter);
+ assert.equal(er.code, 'ENOENT');
+ assert.equal(er.path, 'this file does not exist');
+ assert.equal(typeof er.errno, 'number');
+ break;
+
+ case "ENOENT, open 'stream for nonexistent file'":
+ assert.equal(typeof er.errno, 'number');
+ assert.equal(er.code, 'ENOENT');
+ assert.equal(er.path, 'stream for nonexistent file');
+ assert.equal(er.domain, d);
+ assert.equal(er.domain_emitter, fst);
+ assert.ok(!er.domain_bound);
+ assert.equal(er.domain_thrown, false);
+ break;
+
+ case 'implicit':
+ assert.equal(er.domain_emitter, implicit);
+ assert.equal(er.domain, d);
+ assert.equal(er.domain_thrown, false);
+ assert.ok(!er.domain_bound);
+ break;
+
+ case 'implicit timer':
+ assert.equal(er.domain, d);
+ assert.equal(er.domain_thrown, true);
+ assert.ok(!er.domain_emitter);
+ assert.ok(!er.domain_bound);
+ break;
+
+ case 'Cannot call method \'isDirectory\' of undefined':
+ assert.equal(er.domain, d);
+ assert.ok(!er.domain_emitter);
+ assert.ok(!er.domain_bound);
+ break;
+
+ default:
+ console.error('unexpected error, throwing %j', er.message);
+ throw er;
+ }
+
+ caught++;
+});
+
+process.on('exit', function() {
+ console.error('exit');
+ assert.equal(caught, expectCaught);
+ console.log('ok');
+});
+
+
+
+// Event emitters added to the domain have their errors routed.
+d.add(e);
+e.emit('error', new Error('emitted'));
+
+
+
+// get rid of the `if (er) return cb(er)` malarky, by intercepting
+// the cb functions to the domain, and using the intercepted function
+// as a callback instead.
+function fn(er) {
+ throw new Error('This function should never be called!');
+ process.exit(1);
+}
+
+var bound = d.intercept(fn);
+bound(new Error('bound'));
+
+
+
+// throwing in a bound fn is also caught,
+// even if it's asynchronous, by hitting the
+// global uncaughtException handler. This doesn't
+// require interception, since throws are always
+// caught by the domain.
+function thrower() {
+ throw new Error('thrown');
+}
+setTimeout(d.bind(thrower), 100);
+
+
+
+// Pass an intercepted function to an fs operation that fails.
+var fs = require('fs');
+fs.open('this file does not exist', 'r', d.intercept(function(er) {
+ console.error('should not get here!', er);
+ throw new Error('should not get here!');
+}, true));
+
+
+
+// catch thrown errors no matter how many times we enter the event loop
+// this only uses implicit binding, except for the first function
+// passed to d.run(). The rest are implicitly bound by virtue of being
+// set up while in the scope of the d domain.
+d.run(function() {
+ process.nextTick(function() {
+ var i = setInterval(function () {
+ clearInterval(i);
+ setTimeout(function() {
+ fs.stat('this file does not exist', function(er, stat) {
+ // uh oh! stat isn't set!
+ // pretty common error.
+ console.log(stat.isDirectory());
+ });
+ });
+ });
+ });
+});
+
+
+
+// implicit addition by being created within a domain-bound context.
+var implicit;
+
+d.run(function() {
+ implicit = new events.EventEmitter;
+});
+
+setTimeout(function() {
+ // escape from the domain, but implicit is still bound to it.
+ implicit.emit('error', new Error('implicit'));
+}, 10);
+
+
+
+// implicit addition of a timer created within a domain-bound context.
+d.run(function() {
+ setTimeout(function() {
+ throw new Error('implicit timer');
+ });
+});
+
+
+
+var fst = fs.createReadStream('stream for nonexistent file')
+d.add(fst)