Domain feature
authorisaacs <i@izs.me>
Fri, 6 Apr 2012 23:26:18 +0000 (16:26 -0700)
committerisaacs <i@izs.me>
Tue, 17 Apr 2012 20:14:55 +0000 (13:14 -0700)
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

doc/api/_toc.markdown
doc/api/all.markdown
doc/api/domain.markdown [new file with mode: 0644]
lib/domain.js [new file with mode: 0644]
lib/events.js
lib/timers.js
node.gyp
src/node.js
test/simple/test-domain-http-server.js [new file with mode: 0644]
test/simple/test-domain-multi.js [new file with mode: 0644]
test/simple/test-domain.js [new file with mode: 0644]

index 73e6b9bd58a038e8b921f00d8c53436b0ce446f4..0e90fe6c7658255858d7562a6be885051c15123c 100644 (file)
@@ -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)
index 7b7f7369564d1c195ee5bc37559e347edd058640..c62526713e499aae7dfb259e71eb35ec64bfb872 100644 (file)
@@ -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 (file)
index 0000000..7e546fd
--- /dev/null
@@ -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 (file)
index 0000000..d7a71ed
--- /dev/null
@@ -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];
+  });
+}
index 05255ac3d32c9f7d8fae7dd78d5d11c7c4a96e6f..c4ab9d80a0a03ba2aafa5809441f9535851556e8 100644 (file)
 
 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 {
index 97e5830e6e637e40f7f4449cee68abe53367d1c6..9e2336d058ba8e69bffb9f508b7173a11287f7c3 100644 (file)
@@ -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
index 7fbf175326be3084a4dee8c0fb6868321e1f3fce..9128041ab74cb35eb884b575d602d0d7c9b0916b 100644 (file)
--- 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',
index 3322df6d252e518312ea183607a3e8b5bf45e61e..05c0dd8ecd390932b11c6d0e1220001f3b9072af 100644 (file)
       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) {
     };
 
     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 (file)
index 0000000..bd6336b
--- /dev/null
@@ -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 (file)
index 0000000..f097c06
--- /dev/null
@@ -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 (file)
index 0000000..e20868e
--- /dev/null
@@ -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)