UNPKG

peanar

Version:

A job queue for Node.js based on RabbitMQ

240 lines (239 loc) 8.28 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const debug_1 = __importDefault(require("debug")); const debug = (0, debug_1.default)('peanar:worker'); const util_1 = __importDefault(require("util")); require("colors"); const stream_1 = require("stream"); const exceptions_1 = require("./exceptions"); var EWorkerState; (function (EWorkerState) { EWorkerState["IDLE"] = "IDLE"; EWorkerState["WORKING"] = "WORKING"; EWorkerState["CLOSING"] = "CLOSING"; EWorkerState["CLOSED"] = "CLOSED"; })(EWorkerState || (EWorkerState = {})); let counter = 0; const SHUTDOWN_TIMEOUT = 10000; const _to_ack = new Set(); class PeanarWorker extends stream_1.Transform { app; _channel; queue; n; state = EWorkerState.IDLE; activeJob; destroy_cb; _destroy_timeout; _shutdown_timeout = SHUTDOWN_TIMEOUT; _channel_lost = false; logger; constructor(app, channel, queue, options) { super({ objectMode: true }); this.app = app; this.queue = queue; this.n = counter++; this._channel = channel; this._channel.once('close', this.onChannelClosed); this.logger = options?.logger ?? ((msg) => { debug(`${`PeanarWorker#${this.n}:`.bold} ${msg}`); }); } get channel() { return this._channel; } set channel(ch) { if (this._channel) { this._channel.off('close', this.onChannelClosed); } this._channel_lost = false; this._channel = ch; this._channel.once('close', this.onChannelClosed); this.emit('channelChanged', ch); this.log('channel changed!'); } onChannelClosed = (err) => { this._channel_lost = true; // if (this.activeJob) this.activeJob.cancel(err || 'Channel closed'); }; async shutdown(timeout) { if (timeout) this._shutdown_timeout = timeout; const destroy = util_1.default.promisify(this.destroy); await destroy.call(this, undefined); } _destroy(error, callback) { if (this.state === EWorkerState.IDLE) { this.state = EWorkerState.CLOSED; if (error) { this.log(`Worker destroyed because of an error: ${error?.name}: ${error?.message}`); } this.log('Worker state: Closed'); return callback(null); } else { this.state = EWorkerState.CLOSING; this.destroy_cb = (err) => { if (this.activeJob) { this.log('cancelling active job'); this.activeJob.cancel(); } this.state = EWorkerState.CLOSED; this.log('Worker state: Closed'); setImmediate(() => callback(err)); }; this._destroy_timeout = setTimeout(this.destroy_cb, this._shutdown_timeout, error); } } log(msg) { return this.logger(`${`PeanarWorker#${this.n}:`.bold} ${msg}`); } getJobDefinition(name) { try { return this.app.registry.getJobDefinition(name); } catch (ex) { if (ex instanceof exceptions_1.PeanarInternalError) { } else throw ex; } } _parseBody(body) { try { return JSON.parse(body.toString('utf-8')); } catch (ex) { return null; } } _getJob(delivery) { if (!delivery.body) { console.warn('PeanarWorker#_getJob: Delivery without body!'); return; } const body = this._parseBody(delivery.body); if (!body || !body.name || body.name.length < 1) { console.warn('PeanarWorker#_getJob: Invalid message body!'); return; } debug(`_getJob(${body.name})`); const def = this.getJobDefinition(body.name); if (!def) { console.warn(`PeanarWorker#_getJob: No handler registered for ${this.queue}.${body.name}!`); return; } const headers = delivery.properties.headers; const deathInfo = headers && Array.isArray(headers['x-death']) ? headers['x-death'].find(d => d.queue === def.queue) : undefined; const req = { ...def, deliveryTag: delivery.envelope.deliveryTag, replyTo: delivery.properties.replyTo, correlationId: delivery.properties.correlationId, queue: this.queue, args: body.args, id: body.id, attempt: deathInfo ? Number(deathInfo.count) + 1 : 1 }; if (delivery.properties.replyTo) { req.replyTo = delivery.properties.replyTo; req.correlationId = delivery.properties.correlationId; } const jobClass = def.jobClass ?? this.app.jobClass; return new jobClass(req, def, this.app, this._channel); } async run(job) { this.log('run()'); this.activeJob = job; try { const result = await job.perform(); _to_ack.add(job.id); this.log(`_to_ack.add(${job.id});`); this.push({ status: 'SUCCESS', job, result }); this.log(`Job ${job.name}:${job.id} SUCCESS!`); job.ack(); _to_ack.delete(job.id); this.log(`_to_ack.delete(${job.id});`); this.log(`Job ${job.name}:${job.id} was acked.`); } catch (ex) { if (ex instanceof exceptions_1.PeanarJobCancelledError) { this.log(`job ${job.id} was cancelled.`); return; } if (ex.name === 'IllegalOperationError') { this.log(`Channel closed on ack for job ${job.id}. It will be acked next time it's delivered.`); return; } this.push({ status: 'FAILURE', job, error: ex }); this.log(`Job ${job.name}:${job.id} FAILURE!`); await job.reject(ex); this.log(`Job ${job.name}:${job.id} was rejected.`); } finally { this.activeJob = undefined; } } _transform(delivery, _encoding, cb) { if (this.app.state === "CLOSING" || this.app.state === "CLOSED") { this.log(`Received job ${Number(delivery.envelope.deliveryTag)} while shutting down. Holding on to it...`); return cb(); } const done = (ex) => { this.log('Worker state: Idle'); this.state = EWorkerState.IDLE; cb(ex); if (this.destroy_cb) { this.destroy_cb(null); } if (this._destroy_timeout) { clearTimeout(this._destroy_timeout); this._destroy_timeout = undefined; } }; const startProcessing = () => { this.state = EWorkerState.WORKING; let job = undefined; try { job = this._getJob(delivery); } catch (ex) { // @ts-ignore this.channel.reject({ fields: { deliveryTag: Number(delivery.envelope.deliveryTag) } }, false); return done(ex); } if (!job) { // @ts-ignore this.channel.reject({ fields: { deliveryTag: Number(delivery.envelope.deliveryTag) } }, false); return done(); } if (_to_ack.has(job.id)) { this.log(`Job ${job.name}:${job.id} will be acked from pending list.`); job.ack(); _to_ack.delete(job.id); return done(); } this.run(job).then(_ => done(), done); }; // @ts-ignore if (this._channel_lost) { this.once("channelChanged", startProcessing); } else { startProcessing(); } } } exports.default = PeanarWorker;