diff options
author | isaacs <i@izs.me> | 2012-04-06 16:26:18 -0700 |
---|---|---|
committer | isaacs <i@izs.me> | 2012-04-17 13:14:55 -0700 |
commit | 963459d736d6594de641aff4d8767da113359457 (patch) | |
tree | 38e18f82599ebed15fbed527c16f392efaceb056 /lib | |
parent | a26bee8fa16bcbdaafdee516288c6f59a43376f5 (diff) | |
download | nodejs-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
Diffstat (limited to 'lib')
-rw-r--r-- | lib/domain.js | 233 | ||||
-rw-r--r-- | lib/events.js | 31 | ||||
-rw-r--r-- | lib/timers.js | 9 |
3 files changed, 272 insertions, 1 deletions
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 |