Start/Stop Improvements
- A Node will shutdown if there is an error starting a service as it would lead to cascading errors. - `node.start()` needs to be called, and nolonger is called automatically when the instance is created. - A service will only be added to node.services after it's started - Stopping services that are not started will gracefully continue. - Logging sync status of db will only apply if the service is started. - Debug log about a service without a route will always include the service name
This commit is contained in:
parent
b4ed29eabe
commit
60af86777f
@ -117,6 +117,12 @@ describe('Node Functionality', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node.start(function(err) {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
95
lib/node.js
95
lib/node.js
@ -10,7 +10,6 @@ var _ = bitcore.deps._;
|
|||||||
var index = require('./');
|
var index = require('./');
|
||||||
var log = index.log;
|
var log = index.log;
|
||||||
var Bus = require('./bus');
|
var Bus = require('./bus');
|
||||||
var BaseService = require('./service');
|
|
||||||
var errors = require('./errors');
|
var errors = require('./errors');
|
||||||
|
|
||||||
function Node(config) {
|
function Node(config) {
|
||||||
@ -18,8 +17,6 @@ function Node(config) {
|
|||||||
return new 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.errors = errors; // So services can use errors without having to have bitcore-node as a dependency
|
||||||
this.log = log;
|
this.log = log;
|
||||||
this.network = null;
|
this.network = null;
|
||||||
@ -38,13 +35,6 @@ function Node(config) {
|
|||||||
|
|
||||||
this._setNetwork(config);
|
this._setNetwork(config);
|
||||||
|
|
||||||
this.start(function(err) {
|
|
||||||
if(err) {
|
|
||||||
return self.emit('error', err);
|
|
||||||
}
|
|
||||||
self.emit('ready');
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
util.inherits(Node, EventEmitter);
|
util.inherits(Node, EventEmitter);
|
||||||
@ -137,35 +127,53 @@ Node.prototype.getServiceOrder = function() {
|
|||||||
return stack;
|
return stack;
|
||||||
};
|
};
|
||||||
|
|
||||||
Node.prototype._instantiateService = function(service) {
|
Node.prototype._startService = function(serviceInfo, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
$.checkState(_.isObject(service.config));
|
$.checkState(_.isObject(serviceInfo.config));
|
||||||
$.checkState(!service.config.node);
|
$.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.node = this;
|
||||||
config.name = service.name;
|
config.name = serviceInfo.name;
|
||||||
var mod = new service.module(config);
|
var service = new serviceInfo.module(config);
|
||||||
|
|
||||||
// include in loaded services
|
service.start(function(err) {
|
||||||
this.services[service.name] = mod;
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
// 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);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
Node.prototype.start = function(callback) {
|
||||||
@ -175,15 +183,15 @@ Node.prototype.start = function(callback) {
|
|||||||
async.eachSeries(
|
async.eachSeries(
|
||||||
servicesOrder,
|
servicesOrder,
|
||||||
function(service, next) {
|
function(service, next) {
|
||||||
log.info('Starting ' + service.name);
|
self._startService(service, next);
|
||||||
try {
|
},
|
||||||
self._instantiateService(service);
|
function(err) {
|
||||||
} catch(err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
self.services[service.name].start(next);
|
self.emit('ready');
|
||||||
},
|
callback();
|
||||||
callback
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -198,8 +206,13 @@ Node.prototype.stop = function(callback) {
|
|||||||
async.eachSeries(
|
async.eachSeries(
|
||||||
services,
|
services,
|
||||||
function(service, next) {
|
function(service, next) {
|
||||||
log.info('Stopping ' + service.name);
|
if (self.services[service.name]) {
|
||||||
self.services[service.name].stop(next);
|
log.info('Stopping ' + service.name);
|
||||||
|
self.services[service.name].stop(next);
|
||||||
|
} else {
|
||||||
|
log.info('Stopping ' + service.name + ' (not started)');
|
||||||
|
setImmediate(next);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
callback
|
callback
|
||||||
);
|
);
|
||||||
|
|||||||
@ -83,27 +83,31 @@ function registerSyncHandlers(node, delay) {
|
|||||||
|
|
||||||
function logSyncStatus() {
|
function logSyncStatus() {
|
||||||
log.info(
|
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,
|
'Height:', node.services.db.tip.__height,
|
||||||
'Rate:', count/10, 'blocks per second'
|
'Rate:', count/10, 'blocks per second'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
node.on('synced', function() {
|
|
||||||
clearInterval(interval);
|
|
||||||
});
|
|
||||||
|
|
||||||
node.on('ready', function() {
|
node.on('ready', function() {
|
||||||
node.services.db.on('addblock', function(block) {
|
|
||||||
count++;
|
if (node.services.db) {
|
||||||
// Initialize logging if not already instantiated
|
node.on('synced', function() {
|
||||||
if (!interval) {
|
clearInterval(interval);
|
||||||
interval = setInterval(function() {
|
logSyncStatus();
|
||||||
logSyncStatus();
|
});
|
||||||
count = 0;
|
node.services.db.on('addblock', function(block) {
|
||||||
}, delay);
|
count++;
|
||||||
}
|
// Initialize logging if not already instantiated
|
||||||
});
|
if (!interval) {
|
||||||
|
interval = setInterval(function() {
|
||||||
|
logSyncStatus();
|
||||||
|
count = 0;
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
node.on('stopping', function() {
|
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
|
* Will register event handlers to stop the node for `process` events
|
||||||
* `uncaughtException` and `SIGINT`.
|
* `uncaughtException` and `SIGINT`.
|
||||||
@ -133,15 +153,7 @@ function registerExitHandlers(_process, node) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (options.sigint) {
|
if (options.sigint) {
|
||||||
node.stop(function(err) {
|
cleanShutdown(_process, node);
|
||||||
if(err) {
|
|
||||||
log.error('Failed to stop services: ' + err);
|
|
||||||
return _process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('Halted');
|
|
||||||
_process.exit(0);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,6 +202,16 @@ function start(options) {
|
|||||||
log.error(err);
|
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;
|
return node;
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -235,3 +257,4 @@ module.exports.registerExitHandlers = registerExitHandlers;
|
|||||||
module.exports.registerSyncHandlers = registerSyncHandlers;
|
module.exports.registerSyncHandlers = registerSyncHandlers;
|
||||||
module.exports.setupServices = setupServices;
|
module.exports.setupServices = setupServices;
|
||||||
module.exports.spawnChildProcess = spawnChildProcess;
|
module.exports.spawnChildProcess = spawnChildProcess;
|
||||||
|
module.exports.cleanShutdown = cleanShutdown;
|
||||||
|
|||||||
@ -75,6 +75,7 @@ DB.prototype._setDataPath = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
DB.prototype.start = function(callback) {
|
DB.prototype.start = function(callback) {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
if (!fs.existsSync(this.dataPath)) {
|
if (!fs.existsSync(this.dataPath)) {
|
||||||
mkdirp.sync(this.dataPath);
|
mkdirp.sync(this.dataPath);
|
||||||
|
|||||||
@ -48,7 +48,7 @@ WebService.prototype.stop = function(callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
})
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
WebService.prototype.setupAllRoutes = function() {
|
WebService.prototype.setupAllRoutes = function() {
|
||||||
@ -60,7 +60,7 @@ WebService.prototype.setupAllRoutes = function() {
|
|||||||
this.app.use('/' + this.node.services[key].getRoutePrefix(), subApp);
|
this.app.use('/' + this.node.services[key].getRoutePrefix(), subApp);
|
||||||
this.node.services[key].setupRoutes(subApp, express);
|
this.node.services[key].setupRoutes(subApp, express);
|
||||||
} else {
|
} else {
|
||||||
log.info('Not setting up routes for ' + service.name);
|
log.debug('No routes defined for: ' + key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -65,7 +65,6 @@ describe('Bitcore Node', function() {
|
|||||||
var TestNode = proxyquire('../lib/node', {});
|
var TestNode = proxyquire('../lib/node', {});
|
||||||
TestNode.prototype.start = sinon.spy();
|
TestNode.prototype.start = sinon.spy();
|
||||||
var node = new TestNode(config);
|
var node = new TestNode(config);
|
||||||
TestNode.prototype.start.callCount.should.equal(1);
|
|
||||||
node._unloadedServices.length.should.equal(1);
|
node._unloadedServices.length.should.equal(1);
|
||||||
node._unloadedServices[0].name.should.equal('test1');
|
node._unloadedServices[0].name.should.equal('test1');
|
||||||
node._unloadedServices[0].module.should.equal(TestService);
|
node._unloadedServices[0].module.should.equal(TestService);
|
||||||
@ -105,29 +104,6 @@ describe('Bitcore Node', function() {
|
|||||||
should.exist(regtest);
|
should.exist(regtest);
|
||||||
node.network.should.equal(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() {
|
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() {
|
it('will instantiate an instance and load api methods', function() {
|
||||||
var node = new Node(baseConfig);
|
var node = new Node(baseConfig);
|
||||||
function TestService() {}
|
function TestService() {}
|
||||||
util.inherits(TestService, BaseService);
|
util.inherits(TestService, BaseService);
|
||||||
|
TestService.prototype.start = sinon.stub().callsArg(0);
|
||||||
TestService.prototype.getData = function() {};
|
TestService.prototype.getData = function() {};
|
||||||
TestService.prototype.getAPIMethods = function() {
|
TestService.prototype.getAPIMethods = function() {
|
||||||
return [
|
return [
|
||||||
@ -230,9 +207,28 @@ describe('Bitcore Node', function() {
|
|||||||
module: TestService,
|
module: TestService,
|
||||||
config: {}
|
config: {}
|
||||||
};
|
};
|
||||||
node._instantiateService(service);
|
node._startService(service, function(err) {
|
||||||
should.exist(node.services.testservice);
|
if (err) {
|
||||||
should.exist(node.getData);
|
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) {
|
node.start(function(err) {
|
||||||
should.exist(err);
|
should.exist(err);
|
||||||
err.message.should.match(/^Existing API method exists/);
|
err.message.should.match(/^Existing API method\(s\) exists\:/);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ describe('#start', function() {
|
|||||||
config: {}
|
config: {}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
TestNode.prototype.start = sinon.stub().callsArg(0);
|
||||||
TestNode.prototype.on = sinon.stub();
|
TestNode.prototype.on = sinon.stub();
|
||||||
TestNode.prototype.chain = {
|
TestNode.prototype.chain = {
|
||||||
on: sinon.stub()
|
on: sinon.stub()
|
||||||
@ -39,7 +40,29 @@ describe('#start', function() {
|
|||||||
node.should.be.instanceof(TestNode);
|
node.should.be.instanceof(TestNode);
|
||||||
done();
|
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) {
|
it('require each bitcore-node service with explicit config', function(done) {
|
||||||
var node;
|
var node;
|
||||||
var TestNode = function(options) {
|
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.on = sinon.stub();
|
||||||
TestNode.prototype.chain = {
|
TestNode.prototype.chain = {
|
||||||
on: sinon.stub()
|
on: sinon.stub()
|
||||||
|
|||||||
@ -124,6 +124,55 @@ describe('#start', function() {
|
|||||||
}, 35);
|
}, 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() {
|
describe('#registerExitHandlers', function() {
|
||||||
var log = {
|
var log = {
|
||||||
info: sinon.stub(),
|
info: sinon.stub(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user