Merge pull request #199 from braydonf/start-error

Start/Stop Improvements
This commit is contained in:
Patrick Nagurny 2015-09-08 17:33:54 -04:00
commit 2469c6d3fb
8 changed files with 210 additions and 98 deletions

View File

@ -117,6 +117,12 @@ describe('Node Functionality', function() {
});
});
node.start(function(err) {
if (err) {
throw err;
}
});
});
});

View File

@ -10,7 +10,6 @@ var _ = bitcore.deps._;
var index = require('./');
var log = index.log;
var Bus = require('./bus');
var BaseService = require('./service');
var errors = require('./errors');
function Node(config) {
@ -18,8 +17,6 @@ function Node(config) {
return new Node(config);
}
var self = this;
this.errors = errors; // So services can use errors without having to have bitcore-node as a dependency
this.log = log;
this.network = null;
@ -38,13 +35,6 @@ function Node(config) {
this._setNetwork(config);
this.start(function(err) {
if(err) {
return self.emit('error', err);
}
self.emit('ready');
});
}
util.inherits(Node, EventEmitter);
@ -137,35 +127,53 @@ Node.prototype.getServiceOrder = function() {
return stack;
};
Node.prototype._instantiateService = function(service) {
Node.prototype._startService = function(serviceInfo, callback) {
var self = this;
$.checkState(_.isObject(service.config));
$.checkState(!service.config.node);
$.checkState(_.isObject(serviceInfo.config));
$.checkState(!serviceInfo.config.node);
$.checkState(!serviceInfo.config.name);
var config = service.config;
log.info('Starting ' + serviceInfo.name);
var config = serviceInfo.config;
config.node = this;
config.name = service.name;
var mod = new service.module(config);
config.name = serviceInfo.name;
var service = new serviceInfo.module(config);
// include in loaded services
this.services[service.name] = mod;
// add API methods
var methodData = mod.getAPIMethods();
methodData.forEach(function(data) {
var name = data[0];
var instance = data[1];
var method = data[2];
if (self[name]) {
throw new Error('Existing API method exists: ' + name);
} else {
self[name] = function() {
return method.apply(instance, arguments);
};
service.start(function(err) {
if (err) {
return callback(err);
}
// include in loaded services
self.services[serviceInfo.name] = service;
// add API methods
var methodData = service.getAPIMethods();
var methodNameConflicts = [];
methodData.forEach(function(data) {
var name = data[0];
var instance = data[1];
var method = data[2];
if (self[name]) {
methodNameConflicts.push(name);
} else {
self[name] = function() {
return method.apply(instance, arguments);
};
}
});
if (methodNameConflicts.length > 0) {
return callback(new Error('Existing API method(s) exists: ' + methodNameConflicts.join(', ')));
}
callback();
});
};
Node.prototype.start = function(callback) {
@ -175,15 +183,15 @@ Node.prototype.start = function(callback) {
async.eachSeries(
servicesOrder,
function(service, next) {
log.info('Starting ' + service.name);
try {
self._instantiateService(service);
} catch(err) {
self._startService(service, next);
},
function(err) {
if (err) {
return callback(err);
}
self.services[service.name].start(next);
},
callback
self.emit('ready');
callback();
}
);
};
@ -198,8 +206,13 @@ Node.prototype.stop = function(callback) {
async.eachSeries(
services,
function(service, next) {
log.info('Stopping ' + service.name);
self.services[service.name].stop(next);
if (self.services[service.name]) {
log.info('Stopping ' + service.name);
self.services[service.name].stop(next);
} else {
log.info('Stopping ' + service.name + ' (not started)');
setImmediate(next);
}
},
callback
);

View File

@ -83,27 +83,31 @@ function registerSyncHandlers(node, delay) {
function logSyncStatus() {
log.info(
'Sync Status: Tip:', node.services.db.tip.hash,
'Database Sync Status: Tip:', node.services.db.tip.hash,
'Height:', node.services.db.tip.__height,
'Rate:', count/10, 'blocks per second'
);
}
node.on('synced', function() {
clearInterval(interval);
});
node.on('ready', function() {
node.services.db.on('addblock', function(block) {
count++;
// Initialize logging if not already instantiated
if (!interval) {
interval = setInterval(function() {
logSyncStatus();
count = 0;
}, delay);
}
});
if (node.services.db) {
node.on('synced', function() {
clearInterval(interval);
logSyncStatus();
});
node.services.db.on('addblock', function(block) {
count++;
// Initialize logging if not already instantiated
if (!interval) {
interval = setInterval(function() {
logSyncStatus();
count = 0;
}, delay);
}
});
}
});
node.on('stopping', function() {
@ -111,6 +115,22 @@ function registerSyncHandlers(node, delay) {
});
}
/**
* Will shutdown a node and then the process
* @param {Object} _process - The Node.js process object
* @param {Node} node - The Bitcore Node instance
*/
function cleanShutdown(_process, node) {
node.stop(function(err) {
if(err) {
log.error('Failed to stop services: ' + err);
return _process.exit(1);
}
log.info('Halted');
_process.exit(0);
});
}
/**
* Will register event handlers to stop the node for `process` events
* `uncaughtException` and `SIGINT`.
@ -133,15 +153,7 @@ function registerExitHandlers(_process, node) {
});
}
if (options.sigint) {
node.stop(function(err) {
if(err) {
log.error('Failed to stop services: ' + err);
return _process.exit(1);
}
log.info('Halted');
_process.exit(0);
});
cleanShutdown(_process, node);
}
}
@ -190,6 +202,16 @@ function start(options) {
log.error(err);
});
node.start(function(err) {
if(err) {
log.error('Failed to start services');
if (err.stack) {
log.error(err.stack);
}
start.cleanShutdown(process, node);
}
});
return node;
}
@ -235,3 +257,4 @@ module.exports.registerExitHandlers = registerExitHandlers;
module.exports.registerSyncHandlers = registerSyncHandlers;
module.exports.setupServices = setupServices;
module.exports.spawnChildProcess = spawnChildProcess;
module.exports.cleanShutdown = cleanShutdown;

View File

@ -75,6 +75,7 @@ DB.prototype._setDataPath = function() {
};
DB.prototype.start = function(callback) {
var self = this;
if (!fs.existsSync(this.dataPath)) {
mkdirp.sync(this.dataPath);

View File

@ -48,7 +48,7 @@ WebService.prototype.stop = function(callback) {
}
callback();
})
});
};
WebService.prototype.setupAllRoutes = function() {
@ -60,7 +60,7 @@ WebService.prototype.setupAllRoutes = function() {
this.app.use('/' + this.node.services[key].getRoutePrefix(), subApp);
this.node.services[key].setupRoutes(subApp, express);
} else {
log.info('Not setting up routes for ' + service.name);
log.debug('No routes defined for: ' + key);
}
}
};
@ -186,4 +186,4 @@ WebService.prototype.socketMessageHandler = function(message, socketCallback) {
}
};
module.exports = WebService;
module.exports = WebService;

View File

@ -65,7 +65,6 @@ describe('Bitcore Node', function() {
var TestNode = proxyquire('../lib/node', {});
TestNode.prototype.start = sinon.spy();
var node = new TestNode(config);
TestNode.prototype.start.callCount.should.equal(1);
node._unloadedServices.length.should.equal(1);
node._unloadedServices[0].name.should.equal('test1');
node._unloadedServices[0].module.should.equal(TestService);
@ -105,29 +104,6 @@ describe('Bitcore Node', function() {
should.exist(regtest);
node.network.should.equal(regtest);
});
it('should emit error if an error occurred starting services', function(done) {
var config = {
datadir: 'testdir',
services: [
{
name: 'test1',
module: TestService
}
],
};
var TestNode = proxyquire('../lib/node', {});
TestNode.prototype.start = function(callback) {
setImmediate(function() {
callback(new Error('error'));
});
};
var node = new TestNode(config);
node.once('error', function(err) {
should.exist(err);
err.message.should.equal('error');
done();
});
});
});
describe('#openBus', function() {
@ -214,11 +190,12 @@ describe('Bitcore Node', function() {
});
});
describe('#_instantiateService', function() {
describe('#_startService', function() {
it('will instantiate an instance and load api methods', function() {
var node = new Node(baseConfig);
function TestService() {}
util.inherits(TestService, BaseService);
TestService.prototype.start = sinon.stub().callsArg(0);
TestService.prototype.getData = function() {};
TestService.prototype.getAPIMethods = function() {
return [
@ -230,9 +207,28 @@ describe('Bitcore Node', function() {
module: TestService,
config: {}
};
node._instantiateService(service);
should.exist(node.services.testservice);
should.exist(node.getData);
node._startService(service, function(err) {
if (err) {
throw err;
}
TestService.prototype.start.callCount.should.equal(1);
should.exist(node.services.testservice);
should.exist(node.getData);
});
});
it('will give an error from start', function() {
var node = new Node(baseConfig);
function TestService() {}
util.inherits(TestService, BaseService);
TestService.prototype.start = sinon.stub().callsArgWith(0, new Error('test'));
var service = {
name: 'testservice',
module: TestService,
config: {}
};
node._startService(service, function(err) {
err.message.should.equal('test');
});
});
});
@ -318,7 +314,7 @@ describe('Bitcore Node', function() {
node.start(function(err) {
should.exist(err);
err.message.should.match(/^Existing API method exists/);
err.message.should.match(/^Existing API method\(s\) exists\:/);
done();
});

View File

@ -18,6 +18,7 @@ describe('#start', function() {
config: {}
});
};
TestNode.prototype.start = sinon.stub().callsArg(0);
TestNode.prototype.on = sinon.stub();
TestNode.prototype.chain = {
on: sinon.stub()
@ -39,7 +40,29 @@ describe('#start', function() {
node.should.be.instanceof(TestNode);
done();
});
it('shutdown with an error from start', function(done) {
var TestNode = proxyquire('../../lib/node', {});
TestNode.prototype.start = function(callback) {
setImmediate(function() {
callback(new Error('error'));
});
};
var starttest = proxyquire('../../lib/scaffold/start', {
'../node': TestNode
});
starttest.cleanShutdown = sinon.stub();
starttest({
path: __dirname,
config: {
services: [],
datadir: './testdir'
}
});
setImmediate(function() {
starttest.cleanShutdown.callCount.should.equal(1);
done();
});
});
it('require each bitcore-node service with explicit config', function(done) {
var node;
var TestNode = function(options) {
@ -51,6 +74,7 @@ describe('#start', function() {
}
});
};
TestNode.prototype.start = sinon.stub().callsArg(0);
TestNode.prototype.on = sinon.stub();
TestNode.prototype.chain = {
on: sinon.stub()

View File

@ -124,6 +124,55 @@ describe('#start', function() {
}, 35);
});
});
describe('#cleanShutdown', function() {
it('will call node stop and process exit', function() {
var log = {
info: sinon.stub(),
error: sinon.stub()
};
var cleanShutdown = proxyquire('../../lib/scaffold/start', {
'../': {
log: log
}
}).cleanShutdown;
var node = {
stop: sinon.stub().callsArg(0)
};
var _process = {
exit: sinon.stub()
};
cleanShutdown(_process, node);
setImmediate(function() {
node.stop.callCount.should.equal(1);
_process.exit.callCount.should.equal(1);
_process.exit.args[0][0].should.equal(0);
});
});
it('will log error during shutdown and exit with status 1', function() {
var log = {
info: sinon.stub(),
error: sinon.stub()
};
var cleanShutdown = proxyquire('../../lib/scaffold/start', {
'../': {
log: log
}
}).cleanShutdown;
var node = {
stop: sinon.stub().callsArgWith(0, new Error('test'))
};
var _process = {
exit: sinon.stub()
};
cleanShutdown(_process, node);
setImmediate(function() {
node.stop.callCount.should.equal(1);
log.error.callCount.should.equal(1);
_process.exit.callCount.should.equal(1);
_process.exit.args[0][0].should.equal(1);
});
});
});
describe('#registerExitHandlers', function() {
var log = {
info: sinon.stub(),