inits
Version:
Init system for Node.js.
440 lines (413 loc) • 8.75 kB
JavaScript
;
/**
* Init system for Node.js, main file.
* (C) 2015 Alex Fernández.
*/
// requires
require('prototypes');
var Log = require('log');
var util = require('util');
var events = require('events');
var domain = require('domain');
var queueLib = require('./queueLib.js');
// globals
var globalDomain = domain.create();
// constants
var EVENTS = ['init', 'start', 'stop', 'finish'];
var DEFAULT_OPTIONS = {
catchSignals: true,
catchErrors: true,
exitProcess: true,
showErrors: true,
showTraces: false,
logTimes: true,
stopOnError: true,
maxTaskTimeSec: 10,
stopInParallel: false,
};
function InitSystem()
{
this.taskQueues = {};
this.flags = {};
this.standaloneTask = null;
this.startingUp = false;
this.shuttingDown = false;
this.finished = false;
this.phase = 'pre';
this.log = new Log('info');
this.options = {};
this.options.overwriteWith(DEFAULT_OPTIONS);
}
util.inherits(InitSystem, events.EventEmitter);
InitSystem.prototype.startup = function()
{
var self = this;
EVENTS.forEach(function(event)
{
self.taskQueues[event] = queueLib.create(event);
self[event] = self.getAdder(event);
});
events.EventEmitter.call(self);
setImmediate(function()
{
setTimeout(function()
{
self.reallyStartup();
}, 0);
});
};
InitSystem.prototype.getAdder = function(event)
{
var self = this;
return function(order, task)
{
if (self.flags[event])
{
return self.manageInternalError('Could not add ' + event + ' to queue after it has run: ' + new Error().stack);
}
if (typeof order == 'function')
{
task = order;
order = null;
}
if (!task)
{
return self.manageInternalError('Could not add empty task to queue ' + event);
}
self.taskQueues[event].add(order, task);
};
};
InitSystem.prototype.reallyStartup = function()
{
var self = this;
if (self.startingUp)
{
return self.manageInternalError('Could not start up again');
}
self.startingUp = true;
var start = Date.now();
self.emit('startup');
self.setupErrors();
self.runAll('init', function(error)
{
if (error)
{
return self.manageInternalError('Could not run init tasks: ' + error);
}
self.runAll('start', function(error)
{
if (error)
{
return self.manageInternalError('Could not run start tasks: ' + error);
}
if (self.options.logTimes)
{
var elapsedSeconds = (Date.now() - start) / 1000;
self.log.info('Initialization took %s seconds', elapsedSeconds.toFixed(1));
}
self.startingUp = false;
self.emit('ready');
});
});
};
InitSystem.prototype.manageInternalError = function(error)
{
var self = this;
if (self.listeners('error').length > 0)
{
self.emit('error', error);
}
if (self.options.showTraces)
{
error = new Error(error).stack;
}
self.startShutdownByError(util.format('Error in phase %s: %s', self.phase, error));
};
InitSystem.prototype.runAll = function(event, callback)
{
var self = this;
if (self.flags[event])
{
return callback('Tasks for ' + event + ' already run');
}
if (self.taskQueues[event].remaining())
{
self.log.debug('Running queue for %s: %s tasks', event, self.taskQueues[event].remaining());
}
self.phase = event;
self.emitInProgress(event);
var runner = 'runQueue';
if (self.options[event + 'InParallel'])
{
runner = 'runInParallel';
}
self[runner](self.taskQueues[event], function(error)
{
if (error)
{
return callback(error);
}
self.flags[event] = true;
self.emit(event + 'ed');
return callback(null);
});
};
InitSystem.prototype.emitInProgress = function(event)
{
var self = this;
self.emit(event + 'ing');
if (event.endsWith('p'))
{
self.emit(event + 'ping');
}
};
InitSystem.prototype.runQueue = function(queue, callback)
{
var self = this;
if (!queue.remaining())
{
return callback(null);
}
var current = queue.next();
self.run(current, function(error)
{
if (error)
{
if (self.options.showErrors)
{
self.log.error('Error in phase %s: %s', self.phase, error);
}
if (self.options.stopOnError)
{
return callback(error);
}
}
return self.runQueue(queue, callback);
});
};
InitSystem.prototype.runInParallel = function(queue, callback)
{
var self = this;
var total = queue.remaining();
if (!total)
{
return callback(null);
}
var received = 0;
while (queue.remaining())
{
var task = queue.next();
self.run(task, function(error)
{
if (error)
{
if (self.options.showErrors)
{
self.log.error('Error in phase %s: %s', self.phase, error);
}
if (self.options.stopOnError && received < total)
{
received += total;
return callback(error);
}
}
received += 1;
if (received == total)
{
return callback(null);
}
});
}
};
InitSystem.prototype.run = function(current, callback)
{
var self = this;
self.log.debug('Running task: %s', current.name);
var timeout = setTimeout(function()
{
self.log.warning('Task %s is taking more than %s seconds', current.name, self.options.maxTaskTimeSec);
}, self.options.maxTaskTimeSec * 1000);
timeout.unref();
globalDomain.run(function()
{
current(function(error)
{
clearTimeout(timeout);
callback(error);
});
});
};
InitSystem.prototype.setupErrors = function()
{
var self = this;
if (self.options.catchErrors)
{
globalDomain.on('error', function(error)
{
self.manageCrash('error', error);
});
process.on('uncaughtException', function(error)
{
self.manageCrash('uncaught exception', error);
});
process.on('beforeExit', function()
{
if (!self.shuttingDown)
{
self.shutdown();
}
});
process.on('exit', function()
{
if (!self.finished && self.options.showErrors)
{
self.log.warning('Unexpected exit, please see documentation: https://github.com/alexfernandez/inits#unexpected-exit');
}
});
}
if (self.options.catchSignals)
{
process.on('SIGINT', function()
{
self.log.notice('User pressed control-C');
self.signalled();
});
process.on('SIGTERM', function()
{
self.log.notice('Process killed');
self.signalled();
});
}
};
InitSystem.prototype.manageCrash = function(type, error)
{
var self = this;
if (!self.options.catchErrors)
{
return;
}
if (self.listeners('error').length > 0)
{
self.emit('error', 'Unexpected ' + type + ': ' + error);
}
if (error.stack)
{
error = error.stack;
}
self.startShutdownByError(util.format('Process crashed with %s: %s', type, error));
};
InitSystem.prototype.startShutdownByError = function(error)
{
var self = this;
if (self.options.showErrors)
{
self.log.error(error);
}
if (!self.startingUp && !self.shuttingDown)
{
return self.shutdown(1);
}
if (self.options.showErrors)
{
var when = 'starting up';
if (self.shuttingDown)
{
when = 'shutting down';
}
self.log.warning('Error while %s: exiting', when);
}
if (self.options.exitProcess)
{
self.finished = true;
process.exit(1);
}
};
InitSystem.prototype.signalled = function()
{
var self = this;
if (!self.options.catchSignals)
{
return;
}
self.shutdown();
};
/**
* Shut down. Pass an error code if you want process.exit(errorCode), otherwise 0.
*/
InitSystem.prototype.shutdown = function(errorCode)
{
var self = this;
if (self.shuttingDown)
{
if (self.options.showErrors)
{
self.log.warning('Could not shutdown again');
}
return;
}
self.shuttingDown = true;
var start = Date.now();
self.emit('shutdown');
self.runAll('stop', function(error)
{
if (error)
{
return self.manageInternalError('Could not run stop tasks: ' + error);
}
self.runAll('finish', function(error)
{
if (error)
{
return self.manageInternalError('Could not run finish tasks: ' + error);
}
if (self.options.logTimes)
{
var elapsedSeconds = (Date.now() - start) / 1000;
self.log.info('Shutdown took %s seconds', elapsedSeconds.toFixed(1));
}
self.emit('end');
self.finished = true;
if (self.options.exitProcess)
{
process.exit(errorCode || 0);
}
});
});
};
InitSystem.prototype.standalone = function(task)
{
var self = this;
if (self.standaloneTask)
{
return self.manageInternalError('Already have standalone task');
}
self.standaloneTask = task;
self.on('ready', self.runStandalone);
};
InitSystem.prototype.runStandalone = function()
{
var self = this;
self.phase = 'standalone';
setImmediate(function()
{
self.standaloneTask(function(error)
{
if (error)
{
return self.manageInternalError('Could not run standalone task: ' + error);
}
setImmediate(function()
{
self.shutdown();
});
});
});
};
function create()
{
var initSystem = new InitSystem();
initSystem.startup();
return initSystem;
}
module.exports = create();
module.exports.create = create;