From 963459d736d6594de641aff4d8767da113359457 Mon Sep 17 00:00:00 2001 From: isaacs Date: Fri, 6 Apr 2012 16:26:18 -0700 Subject: 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 --- doc/api/_toc.markdown | 1 + doc/api/all.markdown | 1 + doc/api/domain.markdown | 181 +++++++++++++++++++++++++ lib/domain.js | 233 +++++++++++++++++++++++++++++++++ lib/events.js | 31 ++++- lib/timers.js | 9 ++ node.gyp | 1 + src/node.js | 12 +- test/simple/test-domain-http-server.js | 115 ++++++++++++++++ test/simple/test-domain-multi.js | 100 ++++++++++++++ test/simple/test-domain.js | 198 ++++++++++++++++++++++++++++ 11 files changed, 879 insertions(+), 3 deletions(-) create mode 100644 doc/api/domain.markdown create mode 100644 lib/domain.js create mode 100644 test/simple/test-domain-http-server.js create mode 100644 test/simple/test-domain-multi.js create mode 100644 test/simple/test-domain.js 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 + + + +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 + + + +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) -- cgit v1.2.3