UNPKG

redis-smq

Version:

A simple high-performance Redis message queue for Node.js.

317 lines 15.4 kB
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