UNPKG

dispo

Version:

Job and cronjob scheduler for Node

771 lines (621 loc) 22.2 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.getJob = undefined; var _assign = require('babel-runtime/core-js/object/assign'); var _assign2 = _interopRequireDefault(_assign); var _regenerator = require('babel-runtime/regenerator'); var _regenerator2 = _interopRequireDefault(_regenerator); var _getIterator2 = require('babel-runtime/core-js/get-iterator'); var _getIterator3 = _interopRequireDefault(_getIterator2); var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator'); var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); var _createClass2 = require('babel-runtime/helpers/createClass'); var _createClass3 = _interopRequireDefault(_createClass2); var _kue = require('kue'); var _kue2 = _interopRequireDefault(_kue); var _later = require('later'); var _later2 = _interopRequireDefault(_later); var _assert = require('assert'); var _assert2 = _interopRequireDefault(_assert); var _zmqPrebuilt = require('zmq-prebuilt'); var _zmqPrebuilt2 = _interopRequireDefault(_zmqPrebuilt); var _bluebird = require('bluebird'); var _lodash = require('lodash'); var _logger = require('./logger'); var _logger2 = _interopRequireDefault(_logger); var _mailer = require('./mailer'); var _mailer2 = _interopRequireDefault(_mailer); var _util = require('./util'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var NODE_ENV = process.env.NODE_ENV; /** * Default scheduler config */ var defaultConfig = { jobs: [], options: { port: 5555, logging: { path: 'log' }, mailer: null } }; /** * Promisified version of `kue.Job.rangeByType` * @type {Function} */ var getJobsByType = (0, _bluebird.promisify)(_kue2.default.Job.rangeByType); /** * Promisified version of `kue.Job.get` * * @type {Function} */ var getJob = exports.getJob = (0, _bluebird.promisify)(_kue2.default.Job.get); /** * Dispo Scheduler */ var Dispo = function () { /** * Creates an instance of Dispo. * * @memberOf Dispo * @param {Object} [config={}] */ function Dispo() { var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; (0, _classCallCheck3.default)(this, Dispo); this.config = (0, _lodash.merge)({}, defaultConfig, config); } /** * Initializes logging, socket bindings and the queue mechanism * * @memberOf Dispo * @return {Promise<void>} */ (0, _createClass3.default)(Dispo, [{ key: 'init', value: function () { var _ref = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee() { var _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, job; return _regenerator2.default.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: this._logger = new _logger2.default(this.config.options.logging); this._logger.init(); if (this.config.options.mailer) { this._mailer = new _mailer2.default(this.config.options.mailer); this._mailer.init(); } this._initSocket(); this._initQueue(this.config.options.queue); _iteratorNormalCompletion = true; _didIteratorError = false; _iteratorError = undefined; _context.prev = 8; _iterator = (0, _getIterator3.default)(this.config.jobs); case 10: if (_iteratorNormalCompletion = (_step = _iterator.next()).done) { _context.next = 17; break; } job = _step.value; _context.next = 14; return this.defineJob(job); case 14: _iteratorNormalCompletion = true; _context.next = 10; break; case 17: _context.next = 23; break; case 19: _context.prev = 19; _context.t0 = _context['catch'](8); _didIteratorError = true; _iteratorError = _context.t0; case 23: _context.prev = 23; _context.prev = 24; if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } case 26: _context.prev = 26; if (!_didIteratorError) { _context.next = 29; break; } throw _iteratorError; case 29: return _context.finish(26); case 30: return _context.finish(23); case 31: case 'end': return _context.stop(); } } }, _callee, this, [[8, 19, 23, 31], [24,, 26, 30]]); })); function init() { return _ref.apply(this, arguments); } return init; }() /** * @typedef {Object} DefineJobOptions * @property {String} name - Job name * @property {Function} fn - Job method that is executed when the job is run * @property {Number} attempts - Number of attempts a job is retried until marked as failure * @property {String} cron - Interval-based scheduling written in cron syntax, ignored when delay is given */ /** * Defines a job * * @memberOf Dispo * @param {DefineJobOptions} options - Job options * @return {Promise<void>} */ }, { key: 'defineJob', value: function () { var _ref2 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee2(_ref3) { var attempts = _ref3.attempts; var cron = _ref3.cron; var notifyOnError = _ref3.notifyOnError; var fn = _ref3.fn; var name = _ref3.name; var backoff = _ref3.backoff; var options; return _regenerator2.default.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: (0, _assert2.default)(name, 'Job must have a name'); options = { attempts: attempts, backoff: backoff }; this._queue.process(name, function (job, done) { return fn(job).then(done, done); }); if (notifyOnError) { options.notifyOnError = notifyOnError; } if (!cron) { _context2.next = 8; break; } options.cron = cron; _context2.next = 8; return this._queueJob(name, options); case 8: case 'end': return _context2.stop(); } } }, _callee2, this); })); function defineJob(_x2) { return _ref2.apply(this, arguments); } return defineJob; }() /** * Initializes the queue mechanism * * This is mostly done to set up queue level logging and to be able to automatically * queue the next runs of cronjobs after their previous runs have completed. * * @memberOf Dispo * @param {Object} [options={}] */ }, { key: '_initQueue', value: function _initQueue() { var _this = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; this._queue = _kue2.default.createQueue(options); this._queue.watchStuckJobs(5e3); if (NODE_ENV !== 'test') { this._queue.on('job start', function () { var _ref4 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee3(id) { return _regenerator2.default.wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: _context3.next = 2; return _this._handleStart(id); case 2: return _context3.abrupt('return', _context3.sent); case 3: case 'end': return _context3.stop(); } } }, _callee3, _this); })); return function (_x4) { return _ref4.apply(this, arguments); }; }()); this._queue.on('job failed attempt', function () { var _ref5 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee4(id, msg) { return _regenerator2.default.wrap(function _callee4$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: _context4.next = 2; return _this._handleFailedAttempt(id, msg); case 2: return _context4.abrupt('return', _context4.sent); case 3: case 'end': return _context4.stop(); } } }, _callee4, _this); })); return function (_x5, _x6) { return _ref5.apply(this, arguments); }; }()); this._queue.on('job failed', function () { var _ref6 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee5(id, msg) { return _regenerator2.default.wrap(function _callee5$(_context5) { while (1) { switch (_context5.prev = _context5.next) { case 0: _context5.next = 2; return _this._handleFailed(id, msg); case 2: return _context5.abrupt('return', _context5.sent); case 3: case 'end': return _context5.stop(); } } }, _callee5, _this); })); return function (_x7, _x8) { return _ref6.apply(this, arguments); }; }()); } this._queue.on('job complete', function () { var _ref7 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee6(id) { return _regenerator2.default.wrap(function _callee6$(_context6) { while (1) { switch (_context6.prev = _context6.next) { case 0: _context6.next = 2; return _this._handleComplete(id); case 2: return _context6.abrupt('return', _context6.sent); case 3: case 'end': return _context6.stop(); } } }, _callee6, _this); })); return function (_x9) { return _ref7.apply(this, arguments); }; }()); } /** * Logs job starts * * @memberOf Dispo * @param {Number} id - Job id */ }, { key: '_handleStart', value: function () { var _ref8 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee7(id) { return _regenerator2.default.wrap(function _callee7$(_context7) { while (1) { switch (_context7.prev = _context7.next) { case 0: _context7.next = 2; return this._logger.logStart(id); case 2: case 'end': return _context7.stop(); } } }, _callee7, this); })); function _handleStart(_x10) { return _ref8.apply(this, arguments); } return _handleStart; }() /** * Logs failed attempts * * @memberOf Dispo * @param {Number} id - Job id * @param {String} msg - Error message */ }, { key: '_handleFailedAttempt', value: function () { var _ref9 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee8(id, msg) { return _regenerator2.default.wrap(function _callee8$(_context8) { while (1) { switch (_context8.prev = _context8.next) { case 0: _context8.next = 2; return this._logger.logFailedAttempt(id, msg); case 2: case 'end': return _context8.stop(); } } }, _callee8, this); })); function _handleFailedAttempt(_x11, _x12) { return _ref9.apply(this, arguments); } return _handleFailedAttempt; }() /** * Logs failed jobs and sends notification emails if configured to do so * * @memberOf Dispo * @param {Number} id - Job id * @param {String} msg - Error message */ }, { key: '_handleFailed', value: function () { var _ref10 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee9(id, msg) { var job; return _regenerator2.default.wrap(function _callee9$(_context9) { while (1) { switch (_context9.prev = _context9.next) { case 0: _context9.next = 2; return this._logger.logFailure(id, msg); case 2: _context9.next = 4; return getJob(id); case 4: job = _context9.sent; if (!this._mailer) { _context9.next = 8; break; } _context9.next = 8; return this._mailer.sendMail(id, job.error()); case 8: case 'end': return _context9.stop(); } } }, _callee9, this); })); function _handleFailed(_x13, _x14) { return _ref10.apply(this, arguments); } return _handleFailed; }() /** * Logs completed jobs and re-queues them when defined as a cron * * @memberOf Dispo * @param {Number} id - Job id */ }, { key: '_handleComplete', value: function () { var _ref11 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee10(id) { var job; return _regenerator2.default.wrap(function _callee10$(_context10) { while (1) { switch (_context10.prev = _context10.next) { case 0: if (!(NODE_ENV !== 'test')) { _context10.next = 3; break; } _context10.next = 3; return this._logger.logComplete(id); case 3: _context10.next = 5; return getJob(id); case 5: job = _context10.sent; if (!job.data.cron) { _context10.next = 9; break; } _context10.next = 9; return this._queueJob(job.data.name, job.data); case 9: case 'end': return _context10.stop(); } } }, _callee10, this); })); function _handleComplete(_x15) { return _ref11.apply(this, arguments); } return _handleComplete; }() /** * Initialize ØMQ reply socket * * Received messages add new jobs to the queue when the given job is defined in * the job configuration * * @memberOf Dispo * @param {Number|String} [port=this.config.options.port] */ }, { key: '_initSocket', value: function _initSocket() { var _this2 = this; var port = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.config.options.port; var responder = _zmqPrebuilt2.default.socket('rep'); responder.on('message', function () { var _ref12 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee11(message) { var payload, job, data; return _regenerator2.default.wrap(function _callee11$(_context11) { while (1) { switch (_context11.prev = _context11.next) { case 0: payload = JSON.parse(message.toString()); job = _this2.config.jobs.filter(function (job) { return job.name === payload.name; }).shift(); if (!job) { _context11.next = 7; break; } data = (0, _lodash.omit)((0, _assign2.default)(payload, job), 'fn', 'name'); _context11.next = 6; return _this2._queueJob(job.name, data); case 6: responder.send('ok'); case 7: case 'end': return _context11.stop(); } } }, _callee11, _this2); })); return function (_x17) { return _ref12.apply(this, arguments); }; }()); responder.bind('tcp://*:' + port, function (err) { if (err) { throw new Error('Port binding: ' + err.message); } else if (NODE_ENV !== 'test') { _this2._logger.verbose('ZeroMQ rep socket listening on port ' + port); } }); } /** * Checks if a cronjob of the given `name` is already scheduled. * * @memberOf Dispo * @param {String} name - The jobs name * @return {Promise<Boolean>} */ }, { key: '_isCronScheduled', value: function () { var _ref13 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee12(name) { var jobsByType, cronjobsByType; return _regenerator2.default.wrap(function _callee12$(_context12) { while (1) { switch (_context12.prev = _context12.next) { case 0: _context12.next = 2; return getJobsByType(name, 'delayed', 0, 10000, 'desc'); case 2: jobsByType = _context12.sent; cronjobsByType = jobsByType.filter(function (job) { return !!job.data.cron; }); return _context12.abrupt('return', cronjobsByType.length > 0); case 5: case 'end': return _context12.stop(); } } }, _callee12, this); })); function _isCronScheduled(_x18) { return _ref13.apply(this, arguments); } return _isCronScheduled; }() /** * @typedef {Object} QueueJobOptions * @property {Number} attempts - Number of attempts a job is retried until marked as failure * @property {Number} delay - Delay job run by the given amount of miliseconds * @property {String} cron - Interval-based scheduling written in cron syntax, ignored when delay is given * @property {Boolean|{type:String,delay:Number}} backoff - Interval-based scheduling written in cron syntax, ignored when delay is given */ /** * Queues a job. * * @memberOf Dispo * @param {String} name - Job name * @param {QueueJobOptions} options - Job options * @return {Promise<void>} */ }, { key: '_queueJob', value: function () { var _ref14 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee13(name, options) { var _this3 = this; var attempts, cron, delay, backoff, isScheduled; return _regenerator2.default.wrap(function _callee13$(_context13) { while (1) { switch (_context13.prev = _context13.next) { case 0: attempts = options.attempts; cron = options.cron; delay = options.delay; backoff = options.backoff; (0, _assert2.default)(!!cron || !!delay, 'To queue a job, either `cron` or `delay` needs to be defined'); _context13.next = 7; return this._isCronScheduled(name); case 7: isScheduled = _context13.sent; if (!cron || !isScheduled) { (function () { var job = _this3._queue.create(name, (0, _assign2.default)(options, { name: name })).delay(delay || _this3._calculateDelay(cron)).attempts(attempts); if (backoff) { console.log(name, backoff); job.backoff((0, _util.parseBackoff)(backoff)); } job.save(function (err) { if (err) { throw new Error('Job save: ' + err.message); } else if (NODE_ENV !== 'test') { _this3._logger.logQueued(job); } }); })(); } case 9: case 'end': return _context13.stop(); } } }, _callee13, this); })); function _queueJob(_x19, _x20) { return _ref14.apply(this, arguments); } return _queueJob; }() /** * Calculates the delay until a cronjobs next run is due * * @memberOf Dispo * @param {String} cron - Interval-based scheduling written in cron syntax * @return {Number} Number of miliseconds until next cron run */ }, { key: '_calculateDelay', value: function _calculateDelay(cron) { return _later2.default.schedule(_later2.default.parse.cron(cron)).next(2).map(function (date) { return date.getTime() - Date.now(); }).filter(function (msec) { return msec > 500; }).shift(); } }]); return Dispo; }(); exports.default = Dispo;