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
This commit is contained in:
isaacs
2012-04-06 16:26:18 -07:00
parent a26bee8fa1
commit 963459d736
11 changed files with 879 additions and 3 deletions

View File

@@ -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');
});

View File

@@ -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');
});

198
test/simple/test-domain.js Normal file
View File

@@ -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)