UNPKG

worker-server

Version:

Worker server to run jobs instructed by central server

234 lines (192 loc) 6.08 kB
/* -------------------- * worker-server module * Job constructor * ------------------*/ // Modules const {wrap: cow} = require('co-bluebird'), Locker = require('lock-queue'); // Imports const Errors = require('./errors'), Timer = require('./timer'); // Exports /** * Job constructor. * @param {Object} params - Job parameters * @param {Server} server - Server job is running on. */ function Job(params, server) { // Record details to job object this.id = params.jobId; delete params.jobId; this.params = params; this.server = server; this.workerName = params.worker; this.worker = server.workers[params.worker]; // Create logger this.log = server.log.child({jobId: this.id, worker: this.workerName}); // Create locker and timer for messaging this.locker = new Locker(); this.timer = new Timer(); // Create empty progress object this.progress = {}; // Flag as not complete or cancelled this.complete = false; this.cancelled = false; } module.exports = Job; /** * Start job * Returns promise that resolves when job completes (either success or fail). * Promise will never reject. * @returns {Promise} */ Job.prototype.start = cow(function*() { // Run job const {worker} = this; this.log('Starting job', { params: this.params, worker: {name: worker.name, version: worker.version} }); let result; try { const promise = this.run().cancellable(); this.promise = promise; result = yield promise; } catch (err) { // Job failed - record error this.log.warn('Job failed', err); yield this.failed(err); return; } // Job complete - record result yield this.done(result); }); /** * Run job. * Called by `.start()`. * @returns {Promise} - Resolves or rejects depending on success/fail of job */ Job.prototype.run = cow(function*() { // Get worker const {worker} = this; if (!worker) throw new Errors.Worker(`No worker with name '${this.workerName}'`); // Record initial progress message and start progress timer running this.sendProgress(); // Run worker return yield worker.run(this); }); /** * Job is done. * @param {Object} result - Result of job * @returns {Promise} - Resolves when job status recorded, rejects if failed to record */ Job.prototype.done = cow(function*(result) { this.progress.done = this.progress.total; this.progress.percent = 100; yield this.finished('Done', result); }); /** * Job is failed. * @param {Error} err - Error from job * @returns {Promise} - Resolves when job status recorded, rejects if failed to record */ Job.prototype.failed = cow(function*(err) { const status = (err instanceof Errors.Job.Cancelled) ? 'Cancelled' : 'Failed'; err = Object.assign({message: err.message, name: err.name, stack: err.stack}, err); yield this.finished(status, {error: err}); }); /** * Job is finished. * Called by `.done()` and `.failed()`. * @param {string} status - Status code i.e. 'Done', 'Failed', 'Cancelled' * @param {Error} data - Message payload * @returns {Promise} - Resolves when job status recorded, rejects if failed to record */ Job.prototype.finished = cow(function*(status, data) { // Flag job as complete this.complete = true; // Clear timer (so progress does not send in future) this.timer.clear(); // After any current message finished, send this message yield this.locker.lock(function*() { // Record job status yield this.recordStatus(status, data); // Delete record of job from server this.server.finishedJob(this); }, this); }); /** * Called when job progressed. * If first time `total` provided, progress message sent immediately. * Otherwise, progress recorded in memory for perioidic update sending. * @param {number} [done] - Amount of work done * @param {number} [total] - Total amount of work to do * @returns {undefined} */ Job.prototype.progressed = function(done, total) { const first = !this.progress.total && total; if (total !== undefined) this.progress.total = total; if (done !== undefined) this.progress.done = done; if (first) this.sendProgress(); }; /** * Send current progress to master server. * If recording progress message fails, will throw and crash process. * @returns {undefined} */ Job.prototype.sendProgress = function() { this._sendProgress().done(); }; /** * Send current progress to master server. * If recording progress message fails, will throw and crash process. * Schedules self to run again after interval. * @returns {Promise} - Resolves when progress recorded, rejects if failed to record */ Job.prototype._sendProgress = cow(function*() { // If job complete, exit if (this.complete) return; // If timer scheduled, cancel it this.timer.clear(); // Send progress message yield this.locker.run(function*() { // Prepare progress object const {total, done} = this.progress, progress = total ? {total, done, percent: Math.floor(done / total * 100)} : {}; // Record status yield this.recordStatus('Processing', {progress}); }, this); // If job complete, exit if (this.complete) return; // Schedule to report progress again after delay this.timer.schedule(this.sendProgress, this, this.server.options.messageInterval); }); /** * Record job status. * @param {string} status - Status code i.e. 'Done', 'Failed', 'Processing' * @param {Error} data - Message payload * @returns {Promise} - Resolves when job status recorded, rejects if failed to record */ Job.prototype.recordStatus = cow(function*(status, data) { this.log(status, data); yield this.server.recordJobStatus(this.id, status, data); }); /** * Cancel job. * Called when server is stopped. * Cancellation is effected by calling `.cancel()` on the job's promise. * @returns {Promise} - Resolved when cancelled */ Job.prototype.cancel = cow(function*() { if (this.cancelled) return; this.cancelled = true; this.log('Cancelling job'); // Cancel the job try { yield this.worker.cancel(this); if (this.promise) this.promise.cancel(new Errors.Job.Cancelled()); this.log('Cancelled job'); } catch (err) { this.log('Cancellation failed', {err}); } });