peanar
Version:
A job queue for Node.js based on RabbitMQ
240 lines (239 loc) • 8.28 kB
JavaScript
;
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;