peanar
Version:
A job queue for Node.js based on RabbitMQ
180 lines (179 loc) • 6.56 kB
JavaScript
"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:job');
const events_1 = require("events");
const exceptions_1 = require("./exceptions");
class PeanarJob extends events_1.EventEmitter {
id;
name;
args;
handler;
deliveryTag;
correlationId;
channel;
app;
def;
attempt;
max_retries;
cancelled = false;
constructor(req, def, app, channel) {
super();
this.def = def;
this.id = req.id;
this.name = req.name;
this.args = req.args;
this.correlationId = req.correlationId || req.id;
this.deliveryTag = req.deliveryTag;
this.attempt = req.attempt;
this.max_retries = def.max_retries || 0;
this.handler = def.handler;
this.channel = channel;
this.app = app;
}
cancel(reason) {
this.cancelled = true;
this.emit('cancel', new exceptions_1.PeanarJobCancelledError(reason));
}
ack() {
if (!this.channel)
throw new exceptions_1.PeanarAdapterError("Worker: AMQP connection lost!");
if (!this.deliveryTag)
throw new exceptions_1.PeanarJobError("Worker: No deliveryTag set!");
if (!this.cancelled) {
// @ts-ignore
this.channel.ack({ fields: { deliveryTag: Number(this.deliveryTag) } }, false);
debug(`PeanarJob#${this.id}: Acknowledged!`);
}
}
canRetry() {
return (typeof this.def.retry_exchange === 'string' &&
(this.max_retries < 0 || this.attempt <= this.max_retries));
}
async reject(ex) {
if (!this.channel)
throw new exceptions_1.PeanarAdapterError("Worker: AMQP connection lost!");
if (!this.deliveryTag)
throw new exceptions_1.PeanarJobError("Worker: No deliveryTag set!");
if (ex?.retry !== false && this.canRetry()) {
debug(`PeanarJob#${this.id}: Trying again...`);
await this._declareRetryQueues();
debug(`PeanarJob#${this.id}: Rejecting to retry queue...`);
// @ts-ignore
this.channel.reject({ fields: { deliveryTag: Number(this.deliveryTag) } }, false);
}
else {
if (!this.def.error_exchange || this.def.error_exchange.length < 1) {
debug(`PeanarJob#${this.id}: No retries left. Discarding...`);
// @ts-ignore
return this.channel.reject({ fields: { deliveryTag: Number(this.deliveryTag) } }, false);
}
// No attempts left. Publish to error exchange for manual investigation.
debug(`PeanarJob#${this.id}: No retries. Writing to error exchange!`);
this.channel.publish(this.def.error_exchange, this.def.routingKey, Buffer.from(JSON.stringify({
id: this.id,
name: this.name,
error: ex?.message,
args: this.args
})), {
contentType: 'application/json'
});
return this.ack();
}
}
enqueue() {
return this.app.enqueueJobRequest(this.def, {
id: this.id,
args: this.args,
name: this.name,
attempt: 1,
correlationId: this.correlationId
});
}
get retry_name() {
return `${this.def.queue}.retry`;
}
get requeue_name() {
return `${this.def.queue}.retry-requeue`;
}
async _declareRetryQueues() {
if (!this.def.retry_exchange)
throw new exceptions_1.PeanarInternalError('Attempting retry without a retry_exchange specified.');
const retry_name = this.retry_name;
const requeue_name = this.requeue_name;
try {
debug(`declare retry queue ${retry_name}`);
await this.app.broker.queues([{
name: retry_name,
arguments: {
expires: 2 * (this.def.retry_delay || 60000),
messageTtl: this.def.retry_delay || 60000,
deadLetterExchange: requeue_name
},
auto_delete: false,
durable: true,
exclusive: false
}]);
debug(`bind retry exchange ${this.def.retry_exchange} to retry queue ${retry_name}`);
await this.app.broker.bindings([{
exchange: this.def.retry_exchange,
queue: retry_name,
routing_key: '#'
}]);
debug('_declareRetryQueues(): done');
}
catch (ex) {
console.error(ex);
throw ex;
}
}
retry() {
if (!this.max_retries || this.attempt >= this.max_retries) {
console.warn('Illegal PeanarJob.retry()!');
}
return this.app.enqueueJobRequest(this.def, {
id: this.id,
args: this.args,
name: this.name,
attempt: this.attempt + 1,
correlationId: this.correlationId
});
}
pauseQueue() {
return this.app.pauseQueue(this.def.queue);
}
resumeQueue() {
return this.app.resumeQueue(this.def.queue);
}
_perform() {
return this.handler.apply(this, this.args);
}
perform() {
let already_finished = false;
return new Promise((resolve, reject) => {
const callResolve = (result) => {
if (!already_finished) {
already_finished = true;
debug(`PeanarJob#${this.id}: (callResolve) not finished. all good. resolving...`);
resolve(result);
}
};
const callReject = (ex) => {
if (!already_finished) {
already_finished = true;
debug(`PeanarJob#${this.id}: (callReject) not finished. rejecting.`);
reject(ex);
}
else {
debug(`PeanarJob#${this.id}: (callReject) already finished somehow! when did it finish?!`);
}
};
this._perform().then(callResolve, callReject);
this.once('cancel', callReject);
});
}
}
exports.default = PeanarJob;