coffee-core
Version:
Coffee IT API core library
249 lines (217 loc) • 7.51 kB
text/typescript
import { Logger } from '@nestjs/common';
import { Channel, ChannelModel, connect, Replies } from 'amqplib';
import { ClassConstructor } from 'class-transformer';
import { BrokerConfigInterface } from './broker-config.interface';
import { ConsumeHandler } from './consume-handler';
import { EventSubscriber } from './event-subscriber';
import { ValidationMiddleware } from './validation/validation-middleware';
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: ChannelModel;
private createdQueue: AssertQueue;
private readonly consumeHandlers: ConsumeHandler[] = [];
private readonly usingRandomQueueName: boolean;
private readonly queueName: string;
private readonly logger = new Logger(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: Channel;
try {
channel = await this.getChannel();
} catch (err) {
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 (err) {
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: Channel;
try {
channel = await this.getChannel();
} catch (err) {
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<ChannelModel> {
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<ChannelModel> {
return connect(this.config.AMQP_CONNECTION_URI);
}
public static getInstance(config: BrokerConfigInterface): RabbitMQSubscriber {
if (!RabbitMQSubscriber.instance) {
RabbitMQSubscriber.instance = new RabbitMQSubscriber(config);
}
return RabbitMQSubscriber.instance;
}
}