redis-smq
Version:
A simple high-performance Redis message queue for Node.js.
317 lines • 15.4 kB
JavaScript
import { async, logger, PanicError, Runnable, } from 'redis-smq-common';
import { RedisClient } from '../../common/redis-client/redis-client.js';
import { ELuaScriptName } from '../../common/redis-client/scripts/scripts.js';
import { redisKeys } from '../../common/redis-keys/redis-keys.js';
import { Configuration } from '../../config/index.js';
import { EventBus } from '../../common/index.js';
import { _getExchangeQueues } from '../exchange/_/_get-exchange-queues.js';
import { EExchangeType } from '../exchange/index.js';
import { EMessageProperty, EMessagePropertyStatus, } from '../message/index.js';
import { MessageEnvelope } from '../message/message-envelope.js';
import { EQueueProperty, EQueueType } from '../queue/index.js';
import { _scheduleMessage } from './_/_schedule-message.js';
import { ProducerError, ProducerExchangeNoMatchedQueueError, ProducerInstanceNotRunningError, ProducerMessageExchangeRequiredError, ProducerMessagePriorityRequiredError, ProducerPriorityQueuingNotEnabledError, ProducerQueueMissingConsumerGroupsError, ProducerQueueNotFoundError, ProducerUnknownQueueTypeError, } from './errors/index.js';
import { eventBusPublisher } from './event-bus-publisher.js';
import { QueueConsumerGroupsCache } from './queue-consumer-groups-cache.js';
export class Producer extends Runnable {
logger;
redisClient;
eventBus;
queueConsumerGroupsHandler = null;
constructor() {
super();
this.redisClient = new RedisClient();
this.redisClient.on('error', (err) => this.handleError(err));
this.eventBus = new EventBus();
this.eventBus.on('error', (err) => this.handleError(err));
this.logger = logger.getLogger(Configuration.getSetConfig().logger, this.constructor.name.toLowerCase());
this.logger.info(`Producer instance created with ID: ${this.getId()}`);
if (Configuration.getSetConfig().eventBus.enabled) {
this.logger.debug('Event bus is enabled, initializing event bus publisher');
eventBusPublisher(this, this.eventBus, this.logger);
}
else {
this.logger.debug('Event bus is disabled, skipping event bus publisher initialization');
}
}
getLogger() {
return this.logger;
}
initQueueConsumerGroupsHandler = (cb) => {
this.logger.debug('Initializing queue consumer groups handler');
this.queueConsumerGroupsHandler = new QueueConsumerGroupsCache(this, this.redisClient, this.eventBus);
this.queueConsumerGroupsHandler.run((err) => {
if (err) {
this.logger.error('Failed to initialize queue consumer groups handler', err);
}
else {
this.logger.debug('Queue consumer groups handler initialized successfully');
}
cb(err);
});
};
shutDownQueueConsumerGroupsHandler = (cb) => {
if (this.queueConsumerGroupsHandler) {
this.logger.debug('Shutting down queue consumer groups handler');
this.queueConsumerGroupsHandler.shutdown(() => {
this.logger.debug('Queue consumer groups handler shut down successfully');
this.queueConsumerGroupsHandler = null;
cb();
});
}
else {
this.logger.debug('No queue consumer groups handler to shut down');
cb();
}
};
goingUp() {
this.logger.info(`Producer ${this.getId()} is starting up`);
return super.goingUp().concat([
this.redisClient.init,
this.eventBus.init,
(cb) => {
this.logger.debug(`Emitting producer.goingUp event for producer ${this.id}`);
this.emit('producer.goingUp', this.id);
cb();
},
this.initQueueConsumerGroupsHandler,
]);
}
up(cb) {
super.up(() => {
this.logger.info(`Producer ${this.getId()} is now up and running`);
this.emit('producer.up', this.id);
cb(null, true);
});
}
goingDown() {
this.logger.info(`Producer ${this.getId()} is shutting down`);
this.emit('producer.goingDown', this.id);
return [
this.shutDownQueueConsumerGroupsHandler,
this.redisClient.shutdown,
].concat(super.goingDown());
}
down(cb) {
super.down(() => {
this.logger.info(`Producer ${this.getId()} is now down`);
this.emit('producer.down', this.id);
this.logger.debug('Shutting down event bus with 1 second delay');
setTimeout(() => {
this.eventBus.shutdown(() => {
this.logger.debug('Event bus shut down successfully');
cb(null, true);
});
}, 1000);
});
}
getQueueConsumerGroupsHandler() {
if (!this.queueConsumerGroupsHandler) {
const error = new PanicError(`Expected an instance of QueueConsumerGroupsHandler`);
this.logger.error('Queue consumer groups handler not initialized', error);
throw error;
}
return this.queueConsumerGroupsHandler;
}
enqueue(redisClient, message, cb) {
const messageState = message.getMessageState();
messageState.setPublishedAt(Date.now());
const messageId = message.getId();
const destinationQueue = message.getDestinationQueue();
const queueName = `${destinationQueue.name}@${destinationQueue.ns}`;
const consumerGroupId = message.getConsumerGroupId();
this.logger.debug(`Enqueuing message ${messageId} to queue ${queueName}${consumerGroupId ? ` for consumer group ${consumerGroupId}` : ''}`);
const keys = redisKeys.getQueueKeys(destinationQueue, consumerGroupId);
const { keyMessage } = redisKeys.getMessageKeys(messageId);
const priority = message.producibleMessage.getPriority();
this.logger.debug(`Message ${messageId} details: priority=${priority ?? 'none'}, queue=${queueName}`);
const scriptArgs = [
EQueueProperty.QUEUE_TYPE,
EQueueProperty.MESSAGES_COUNT,
EQueueType.PRIORITY_QUEUE,
EQueueType.LIFO_QUEUE,
EQueueType.FIFO_QUEUE,
priority ?? '',
messageId,
EMessageProperty.STATUS,
EMessagePropertyStatus.PENDING,
EMessageProperty.STATE,
JSON.stringify(messageState),
EMessageProperty.MESSAGE,
JSON.stringify(message.toJSON()),
];
redisClient.runScript(ELuaScriptName.PUBLISH_MESSAGE, [
keys.keyQueueProperties,
keys.keyQueuePriorityPending,
keys.keyQueuePending,
keys.keyQueueMessages,
keyMessage,
], scriptArgs, (err, reply) => {
if (err) {
this.logger.error(`Failed to enqueue message ${messageId}`, err);
return cb(err);
}
switch (reply) {
case 'OK':
this.logger.debug(`Successfully enqueued message ${messageId} to queue ${queueName}`);
return cb();
case 'QUEUE_NOT_FOUND':
this.logger.error(`Queue ${queueName} not found for message ${messageId}`);
return cb(new ProducerQueueNotFoundError());
case 'MESSAGE_PRIORITY_REQUIRED':
this.logger.error(`Priority required for message ${messageId} but not provided`);
return cb(new ProducerMessagePriorityRequiredError());
case 'PRIORITY_QUEUING_NOT_ENABLED':
this.logger.error(`Priority queuing not enabled for queue ${queueName}`);
return cb(new ProducerPriorityQueuingNotEnabledError());
case 'UNKNOWN_QUEUE_TYPE':
this.logger.error(`Unknown queue type for queue ${queueName}`);
return cb(new ProducerUnknownQueueTypeError());
default:
this.logger.error(`Unknown error while enqueuing message ${messageId}: ${reply}`);
return cb(new ProducerError());
}
});
}
produceMessageItem(redisClient, message, queue, cb) {
const messageId = message
.setDestinationQueue(queue)
.getMessageState()
.getId();
const queueName = `${queue.name}@${queue.ns}`;
this.logger.debug(`Producing message item ${messageId} for queue ${queueName}${message.isSchedulable() ? ' (scheduled)' : ''}`);
const handleResult = (err) => {
if (err) {
this.logger.error(`Failed to produce message ${messageId} for queue ${queueName}`, err);
cb(err);
}
else {
const action = message.isSchedulable() ? 'scheduled' : 'published';
this.logger.info(`Message (ID ${messageId}) has been ${action} to queue ${queueName}`);
if (!message.isSchedulable()) {
this.logger.debug(`Emitting messagePublished event for message ${messageId}`);
this.emit('producer.messagePublished', messageId, { queueParams: queue, groupId: message.getConsumerGroupId() }, this.id);
}
cb(null, messageId);
}
};
if (message.isSchedulable()) {
this.logger.debug(`Scheduling message ${messageId} for future delivery`);
_scheduleMessage(redisClient, message, handleResult);
}
else {
this.logger.debug(`Enqueueing message ${messageId} for immediate delivery`);
this.enqueue(redisClient, message, handleResult);
}
}
produceMessage(redisClient, message, queue, cb) {
const queueName = `${queue.name}@${queue.ns}`;
this.logger.debug(`Producing message for queue ${queueName}`);
const { exists, consumerGroups } = this.getQueueConsumerGroupsHandler().getConsumerGroups(queue);
if (exists) {
this.logger.debug(`Queue ${queueName} exists with ${consumerGroups.length} consumer groups`);
if (!consumerGroups.length) {
this.logger.error(`Queue ${queueName} has no consumer groups`);
cb(new ProducerQueueMissingConsumerGroupsError());
return;
}
const ids = [];
this.logger.debug(`Producing message for ${consumerGroups.length} consumer groups in queue ${queueName}`);
async.eachOf(consumerGroups, (group, _, done) => {
this.logger.debug(`Producing message for consumer group ${group} in queue ${queueName}`);
const msg = new MessageEnvelope(message).setConsumerGroupId(group);
this.produceMessageItem(redisClient, msg, queue, (err, reply) => {
if (err) {
this.logger.error(`Failed to produce message for consumer group ${group}`, err);
done(err);
}
else {
this.logger.debug(`Successfully produced message ${reply} for consumer group ${group}`);
ids.push(String(reply));
done();
}
});
}, (err) => {
if (err) {
this.logger.error(`Failed to produce messages for some consumer groups in queue ${queueName}`, err);
cb(err);
}
else {
this.logger.info(`Successfully produced ${ids.length} messages for queue ${queueName}`);
cb(null, ids);
}
});
}
else {
this.logger.debug(`Queue ${queueName} has no consumer groups, producing message directly`);
const msg = new MessageEnvelope(message);
this.produceMessageItem(redisClient, msg, queue, (err, reply) => {
if (err) {
this.logger.error(`Failed to produce message for queue ${queueName}`, err);
cb(err);
}
else {
this.logger.info(`Successfully produced message ${reply} for queue ${queueName}`);
cb(null, [String(reply)]);
}
});
}
}
produce(msg, cb) {
if (!this.isUp()) {
this.logger.error('Cannot produce message: Producer instance is not running');
return cb(new ProducerInstanceNotRunningError());
}
const exchangeParams = msg.getExchange();
if (!exchangeParams) {
this.logger.error('Cannot produce message: No exchange parameters provided');
return cb(new ProducerMessageExchangeRequiredError());
}
this.logger.debug(`Producing message with exchange type ${EExchangeType[exchangeParams.type]}`);
const redisClient = this.redisClient.getInstance();
if (redisClient instanceof Error) {
this.logger.error('Cannot produce message: Redis client error', redisClient);
return cb(redisClient);
}
if (exchangeParams.type === EExchangeType.DIRECT) {
const queue = exchangeParams.params;
this.logger.debug(`Direct exchange: producing message for queue ${queue.name}@${queue.ns}`);
return this.produceMessage(redisClient, msg, queue, cb);
}
this.logger.debug(`Fanout exchange: getting queues for exchange ${exchangeParams.type}`);
_getExchangeQueues(redisClient, exchangeParams, (err, queues) => {
if (err) {
this.logger.error('Failed to get exchange queues', err);
return cb(err);
}
if (!queues?.length) {
this.logger.error('No matching queues found for exchange');
return cb(new ProducerExchangeNoMatchedQueueError());
}
this.logger.info(`Found ${queues.length} matching queues for exchange`);
const messages = [];
async.eachOf(queues, (queue, index, done) => {
this.logger.debug(`Producing message for queue ${queue.name}@${queue.ns} (${index + 1}/${queues.length})`);
this.produceMessage(redisClient, msg, queue, (err, reply) => {
if (err) {
this.logger.error(`Failed to produce message for queue ${queue.name}@${queue.ns}`, err);
return done(err);
}
if (reply) {
this.logger.debug(`Successfully produced ${reply.length} messages for queue ${queue.name}@${queue.ns}`);
messages.push(...reply);
}
done();
});
}, (err) => {
if (err) {
this.logger.error('Failed to produce messages for some queues', err);
return cb(err);
}
this.logger.info(`Successfully produced ${messages.length} messages across ${queues.length} queues`);
cb(null, messages);
});
});
}
}
//# sourceMappingURL=producer.js.map