UNPKG

redis-smq

Version:

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

236 lines 13.5 kB
import { async, CallbackEmptyReplyError, createLogger, withWatchTransaction, } from 'redis-smq-common'; import { withSharedPoolConnection } from '../../common/redis/redis-connection-pool/with-shared-pool-connection.js'; import { redisKeys } from '../../common/redis/redis-keys/redis-keys.js'; import { Configuration } from '../../config-manager/configuration.js'; import { ExchangeHasBoundQueuesError, InvalidFanoutExchangeParametersError, NamespaceMismatchError, QueueAlreadyBound, QueueNotBoundError, } from '../../errors/index.js'; import { _parseQueueParams } from '../../queue-manager/_/_parse-queue-params.js'; import { EQueueType } from '../../queue-manager/index.js'; import { _parseExchangeParams } from '../_/_parse-exchange-params.js'; import { _saveExchange } from '../_/_save-exchange.js'; import { _validateQueueBinding } from '../_/_validate-queue-binding.js'; import { _validateExchange } from '../_/_validate-exchange.js'; import { EExchangeProperty, EExchangeQueuePolicy, EExchangeType, } from '../types/index.js'; import { _getBoundQueues } from './_/_get-bound-queues.js'; import { _validateOperation } from '../../queue-operation-validator/_/_validate-operation.js'; import { EQueueOperation } from '../../queue-operation-validator/index.js'; export class ExchangeFanout { type = EExchangeType.FANOUT; logger; constructor() { this.logger = createLogger(Configuration.getConfig().logger, this.constructor.name); } matchQueues(exchange, cb) { return async.withOptionalCallback(cb, (callback) => { this.getBindings(exchange, callback); }); } create(exchange, queuePolicy, cb) { return async.withOptionalCallback(cb, (callback) => { const exchangeParams = _parseExchangeParams(exchange, this.type); if (exchangeParams instanceof Error) return callback(new InvalidFanoutExchangeParametersError()); withSharedPoolConnection((client, cb) => _saveExchange(client, exchangeParams, queuePolicy, cb), callback); }); } delete(exchange, cb) { return async.withOptionalCallback(cb, (callback) => { const exchangeParams = _parseExchangeParams(exchange, this.type); if (exchangeParams instanceof Error) return callback(exchangeParams); const { keyExchanges } = redisKeys.getMainKeys(); const { keyNamespaceExchanges } = redisKeys.getNamespaceKeys(exchangeParams.ns); const { keyExchange } = redisKeys.getExchangeKeys(exchangeParams.ns, exchangeParams.name); const { keyFanoutQueues } = redisKeys.getExchangeFanoutKeys(exchangeParams.ns, exchangeParams.name); const exchangeStr = JSON.stringify(exchangeParams); withSharedPoolConnection((client, outerCb) => { withWatchTransaction(client, (c, watch, done) => { let boundQueuesCount = 0; async.waterfall([ (cb1) => watch([ keyExchange, keyFanoutQueues, keyExchanges, keyNamespaceExchanges, ], cb1), (_, cb1) => _validateExchange(c, exchangeParams, true, cb1), (_, cb1) => c.sscanAll(keyFanoutQueues, {}, (err, queues) => { if (err) return cb1(err); boundQueuesCount = queues?.length ?? 0; if (boundQueuesCount > 0) { this.logger.warn(`delete: exchange has ${boundQueuesCount} bound queues, aborting`); return cb1(new ExchangeHasBoundQueuesError()); } cb1(); }), (_, cb1) => { const multi = c.multi(); multi.del(keyExchange); multi.del(keyFanoutQueues); multi.srem(keyExchanges, exchangeStr); multi.srem(keyNamespaceExchanges, exchangeStr); cb1(null, { multi }); }, ], done); }, (err) => { if (err) return outerCb(err); this.logger.info(`delete: deleted exchange ${exchangeParams.name}@${exchangeParams.ns}`); outerCb(); }, { maxAttempts: 5, onRetry: (attemptNo, maxAttempts) => this.logger.warn(`delete: concurrent modification, retrying attempt=${attemptNo}/${maxAttempts}`), }); }, callback); }); } bindQueue(queue, exchange, cb) { return async.withOptionalCallback(cb, (callback) => { const queueParams = _parseQueueParams(queue); const exchangeParams = _parseExchangeParams(exchange, this.type); if (queueParams instanceof Error) return callback(queueParams); if (exchangeParams instanceof Error) return callback(exchangeParams); if (queueParams.ns !== exchangeParams.ns) { return callback(new NamespaceMismatchError()); } this.logger.info(`bindQueue: bind queue=${queueParams.name}@${queueParams.ns} -> ex=${exchangeParams.name}@${exchangeParams.ns}`); const { keyQueueProperties, keyQueueExchangeBindings } = redisKeys.getQueueKeys(queueParams.ns, queueParams.name, null); const { keyExchange } = redisKeys.getExchangeKeys(exchangeParams.ns, exchangeParams.name); const { keyFanoutQueues } = redisKeys.getExchangeFanoutKeys(exchangeParams.ns, exchangeParams.name); const { keyExchanges } = redisKeys.getMainKeys(); const { keyNamespaceExchanges } = redisKeys.getNamespaceKeys(queueParams.ns); const exchangeStr = JSON.stringify(exchangeParams); const queueStr = JSON.stringify(queueParams); withSharedPoolConnection((client, outerCb) => { async.series([ (cb) => _validateOperation(client, queueParams, EQueueOperation.BIND_EXCHANGE, cb), (cb) => withWatchTransaction(client, (c, watch, done) => { let exchangeQueuePolicy; async.waterfall([ (cb1) => watch([ keyExchange, keyQueueProperties, keyFanoutQueues, keyQueueExchangeBindings, keyExchanges, keyNamespaceExchanges, ], cb1), (_, cb1) => _validateQueueBinding(c, exchangeParams, queueParams, (err, reply) => { if (err) return cb1(err); if (!reply) return cb1(new CallbackEmptyReplyError()); const [queueProperties] = reply; exchangeQueuePolicy = queueProperties.queueType === EQueueType.PRIORITY_QUEUE ? EExchangeQueuePolicy.PRIORITY : EExchangeQueuePolicy.STANDARD; cb1(); }), (_, cb1) => c.sismember(keyFanoutQueues, queueStr, (err, reply) => { if (err) return cb1(err); if (reply === 1) { this.logger.debug('bindQueue: already bound'); return cb1(new QueueAlreadyBound()); } cb1(); }), (_, cb1) => { const typeField = String(EExchangeProperty.TYPE); const queuePolicyField = String(EExchangeProperty.QUEUE_POLICY); const multi = c.multi(); multi.hset(keyExchange, typeField, EExchangeType.FANOUT); multi.hset(keyExchange, queuePolicyField, exchangeQueuePolicy); multi.sadd(keyExchanges, exchangeStr); multi.sadd(keyNamespaceExchanges, exchangeStr); multi.sadd(keyFanoutQueues, queueStr); multi.sadd(keyQueueExchangeBindings, exchangeStr); cb1(null, { multi }); }, ], done); }, (err) => { if (err) { if (err instanceof QueueAlreadyBound) return cb(); return cb(err); } this.logger.info(`bindQueue: bound queue=${queueParams.name}@${queueParams.ns} -> ex=${exchangeParams.name}@${exchangeParams.ns}`); cb(); }, { maxAttempts: 5, onRetry: (attemptNo, maxAttempts) => this.logger.warn(`bindQueue: concurrent modification, retrying attempt=${attemptNo}/${maxAttempts}`), }), ], (err) => outerCb(err)); }, callback); }); } unbindQueue(queue, exchange, cb) { return async.withOptionalCallback(cb, (callback) => { const queueParams = _parseQueueParams(queue); const exchangeParams = _parseExchangeParams(exchange, this.type); if (queueParams instanceof Error) return callback(queueParams); if (exchangeParams instanceof Error) return callback(exchangeParams); if (queueParams.ns !== exchangeParams.ns) { this.logger.error(`Namespace mismatch`); return callback(new NamespaceMismatchError()); } const { keyQueueProperties, keyQueueExchangeBindings } = redisKeys.getQueueKeys(queueParams.ns, queueParams.name, null); const { keyExchange } = redisKeys.getExchangeKeys(exchangeParams.ns, exchangeParams.name); const { keyFanoutQueues } = redisKeys.getExchangeFanoutKeys(exchangeParams.ns, exchangeParams.name); const queueStr = JSON.stringify(queueParams); const exchangeStr = JSON.stringify(exchangeParams); withSharedPoolConnection((client, outerCb) => { async.series([ (cb) => _validateOperation(client, queueParams, EQueueOperation.UNBIND_EXCHANGE, cb), (cb) => withWatchTransaction(client, (c, watch, done) => { async.waterfall([ (cb1) => watch([ keyExchange, keyQueueProperties, keyFanoutQueues, keyQueueExchangeBindings, ], cb1), (_, cb1) => _validateExchange(c, exchangeParams, true, cb1), (_, cb1) => c.sismember(keyFanoutQueues, queueStr, (err, reply) => { if (err) return cb1(err); if (reply !== 1) return cb1(new QueueNotBoundError()); cb1(); }), (_, cb1) => { const multi = c.multi(); multi.srem(keyFanoutQueues, queueStr); multi.srem(keyQueueExchangeBindings, exchangeStr); cb1(null, { multi }); }, ], done); }, (err) => { if (err) return cb(err); this.logger.info(`unbindQueue: unbound queue=${queueParams.name}@${queueParams.ns} from ex=${exchangeParams.name}@${exchangeParams.ns}`); cb(); }, { maxAttempts: 5, onRetry: (attemptNo, maxAttempts) => this.logger.warn(`unbindQueue: concurrent modification, retrying attempt=${attemptNo}/${maxAttempts}`), }), ], (err) => outerCb(err)); }, callback); }); } getBindings(exchange, cb) { return async.withOptionalCallback(cb, (callback) => { const exchangeParams = _parseExchangeParams(exchange, this.type); if (exchangeParams instanceof Error) return callback(exchangeParams); withSharedPoolConnection((client, cb) => _getBoundQueues(client, exchangeParams, cb), callback); }); } } //# sourceMappingURL=exchange-fanout.js.map