peanar
Version:
A job queue for Node.js based on RabbitMQ
212 lines (211 loc) • 7.61 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const events_1 = require("events");
const uuid = __importStar(require("uuid"));
const debug_1 = __importDefault(require("debug"));
const debug = (0, debug_1.default)('peanar:transact');
const app_1 = require("./app");
const exceptions_1 = require("./exceptions");
function timeout(ms) {
return new Promise((res, rej) => {
setTimeout(rej, ms);
});
}
class PeanarTransactor extends events_1.EventEmitter {
def;
app;
queue_name;
tid;
_counter = 0;
_acquireChannelPromise;
committed = false;
rolledback = false;
began = false;
constructor(def, app) {
super();
this.def = def;
this.app = app;
this.queue_name = `${this.def.queue}:${this.def.name}:${uuid.v4()}`;
this.tid = this.queue_name;
}
get queueName() { return this.queue_name; }
acquireChannel() {
if (this.app.state !== app_1.EAppState.RUNNING) {
throw new exceptions_1.PeanarInternalError('Peanar is not running!');
}
const _doAcquire = async () => {
const conn = await this.app.broker.connect();
const ch = await conn.createChannel();
await ch.prefetch(0, false);
ch.once('close', () => {
this._acquireChannelPromise = undefined;
if (this.app.state === app_1.EAppState.RUNNING) {
this._acquireChannelPromise = _doAcquire();
}
});
return ch;
};
if (this._acquireChannelPromise)
return this._acquireChannelPromise;
return (this._acquireChannelPromise = _doAcquire());
}
async begin(tid) {
if (tid) {
this.tid = tid;
}
await this.app.broker.queues([{
name: this.queue_name,
durable: true,
exclusive: false,
auto_delete: false,
arguments: {
deadLetterExchange: this.def.exchange,
deadLetterRoutingKey: this.def.routingKey,
}
}]);
}
async call(...args) {
debug(`PeanarTransactor: call(${this.def.queue}:${this.def.name}})`);
const req = {
id: uuid.v4(),
name: this.def.name,
args,
attempt: 1
};
if (this.app.state !== app_1.EAppState.RUNNING) {
throw new exceptions_1.PeanarInternalError('PeanarTransactor::call() called while app is not in running state.');
}
const properties = {};
if (typeof this.def.expires === 'number') {
properties.expiration = this.def.expires.toString();
}
await this.app.broker.publish({
routing_key: this.queue_name,
exchange: '',
properties,
body: {
id: req.id,
name: req.name,
args: req.args
}
});
this._counter += 1;
return req.id;
}
async waitUntil(ms = 0) {
if (this.committed || this.rolledback)
return;
const arr = [
(0, events_1.once)(this, 'conclude'),
];
if (ms > 0) {
arr.push(timeout(ms));
}
await Promise.race(arr);
}
async commit() {
if (this.committed) {
throw new exceptions_1.PeanarInternalError('Transaction already commited.');
}
if (this.rolledback) {
throw new exceptions_1.PeanarInternalError('Transaction has been rolled back.');
}
let resolveFn;
let consumeCounter = 0;
const consumerPromise = new Promise(res => {
resolveFn = () => res(undefined);
});
const consumerCallback = () => {
consumeCounter += 1;
if (consumeCounter === this._counter) {
if (typeof resolveFn === 'function') {
resolveFn();
}
else {
setImmediate(resolveFn);
}
}
};
const ch = await this.acquireChannel();
try {
const consumer = await ch.consume(this.queue_name, consumerCallback);
await consumerPromise;
await ch.cancel(consumer.consumerTag);
ch.nackAll(false);
await ch.deleteQueue(this.queue_name, { ifEmpty: true });
debug(`${this.queue_name}: committed`);
this.committed = true;
this.emit('commit');
this.emit('conclude', 'commit');
}
catch (ex) {
if (ex.message === 'Channel closed' || ex.name === 'IllegalOperationError') {
this.app.log(`Channel closed while committing the transaction ${this.queue_name}. Retrying...`);
return this.commit();
}
throw ex;
}
}
async rollback() {
if (this.committed) {
throw new exceptions_1.PeanarInternalError('Transaction already commited.');
}
if (this.rolledback) {
throw new exceptions_1.PeanarInternalError('Transaction has been rolled back.');
}
if (this.app.state !== app_1.EAppState.RUNNING || !this.app.broker.pool) {
throw new exceptions_1.PeanarInternalError('Peanar is not running!');
}
try {
await this.app.broker.pool.acquireAndRun(ch => ch.deleteQueue(this.queue_name, { ifEmpty: true }));
debug(`${this.queue_name}: rolled back`);
this.rolledback = true;
this.emit('rollback');
this.emit('conclude', 'rollback');
}
catch (ex) {
if (ex.message === 'Channel closed' || ex.name === 'IllegalOperationError') {
this.app.log(`Channel closed while rolling back the transaction ${this.queue_name}. Retrying...`);
return this.rollback();
}
throw ex;
}
}
}
exports.default = PeanarTransactor;