UNPKG

redis-smq

Version:

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

243 lines 9.56 kB
import { async, CallbackEmptyReplyError, 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 '../event-bus/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, `producer:${this.id}`); if (Configuration.getSetConfig().eventBus.enabled) { eventBusPublisher(this, this.eventBus, this.logger); } } getLogger() { return this.logger; } initQueueConsumerGroupsHandler = (cb) => { this.queueConsumerGroupsHandler = new QueueConsumerGroupsCache(this, this.redisClient, this.eventBus, this.logger); this.queueConsumerGroupsHandler.run((err) => cb(err)); }; shutDownQueueConsumerGroupsHandler = (cb) => { if (this.queueConsumerGroupsHandler) { this.queueConsumerGroupsHandler.shutdown(() => { this.queueConsumerGroupsHandler = null; cb(); }); } else cb(); }; initRedisClient = (cb) => this.redisClient.getSetInstance((err, client) => { if (err) cb(err); else if (!client) cb(new CallbackEmptyReplyError()); else { client.on('error', (err) => this.handleError(err)); cb(); } }); goingUp() { return super.goingUp().concat([ this.redisClient.init, this.eventBus.init, (cb) => { this.emit('producer.goingUp', this.id); cb(); }, this.initRedisClient, this.initQueueConsumerGroupsHandler, ]); } up(cb) { super.up(() => { this.emit('producer.up', this.id); cb(null, true); }); } goingDown() { this.emit('producer.goingDown', this.id); return [ this.shutDownQueueConsumerGroupsHandler, this.redisClient.shutdown, ].concat(super.goingDown()); } down(cb) { super.down(() => { this.emit('producer.down', this.id); setTimeout(() => this.eventBus.shutdown(() => cb(null, true)), 1000); }); } getQueueConsumerGroupsHandler() { if (!this.queueConsumerGroupsHandler) throw new PanicError(`Expected an instance of QueueConsumerGroupsHandler`); return this.queueConsumerGroupsHandler; } enqueue(redisClient, message, cb) { const messageState = message.getMessageState(); messageState.setPublishedAt(Date.now()); const messageId = message.getId(); const keys = redisKeys.getQueueKeys(message.getDestinationQueue(), message.getConsumerGroupId()); const { keyMessage } = redisKeys.getMessageKeys(messageId); const scriptArgs = [ EQueueProperty.QUEUE_TYPE, EQueueProperty.MESSAGES_COUNT, EQueueType.PRIORITY_QUEUE, EQueueType.LIFO_QUEUE, EQueueType.FIFO_QUEUE, message.producibleMessage.getPriority() ?? '', 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) return cb(err); switch (reply) { case 'OK': return cb(); case 'QUEUE_NOT_FOUND': return cb(new ProducerQueueNotFoundError()); case 'MESSAGE_PRIORITY_REQUIRED': return cb(new ProducerMessagePriorityRequiredError()); case 'PRIORITY_QUEUING_NOT_ENABLED': return cb(new ProducerPriorityQueuingNotEnabledError()); case 'UNKNOWN_QUEUE_TYPE': return cb(new ProducerUnknownQueueTypeError()); default: return cb(new ProducerError()); } }); } produceMessageItem(redisClient, message, queue, cb) { const messageId = message .setDestinationQueue(queue) .getMessageState() .getId(); const handleResult = (err) => { if (err) { cb(err); } else { const action = message.isSchedulable() ? 'scheduled' : 'published'; this.logger.info(`Message (ID ${messageId}) has been ${action}.`); if (!message.isSchedulable()) { this.emit('producer.messagePublished', messageId, { queueParams: queue, groupId: message.getConsumerGroupId() }, this.id); } cb(null, messageId); } }; if (message.isSchedulable()) { _scheduleMessage(redisClient, message, handleResult); } else { this.enqueue(redisClient, message, handleResult); } } produceMessage(redisClient, message, queue, cb) { const { exists, consumerGroups } = this.getQueueConsumerGroupsHandler().getConsumerGroups(queue); if (exists) { if (!consumerGroups.length) { cb(new ProducerQueueMissingConsumerGroupsError()); } const ids = []; async.eachOf(consumerGroups, (group, _, done) => { const msg = new MessageEnvelope(message).setConsumerGroupId(group); this.produceMessageItem(redisClient, msg, queue, (err, reply) => { if (err) done(err); else { ids.push(String(reply)); done(); } }); }, (err) => { if (err) cb(err); else cb(null, ids); }); } else { const msg = new MessageEnvelope(message); this.produceMessageItem(redisClient, msg, queue, (err, reply) => { if (err) cb(err); else cb(null, [String(reply)]); }); } } produce(msg, cb) { if (!this.isUp()) { return cb(new ProducerInstanceNotRunningError()); } const redisClient = this.redisClient.getInstance(); if (redisClient instanceof Error) { return cb(redisClient); } const exchangeParams = msg.getExchange(); if (!exchangeParams) { return cb(new ProducerMessageExchangeRequiredError()); } if (exchangeParams.type === EExchangeType.DIRECT) { const queue = exchangeParams.params; return this.produceMessage(redisClient, msg, queue, cb); } _getExchangeQueues(redisClient, exchangeParams, (err, queues) => { if (err) { return cb(err); } if (!queues?.length) { return cb(new ProducerExchangeNoMatchedQueueError()); } const messages = []; async.eachOf(queues, (queue, index, done) => { this.produceMessage(redisClient, msg, queue, (err, reply) => { if (err) { return done(err); } if (reply) { messages.push(...reply); } done(); }); }, (err) => { if (err) { return cb(err); } cb(null, messages); }); }); } } //# sourceMappingURL=producer.js.map