inceptum
Version:
hipages take on the foundational library for enterprise-grade apps written in NodeJS
239 lines • 9.98 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RabbitmqClient = exports.ClientPropertyTag = exports.ShutdownMessage = void 0;
const amqplib_1 = require("amqplib");
const ReadyGate_1 = require("../util/ReadyGate");
const PromiseUtil_1 = require("../util/PromiseUtil");
const RabbitmqConfig_1 = require("./RabbitmqConfig");
/*
Please read "RabbitMQ Connection Lifecycle.md" for an overview of how
connection and reconnection is managed for RabbitMQ
*/
var ShutdownMessage;
(function (ShutdownMessage) {
ShutdownMessage["ECONNRESET"] = "read ECONNRESET";
ShutdownMessage["HEARTBEATTIMEOUT"] = "Heartbeat timeout";
})(ShutdownMessage = exports.ShutdownMessage || (exports.ShutdownMessage = {}));
var ClientPropertyTag;
(function (ClientPropertyTag) {
ClientPropertyTag["Connection"] = "connection";
ClientPropertyTag["Channel"] = "channel";
})(ClientPropertyTag = exports.ClientPropertyTag || (exports.ClientPropertyTag = {}));
class RabbitmqClient {
constructor(clientConfig, name) {
/**
* Whether the connection should be closed. This is not a statud indicator that show whether the connection is closed.
* It is a flag that indicates whether the client has been purposely closed by code, as opposed to being closed because of an error.
*/
this.closed = false;
this.reconnecting = false;
this.readyGate = new ReadyGate_1.ReadyGate();
this.connectFunction = amqplib_1.connect;
this.clientConfig = Object.assign({ protocol: 'amqp', maxConnectionAttempts: RabbitmqConfig_1.DEFAULT_MAX_CONNECTION_ATTEMPTS }, clientConfig);
this.name = name;
this.readyGate.channelNotReady();
}
async init(addHandler = true) {
await this.connect(addHandler);
}
/**
* Connect to RabbitMQ broker
*/
async connect(addHandler = true) {
await this.createConnection(addHandler);
await this.createChannel(addHandler);
this.stopReconnecting();
this.readyGate.channelReady();
}
async createConnection(addHandler) {
const newConnection = await this.connectFunction(this.clientConfig);
this.connection = newConnection;
if (addHandler) {
this.addConnectionHandlers();
}
this.logger.info(this.debugMsg('Connection established'));
}
addConnectionHandlers() {
this.connection.on('close', (err) => { this.handleConnectionClose(err); });
this.connection.on('error', (err) => { this.handleError(ClientPropertyTag.Connection, err); });
}
async createChannel(addHandler) {
const newChannel = await this.connection.createChannel();
this.channel = newChannel;
if (addHandler) {
this.addChannelHandlers();
}
this.logger.info(this.debugMsg('Channel opened'));
}
addChannelHandlers() {
this.channel.on('close', (err) => { this.handleChannelClose(err); });
this.channel.on('error', (err) => { this.handleError(ClientPropertyTag.Channel, err); });
}
/**
* 1. Reconnect when errors occur.
* 2. Errors do not exist if connection.close() is called
* or a server initiated graceful close.
* 3. Graceful closed connection will be recovered.
* 4. Because close event with an error will be emitted after connection error event,
* only handle connection close event.
* @param err
*/
async handleConnectionClose(err) {
if (err) {
// A connection is closed with an error.
// eg. "CONNECTION_FORCED - broker forced connection closure with reason 'shutdown'"
this.logger.error(err, this.debugMsg(`Handling a connection close event with an error. Will reconnect.`));
await this.closeAllAndScheduleReconnection();
}
else {
this.logger.warn(this.debugMsg(`A graceful CONNECTION close event is emitted.`));
}
}
/**
* Schedule reconnection in error handler because connection errors do not always trigger a 'close' event.
* A channel error event is emitted if a server closes the channel for any reason.
* A channel will not emit 'error' if its connection closes with an error.
*
* Channel Errors are triggered by one of the following:
* 1. failed to consume.
*
* Channel error event will trigger connection error event which will
* trigger connection close event. Do not handle connection error event because a close event with error will be emiited.
* Then the close event will be handled.
*/
async handleError(emitter, err) {
if (Object.values(ShutdownMessage).includes(err.message)) {
this.logger.error(err, this.debugMsg(`Handling ${emitter} error. Shutting down the app to restart.`));
this.shutdownFunction();
return;
}
this.logger.error(err, this.debugMsg(`Handling ${emitter} error. Will reconnect.`));
await this.closeAllAndScheduleReconnection();
}
/**
* Handle when a channel is closed gracefully.
*/
handleChannelClose(err) {
if (err) {
this.logger.warn(err, this.debugMsg(`Handling a channel close event with an error.`));
}
else {
this.logger.warn(this.debugMsg(`A graceful CHANNEL close event is emitted.`));
}
}
async closeAllAndScheduleReconnection() {
if (!this.reconnecting) {
this.logger.info(this.debugMsg('before channel not ready'));
this.startReconnecting();
this.readyGate.channelNotReady();
if (this.channel) {
await this.closeChannel();
}
this.logger.info(this.debugMsg('passed channel close'));
if (this.connection) {
this.logger.info(this.debugMsg('will close connection'));
await this.closeConnection();
}
this.logger.info(this.debugMsg('passed connection close'));
const result = await this.attemptReconnection();
/**
* Add handlers after connect, channel and subscribe successfully.
*/
if (result) {
this.addConnectionHandlers();
this.addChannelHandlers();
}
return result;
}
else {
this.logger.warn(this.debugMsg('already reconnecting'));
return false;
}
}
async backoffWait(tryNum) {
// 1 second, 5 seconds, 25 seconds, 30 seconds, 30 seconds, ....
const waitBase = Math.min(Math.pow(5, Math.max(0, tryNum - 1)), 30) * 1000;
const waitMillis = waitBase + (Math.round(Math.random() * 800));
this.logger.warn(this.debugMsg(`Waiting for attempt #${tryNum} - ${waitMillis} ms`));
await PromiseUtil_1.PromiseUtil.sleepPromise(waitMillis);
}
async attemptReconnection() {
this.logger.warn(this.debugMsg(`reconnecting... max attempts ${this.clientConfig.maxConnectionAttempts}`));
let attempts = 0;
while (attempts < this.clientConfig.maxConnectionAttempts) {
attempts++;
await this.backoffWait(attempts);
try {
this.logger.info(this.debugMsg(`initialising attempt #${attempts}`));
await this.init(false);
this.logger.warn(this.debugMsg(`reconnection attempt #${attempts} is successful.`));
return true;
}
catch (e) {
await this.closeConnection();
this.logger.warn(e, this.debugMsg(`Failed reconnection attempt #${attempts}. Retrying...`));
}
}
this.logger.error(this.debugMsg(`Couldn't reconnect after ${this.clientConfig.maxConnectionAttempts} attempts`));
this.stopReconnecting();
// tslint:disable-next-line
if (this.clientConfig.exitOnIrrecoverableReconnect !== false) {
this.logger.error(this.debugMsg('Cowardly refusing to continue. Calling shutdown function'));
this.shutdownFunction();
}
return false;
}
async close() {
this.closed = true;
await this.closeChannel();
await this.closeConnection();
}
async closeChannel() {
if (this.channel) {
try {
this.channel.removeAllListeners();
this.logger.info(this.debugMsg('Will close channel.'));
await this.channel.close();
}
catch (err) {
// Do nothing... we tried to play nice
// An error is more likely caused by closing a closed channel.
this.logger.info(err, this.debugMsg('Error when closing channel.'));
}
this.channel = undefined;
this.logger.info(this.debugMsg('Channel closed.'));
}
}
async closeConnection() {
if (this.connection) {
try {
this.connection.removeAllListeners();
this.logger.info(this.debugMsg('will call connection close'));
await this.connection.close();
}
catch (err) {
// Do nothing... we tried to play nice
// An error is more likely caused by closing a closed connection.
this.logger.info(err, this.debugMsg('Error when closing connection.'));
}
this.connection = undefined;
this.logger.info(this.debugMsg('Connection closed.'));
}
}
debugMsg(str) {
return `${this.name}: ${str}`;
}
defaultHeader() {
return {
retriesCount: 0,
};
}
stopReconnecting() {
this.reconnecting = false;
}
startReconnecting() {
this.reconnecting = true;
}
}
exports.RabbitmqClient = RabbitmqClient;
//# sourceMappingURL=RabbitmqClient.js.map