summaryrefslogtreecommitdiff
path: root/lib
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 /lib
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
Diffstat (limited to 'lib')
-rw-r--r--lib/domain.js233
-rw-r--r--lib/events.js31
-rw-r--r--lib/timers.js9
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