coffee-core
Version:
Coffee IT API core library
216 lines (184 loc) • 7.26 kB
text/typescript
import * as amqp from 'amqplib';
import { Channel, Connection, Replies } from 'amqplib';
import { EventSubscriber } from './event-subscriber';
import { LoggerService } from '@nestjs/common';
import { ConsumeHandler } from './consume-handler';
import { ValidationMiddleware } from './validation/validation-middleware';
import { ClassConstructor } from 'class-transformer';
import { BrokerConfigInterface } from './broker-config.interface';
import { CustomLogger } from '../../logger/custom-logger';
import AssertQueue = Replies.AssertQueue;
// Inspired by: https://docs.nestjs.com/fundamentals/custom-providers and https://www.rabbitmq.com/tutorials/tutorial-three-javascript.html
export class RabbitMQSubscriber implements EventSubscriber {
private static instance: RabbitMQSubscriber;
private channel: Channel;
private server: Connection;
private createdQueue: AssertQueue;
private readonly consumeHandlers: ConsumeHandler[] = [];
private readonly usingRandomQueueName: boolean;
private readonly queueName: string;
private readonly logger: LoggerService = new CustomLogger(RabbitMQSubscriber.name);
private readonly eventRetryTimeoutSeconds = 2 * 1000;
private readonly consumeTimeoutSeconds = 5 * 1000;
private readonly autoAcknowledge: boolean;
private readonly isExclusive: boolean;
private readonly autoDeleteQueue: boolean;
private isConsuming = false;
private constructor(
private readonly config: BrokerConfigInterface,
) {
// private
this.queueName = config.QUEUE_NAME;
this.usingRandomQueueName = this.queueName === '';
this.autoDeleteQueue = this.queueName === '';
this.autoAcknowledge = config.AUTO_ACKNOWLEDGE === undefined ? false : config.AUTO_ACKNOWLEDGE;
this.isExclusive = config.EXCLUSIVE_QUEUES === undefined ? true : config.EXCLUSIVE_QUEUES;
}
public async subscribe(
bindingKey: string,
onConsume: (message: Record<string, unknown>) => Promise<void>,
messageClass: ClassConstructor<any>,
enableLogging = true,
): Promise<void> {
let channel;
try {
channel = await this.getChannel();
} catch (e) {
this.logger.error(`Got an error when subscribing for key ${bindingKey}`);
setTimeout(() => {
this.logger.debug(`Retrying subscribe after channel error for key ${bindingKey}`);
this.subscribe(bindingKey, onConsume, messageClass);
}, this.eventRetryTimeoutSeconds);
return;
}
channel.on('close', () => {
this.logger.debug(`Channel closed for key ${bindingKey}`);
this.subscribe(bindingKey, onConsume, messageClass);
});
try {
await this.startEventSubscribe(channel, bindingKey, onConsume, messageClass, enableLogging);
} catch (e) {
this.logger.error(`Got an error when consuming for key ${bindingKey}`);
setTimeout(() => {
this.logger.debug(`Retrying subscribe after consume error for key ${bindingKey}`);
this.subscribe(bindingKey, onConsume, messageClass, enableLogging);
}, this.eventRetryTimeoutSeconds);
}
}
private async startEventSubscribe(
channel: Channel,
bindingKey: string,
onConsume: (message: Record<string, unknown>) => Promise<void>,
messageClass: ClassConstructor<any>,
enableLogging: boolean,
) {
this.consumeHandlers.push(new ConsumeHandler(bindingKey, onConsume, messageClass, enableLogging));
const exchangeName = this.config.EXCHANGE_NAME;
await channel.assertExchange(
exchangeName, this.config.EXCHANGE_TYPE, { durable: this.config.DURABLE_EXCHANGE },
);
const queueOptions = { durable: this.config.DURABLE_QUEUES, autoDelete: this.autoDeleteQueue };
if (this.usingRandomQueueName && !this.createdQueue) {
this.createdQueue = await channel.assertQueue(this.queueName, queueOptions);
}
if (!this.usingRandomQueueName) {
this.createdQueue = await channel.assertQueue(this.queueName, queueOptions);
}
await channel.bindQueue(this.createdQueue.queue, exchangeName, bindingKey);
this.logger.debug(`Subscriber is now consuming on ${bindingKey}`);
}
public async startReceivingMessages(): Promise<void> {
if (this.isConsuming) {
return;
}
let channel;
try {
channel = await this.getChannel();
} catch (e) {
this.logger.error('Got an error while getting the channel to receive messages.');
setTimeout(() => {
this.startReceivingMessages();
}, this.consumeTimeoutSeconds);
return;
}
this.isConsuming = true;
await channel.consume(this.queueName, async (msg) => {
const { content } = msg;
if (!content) {
return;
}
try {
const { routingKey } = msg.fields;
const consumeHandler = this.findConsumeHandler(routingKey);
if (!consumeHandler) {
this.logger.debug(`Could not find consume handler for routingKey: ${routingKey}. Message lost.`);
const requeue = false;
channel.nack(msg, false, requeue);
return;
}
const messageObject = JSON.parse(content.toString());
await consumeHandler.consume(messageObject);
channel.ack(msg);
} catch (err) {
this.logger.error(`Got an error when consuming event from queue ${this.queueName}: ${err}`);
}
}, { noAck: this.autoAcknowledge, exclusive: this.isExclusive });
}
private findConsumeHandler(routingKey: string): ConsumeHandler {
return this.consumeHandlers.find(handler => handler.bindingKey === routingKey);
}
public async close(): Promise<void> {
if (this.channel) {
await this.channel.close();
}
}
public setValidationMiddleware(middleware: ValidationMiddleware): void {
this.consumeHandlers.forEach(handler => handler.setValidationMiddleware(middleware));
}
private async getChannel(): Promise<Channel> {
if (this.channel) {
return this.channel;
}
const server = await this.getServer();
this.channel = await server.createChannel();
this.channel.on('close', () => {
this.logger.debug('Channel closed');
this.channel = undefined;
if (this.isConsuming) {
this.isConsuming = false;
this.startReceivingMessages();
}
this.isConsuming = false;
});
this.channel.on('err', () => {
this.logger.debug('Channel got an error');
this.channel = undefined;
this.isConsuming = false;
});
return this.channel;
}
private async getServer(): Promise<Connection> {
if (this.server) {
return this.server;
}
this.server = await this.connect();
this.server.on('close', () => {
this.logger.debug('RabbitMQ server closed');
this.server = undefined;
});
this.server.on('err', () => {
this.logger.debug('RabbitMQ server got an error');
this.server = undefined;
});
return this.server;
}
private async connect(): Promise<Connection> {
return amqp.connect(this.config.AMQP_CONNECTION_URI);
}
public static getInstance(config: BrokerConfigInterface): RabbitMQSubscriber {
if (!RabbitMQSubscriber.instance) {
RabbitMQSubscriber.instance = new RabbitMQSubscriber(config);
}
return RabbitMQSubscriber.instance;
}
}