UNPKG

redis-smq

Version:

A high-performance, reliable, and scalable message queue for Node.js.

247 lines 10.5 kB
import { async, CallbackEmptyReplyError, createLogger, PanicError, Runnable, } from 'redis-smq-common'; import { Configuration } from '../config-manager/configuration.js'; import { _parseQueueExtendedParams } from '../queue-manager/_/_parse-queue-extended-params.js'; import { MessageHandlerRunner } from './message-handler-runner/message-handler-runner.js'; import { MultiplexedMessageHandlerRunner } from './message-handler-runner/multiplexed-message-handler-runner.js'; import { eventPublisher } from './event-publisher.js'; import { redisKeys } from '../common/redis/redis-keys/redis-keys.js'; import { HeartbeatFactory } from '../common/heartbeat/heartbeat.js'; import { heartbeatEventPublisher } from './heartbeat-event-publisher.js'; import { _parseConsumerOptions } from './_/_parse-consumer-options.js'; import { RedisConnectionPool } from '../common/redis/redis-connection-pool/redis-connection-pool.js'; import { ERedisConnectionAcquisitionMode } from '../common/redis/redis-connection-pool/types/connection-pool.js'; export class Consumer extends Runnable { static defaultOptions = { enableMultiplexing: false, heartbeatTTL: 60_000, batchAcks: { enabled: true, batchSize: 100, batchTimeoutMs: 10_000, }, batchUnacks: { enabled: true, batchSize: 100, batchTimeoutMs: 10_000, }, }; consumerContext; messageHandlerRunner; logger; heartbeat = null; consumerOptions; redisClient = null; constructor(consumerOptions) { super(); this.consumerOptions = _parseConsumerOptions(consumerOptions, Consumer.defaultOptions); const config = Configuration.getConfig(); this.logger = createLogger(config.logger, `${this.constructor.name.toLowerCase()}-${this.id}`); this.consumerContext = { consumerId: this.getId(), config: config, logger: this.logger, consumerOptions: this.consumerOptions, }; eventPublisher(this); this.messageHandlerRunner = this.consumerOptions.enableMultiplexing ? new MultiplexedMessageHandlerRunner(this.consumerContext) : new MessageHandlerRunner(this.consumerContext); this.messageHandlerRunner.on('consumer.messageHandlerRunner.error', (err) => { this.logger.error(`Message handler runner error: ${err.message}`); this.handleError(err); }); this.logger.info(`Consumer initialized${this.consumerOptions.enableMultiplexing ? ' with multiplexing enabled' : ''}`); } initRedisClient = (cb) => { RedisConnectionPool.getInstance().acquire(ERedisConnectionAcquisitionMode.SHARED, (err, client) => { if (err) return cb(err); if (!client) return cb(new CallbackEmptyReplyError()); this.redisClient = client; cb(); }); }; releaseRedisClient = (cb) => { if (this.redisClient) { RedisConnectionPool.getInstance().release(this.redisClient); this.redisClient = null; } cb(); }; setUpHeartbeat = (cb) => { if (!this.redisClient) return cb(new PanicError({ message: 'Redis client is not initialized', })); this.logger.debug('Setting up consumer heartbeat'); const { keyConsumerHeartbeat } = redisKeys.getConsumerKeys(this.id); try { this.heartbeat = HeartbeatFactory(this.redisClient, this.logger, { heartbeatKey: keyConsumerHeartbeat, componentId: this.id, componentType: this.constructor.name, heartbeatTTL: this.consumerOptions.heartbeatTTL, }, heartbeatEventPublisher); this.heartbeat.on('heartbeat.error', (err) => { this.logger.error(`Heartbeat error: ${err.message}`); this.handleError(err); }); this.logger.debug('Starting heartbeat'); this.heartbeat.run((err) => { if (err) { this.logger.error(`Failed to start heartbeat: ${err.message}`); cb(err); } else { this.logger.debug('Heartbeat started successfully'); cb(); } }); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); cb(err); } }; shutdownHeartbeat = (cb) => { if (this.heartbeat) { this.logger.debug('Shutting down heartbeat'); this.heartbeat.shutdown((err) => { if (err) { this.logger.warn(`Error during heartbeat shutdown: ${err.message}`); } this.logger.debug('Heartbeat shut down'); this.heartbeat = null; cb(); }); } else { this.logger.debug('No heartbeat to shut down'); cb(); } }; runMessageHandlers = (cb) => { this.logger.debug('Starting message handlers'); this.messageHandlerRunner.run((err) => { if (err) { this.logger.error(`Failed to start message handlers: ${err.message}`); cb(err); } else { this.logger.debug('Message handlers started successfully'); cb(); } }); }; shutdownMessageHandlers = (cb) => { this.logger.debug('Shutting down message handlers'); this.messageHandlerRunner.shutdown((err) => { if (err) { this.logger.warn(`Error during message handlers shutdown: ${err.message}`); } this.logger.debug('Message handlers shut down'); cb(); }); }; goingUp() { return super.goingUp().concat([ (cb) => { this.logger.debug(`Emitting consumer.goingUp event for consumer ${this.id}`); this.emit('consumer.goingUp', this.id); cb(); }, this.initRedisClient, this.setUpHeartbeat, this.runMessageHandlers, ]); } goingDown() { this.emit('consumer.goingDown', this.id); return [ this.shutdownMessageHandlers, this.shutdownHeartbeat, this.releaseRedisClient, ].concat(super.goingDown()); } finalizeUp() { super.finalizeUp(); this.emit('consumer.up', this.id); } finalizeDown() { super.finalizeDown(); this.emit('consumer.down', this.id); } handleError(err) { if (!this.isOperational()) return; this.logger.error(`Consumer error: ${err.message}`, err); this.logger.debug(`Emitting consumer.error event for consumer ${this.id}`); this.emit('consumer.error', err, this.id); super.handleError(err); } consume(queue, messageHandler, cb) { return async.withOptionalCallback(cb, (callback) => { this.logger.info(`Setting up consumption for queue: ${typeof queue === 'string' ? queue : JSON.stringify(queue)}`); const parsedQueueParams = _parseQueueExtendedParams(queue); if (parsedQueueParams instanceof Error) { this.logger.error(`Failed to parse queue parameters: ${parsedQueueParams.message}`); callback(parsedQueueParams); } else { this.logger.debug(`Adding message handler for queue: ${JSON.stringify(parsedQueueParams)}`); this.messageHandlerRunner.addMessageHandler(parsedQueueParams, messageHandler, (err) => { if (err) { this.logger.error(`Failed to add message handler: ${err.message}`); return callback(err); } this.logger.info(`Successfully set up consumption for queue: ${parsedQueueParams.queueParams.name}@${parsedQueueParams.queueParams.ns}${parsedQueueParams.groupId ? `, group: ${parsedQueueParams.groupId}` : ''}`); callback(); }); } }); } cancel(queue, cb) { return async.withOptionalCallback(cb, (callback) => { this.logger.info(`Canceling consumption for queue: ${typeof queue === 'string' ? queue : JSON.stringify(queue)}`); const parsedQueueParams = _parseQueueExtendedParams(queue); if (parsedQueueParams instanceof Error) { this.logger.error(`Failed to parse queue parameters: ${parsedQueueParams.message}`); callback(parsedQueueParams); return; } this.logger.debug(`Removing message handler for queue: ${JSON.stringify(parsedQueueParams)}`); this.messageHandlerRunner.removeMessageHandler(parsedQueueParams, (err) => { if (err) { this.logger.error(`Failed to remove message handler: ${err.message}`); callback(err); } else { this.logger.info(`Successfully canceled consumption for queue: ${parsedQueueParams.queueParams.name}@${parsedQueueParams.queueParams.ns}${parsedQueueParams.groupId ? `, group: ${parsedQueueParams.groupId}` : ''}`); callback(); } }); }); } getQueues() { this.logger.debug('Getting list of queues being consumed'); const queues = this.messageHandlerRunner.getQueues(); this.logger.debug(`Consumer is handling ${queues.length} queues: ${JSON.stringify(queues)}`); return queues; } getQueuesWithStatus() { this.logger.debug('Getting list of queues being consumed with consumption status...'); const queues = this.messageHandlerRunner.getQueueWithStatus(); this.logger.debug(`Consumer is handling ${queues.length} queues: ${JSON.stringify(queues)}`); return queues; } static setDefaultOptions(options) { this.defaultOptions = _parseConsumerOptions(options, Consumer.defaultOptions); } static getDefaultOptions() { return { ...this.defaultOptions, }; } } //# sourceMappingURL=consumer.js.map