UNPKG

ali-ons-sdk

Version:

Aliyun Open Notification Service Client

716 lines (647 loc) 21.7 kB
'use strict'; const is = require('is-type-of'); const gather = require('p-gather'); const sleep = require('mz-modules/sleep'); const utility = require('utility'); const MixAll = require('./mix_all'); const MQClientAPI = require('./mq_client_api'); const MessageQueue = require('./message_queue'); const PermName = require('./protocol/perm_name'); const TopicPublishInfo = require('./producer/topic_publish_info'); const RequestCode = require('./protocol/request_code'); const MessageDecoder = require('./message/message_decoder'); const MessageConst = require('./message/message_const'); const ByteBuffer = require('byte'); const instanceTable = new Map(); class MQClient extends MQClientAPI { /** * metaq client * @param {Object} clientConfig - * @constructor */ constructor(clientConfig) { super(clientConfig.options); this._clientConfig = clientConfig; this._brokerAddrTable = new Map(); this._consumerTable = new Map(); this._producerTable = new Map(); this._topicRouteTable = new Map(); this.on('request', this.handleServerRequest.bind(this)); } async handleServerRequest(request, address) { const command = request.data; if (!command) return; switch (command.code) { case RequestCode.CHECK_TRANSACTION_STATE: await this.checkTransactionState(command, address); break; default: break; } } async checkTransactionState(command, addr) { const byteBuffer = ByteBuffer.wrap(command.body); const remoteCommand = MessageDecoder.decode(byteBuffer); if (!remoteCommand) { this.logger.warn('checkTransactionState, decode message failed'); } const group = remoteCommand.properties && remoteCommand.properties[MessageConst.PROPERTY_PRODUCER_GROUP]; if (!group) { this.logger.warn('checkTransactionState, pick producer group failed'); } const producer = this._producerTable.get(group); const requestHeader = command.decodeCommandCustomHeader(); await producer.checkTransactionState(addr, remoteCommand, requestHeader); } /** * @property {String} MQClient#clientId */ get clientId() { return this._clientConfig.clientId; } /** * @property {Number} MQClient#pollNameServerInteval */ get pollNameServerInteval() { return this._clientConfig.pollNameServerInteval; } /** * @property {Number} MQClient#heartbeatBrokerInterval */ get heartbeatBrokerInterval() { return this._clientConfig.heartbeatBrokerInterval; } /** * @property {Number} MQClient#persistConsumerOffsetInterval */ get persistConsumerOffsetInterval() { return this._clientConfig.persistConsumerOffsetInterval; } /** * @property {Number} MQClient#rebalanceInterval */ get rebalanceInterval() { return this._clientConfig.rebalanceInterval; } /** * start the client */ async init() { await super.init(); await this.updateAllTopicRouterInfo(); await this.sendHeartbeatToAllBroker(); await this.doRebalance(); this.startScheduledTask('updateAllTopicRouterInfo', this.pollNameServerInteval); this.startScheduledTask('sendHeartbeatToAllBroker', this.heartbeatBrokerInterval); this.startScheduledTask('doRebalance', this.rebalanceInterval); this.startScheduledTask('persistAllConsumerOffset', this.persistConsumerOffsetInterval); } async close() { if (this._consumerTable.size || this._producerTable.size) { return; } await super.close(); // todo: } /** * start a schedule task * @param {String} name - method name * @param {Number} interval - schedule interval * @param {Number} [delay] - delay time interval * @return {void} */ startScheduledTask(name, interval, delay) { (async () => { await sleep(delay || interval); while (this._inited) { try { this.logger.info('[mq:client] execute `%s` at %s', name, utility.YYYYMMDDHHmmss()); await this[name](); } catch (err) { this.logger.error(err); } await sleep(interval); } })(); } /** * regitser consumer * @param {String} group - consumer group name * @param {Consumer} consumer - consumer instance * @return {void} */ registerConsumer(group, consumer) { if (this._consumerTable.has(group)) { this.logger.warn('[mq:client] the consumer group [%s] exist already.', group); return; } this._consumerTable.set(group, consumer); this.logger.info('[mq:client] new consumer has regitsered, group: %s', group); } /** * unregister consumer * @param {String} group - consumer group name * @return {void} */ async unregisterConsumer(group) { this._consumerTable.delete(group); await this.unregister(null, group); this.logger.info('[mq:client] unregister consumer, group: %s', group); } /** * register producer * @param {String} group - producer group name * @param {Producer} producer - producer * @return {void} */ registerProducer(group, producer) { if (this._producerTable.has(group)) { this.logger.warn('[mq:client] the producer group [%s] exist already.', group); return; } this._producerTable.set(group, producer); this.logger.info('[mq:client] new producer has regitsered, group: %s', group); } /** * unregister producer * @param {String} group - producer group name * @return {void} */ async unregisterProducer(group) { this._producerTable.delete(group); await this.unregister(group, null); this.logger.info('[mq:client] unregister producer, group: %s', group); } /** * notify all broker that producer or consumer is offline * @param {String} producerGroup - producer group name * @param {String} consumerGroup - consumer group name * @return {void} */ async unregister(producerGroup, consumerGroup) { const brokerAddrTable = this._brokerAddrTable; for (const brokerName of brokerAddrTable.keys()) { const oneTable = brokerAddrTable.get(brokerName); if (!oneTable) { continue; } for (const id in oneTable) { const addr = oneTable[id]; if (addr) { await this.unregisterClient(addr, this.clientId, producerGroup, consumerGroup, 3000); } } } } /** * update all router info * @return {void} */ async updateAllTopicRouterInfo() { const topics = []; // consumer for (const groupName of this._consumerTable.keys()) { const consumer = this._consumerTable.get(groupName); if (!consumer) { continue; } for (const topic of consumer.subscriptions.keys()) { topics.push(topic); } } // producer for (const groupName of this._producerTable.keys()) { const producer = this._producerTable.get(groupName); if (!producer) { continue; } for (const topic of producer.publishTopicList) { topics.push(topic); } } this.logger.info('[mq:client] try to update all topic route info. topic: %j', topics); const ret = await gather(topics.map(topic => this.updateTopicRouteInfoFromNameServer(topic))); ret.forEach(data => { if (data.isError) { data.error.message = `[mq:client] updateAllTopicRouterInfo occurred error, ${data.error.message}`; this.emit('error', data.error); } }); } /** * update topic route info * @param {String} topic - topic * @param {Boolean} [isDefault] - is default or not * @param {Producer} [defaultMQProducer] - producer */ async updateTopicRouteInfoFromNameServer(topic, isDefault, defaultMQProducer) { this.logger.info('[mq:client] updateTopicRouteInfoFromNameServer() topic: %s, isDefault: %s', topic, !!isDefault); if (isDefault && defaultMQProducer) { const topicRouteData = await this.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.createTopicKey, 3000); if (topicRouteData) { for (const data of topicRouteData.queueDatas) { const queueNums = defaultMQProducer.defaultTopicQueueNums < data.readQueueNums ? defaultMQProducer.defaultTopicQueueNums : data.readQueueNums; data.readQueueNums = queueNums; data.writeQueueNums = queueNums; } this._refreshTopicRouteInfo(topic, topicRouteData); } } else { const topicRouteData = await this.getTopicRouteInfoFromNameServer(topic, 3000); if (topicRouteData) { this._refreshTopicRouteInfo(topic, topicRouteData); } } } _refreshTopicRouteInfo(topic, topicRouteData) { if (!topicRouteData) { return; } // @example // topicRouteData => { // "brokerDatas": [{ // "brokerAddrs": { // "0": "10.218.145.166:10911" // }, // "brokerName": "taobaodaily-02" // }], // "filterServerTable": {}, // "queueDatas": [{ // "brokerName": "taobaodaily-02", // "perm": 6, // "readQueueNums": 8, // "topicSynFlag": 0, // "writeQueueNums": 8 // }] // } const prev = this._topicRouteTable.get(topic); const needUpdate = this._isRouteDataChanged(prev, topicRouteData) || this._isNeedUpdateTopicRouteInfo(topic); this.logger.info('[mq:client] refresh route data for topic: %s, route data: %j, needUpdate: %s', topic, topicRouteData, needUpdate); if (!needUpdate) { return; } // @example: // this.brokerAddrTable => { // "taobaodaily-02": { // "0": "10.218.145.166:10911" // } // } for (const brokerData of topicRouteData.brokerDatas) { this._brokerAddrTable.set(brokerData.brokerName, brokerData.brokerAddrs); } // update producer route data const publishInfo = this._topicRouteData2TopicPublishInfo(topic, topicRouteData); publishInfo.haveTopicRouterInfo = true; for (const groupName of this._producerTable.keys()) { const producer = this._producerTable.get(groupName); if (producer) { producer.updateTopicPublishInfo(topic, publishInfo); } } // 更新订阅队列信息 const subscribeInfo = this._topicRouteData2TopicSubscribeInfo(topic, topicRouteData); for (const groupName of this._consumerTable.keys()) { const consumer = this._consumerTable.get(groupName); if (consumer) { consumer.updateTopicSubscribeInfo(topic, subscribeInfo); } } this.logger.info('[mq:client] update topicRouteTable for topic: %s, with topicRouteData: %j', topic, topicRouteData); this._topicRouteTable.set(topic, topicRouteData); } _isRouteDataChanged(prev, current) { if (is.nullOrUndefined(prev) || is.nullOrUndefined(current)) { return true; } // todo: performance enhance ? return JSON.stringify(prev) !== JSON.stringify(current); } _isNeedUpdateTopicRouteInfo(topic) { // producer for (const groupName of this._producerTable.keys()) { const producer = this._producerTable.get(groupName); if (producer && producer.isPublishTopicNeedUpdate(topic)) { return true; } } // consumer for (const groupName of this._consumerTable.keys()) { const consumer = this._consumerTable.get(groupName); if (consumer && consumer.isSubscribeTopicNeedUpdate(topic)) { return true; } } return false; } _topicRouteData2TopicPublishInfo(topic, topicRouteData) { const info = new TopicPublishInfo(); // 顺序消息 if (topicRouteData.orderTopicConf && topicRouteData.orderTopicConf.length) { const brokers = topicRouteData.orderTopicConf.split(';'); for (const broker of brokers) { const item = broker.split(':'); const nums = parseInt(item[1], 10); for (let i = 0; i < nums; i++) { info.messageQueueList.push(new MessageQueue(topic, item[0], i)); } } info.orderTopic = true; } else { // 非顺序消息 for (const queueData of topicRouteData.queueDatas) { if (PermName.isWriteable(queueData.perm)) { const brokerData = topicRouteData.brokerDatas.find(data => { return data.brokerName === queueData.brokerName; }); if (!brokerData || !brokerData.brokerAddrs[MixAll.MASTER_ID]) { continue; } for (let i = 0, nums = queueData.writeQueueNums; i < nums; i++) { info.messageQueueList.push(new MessageQueue(topic, queueData.brokerName, i)); } } } info.orderTopic = false; } return info; } _topicRouteData2TopicSubscribeInfo(topic, topicRouteData) { const messageQueueList = []; for (const queueData of topicRouteData.queueDatas) { if (PermName.isReadable(queueData.perm)) { for (let i = 0, nums = queueData.readQueueNums; i < nums; i++) { messageQueueList.push(new MessageQueue(topic, queueData.brokerName, i)); } } } return messageQueueList; } /** * send heartbeat to all brokers * @return {void} */ async sendHeartbeatToAllBroker() { this._cleanOfflineBroker(); const heartbeatData = this._prepareHeartbeatData(); const consumerEmpty = heartbeatData.consumerDataSet.length === 0; const producerEmpty = heartbeatData.producerDataSet.length === 0; if (consumerEmpty && producerEmpty) { this.logger.info('[mq:client] sending hearbeat, but no consumer and no producer'); return; } const brokers = []; for (const brokerName of this._brokerAddrTable.keys()) { const oneTable = this._brokerAddrTable.get(brokerName); if (!oneTable) { continue; } for (const id in oneTable) { const addr = oneTable[id]; if (!addr) { continue; } // 说明只有Producer,则不向Slave发心跳 if (consumerEmpty && Number(id) !== MixAll.MASTER_ID) { continue; } brokers.push(addr); } } const ret = await gather(brokers.map(addr => this.sendHearbeat(addr, heartbeatData, 3000))); this.logger.info('[mq:client] send heartbeat: %j to : %j, and result: %j', heartbeatData, brokers, ret); } _prepareHeartbeatData() { const heartbeatData = { clientID: this.clientId, consumerDataSet: [], producerDataSet: [], }; // Consumer for (const groupName of this._consumerTable.keys()) { const consumer = this._consumerTable.get(groupName); if (consumer) { const subscriptionDataSet = []; for (const topic of consumer.subscriptions.keys()) { const data = consumer.subscriptions.get(topic); if (data && data.subscriptionData) { subscriptionDataSet.push(data.subscriptionData); } } heartbeatData.consumerDataSet.push({ groupName: consumer.consumerGroup, consumeType: consumer.consumeType, messageModel: consumer.messageModel, consumeFromWhere: consumer.consumeFromWhere, subscriptionDataSet, unitMode: consumer.unitMode, }); } } // Producer for (const groupName of this._producerTable.keys()) { const producer = this._producerTable.get(groupName); if (producer) { heartbeatData.producerDataSet.push({ groupName, }); } } return heartbeatData; } _cleanOfflineBroker() { for (const brokerName of this._brokerAddrTable.keys()) { const oneTable = this._brokerAddrTable.get(brokerName); let exists = false; for (const brokerId in oneTable) { const addr = oneTable[brokerId]; if (this._isBrokerAddrExistInTopicRouteTable(addr)) { exists = true; } else { delete oneTable[brokerId]; this.logger.info('[mq:client] the broker addr[%s %s] is offline, remove it', brokerName, addr); } } if (!exists) { this._brokerAddrTable.delete(brokerName); this.logger.info('[mq:client] the broker[%s] name\'s host is offline, remove it', brokerName); } } } _isBrokerAddrExistInTopicRouteTable(addr) { for (const topic of this._topicRouteTable.keys()) { const topicRouteData = this._topicRouteTable.get(topic); for (const brokerData of topicRouteData.brokerDatas) { for (const brokerId in brokerData.brokerAddrs) { if (brokerData.brokerAddrs[brokerId] === addr) { return true; } } } } return false; } /** * rebalance * @return {void} */ async doRebalance() { for (const groupName of this._consumerTable.keys()) { const consumer = this._consumerTable.get(groupName); if (consumer) { await consumer.doRebalance(); } } } /** * get all consumer list of topic * @param {String} topic - topic * @param {String} group - consumer group * @return {Array} consumer list */ async findConsumerIdList(topic, group) { let brokerAddr = this.findBrokerAddrByTopic(topic); if (!brokerAddr) { await this.updateTopicRouteInfoFromNameServer(topic); brokerAddr = this.findBrokerAddrByTopic(topic); if (!brokerAddr) { throw new Error(`The broker of topic[${topic}] not exist`); } else { return await this.getConsumerIdListByGroup(brokerAddr, group, 3000); } } else { return await this.getConsumerIdListByGroup(brokerAddr, group, 3000); } } // get broker address by topic findBrokerAddrByTopic(topic) { const topicRouteData = this._topicRouteTable.get(topic); if (topicRouteData) { const brokerDatas = topicRouteData.brokerDatas || []; const broker = brokerDatas[0]; if (broker) { // master first, not found try slave const addr = broker.brokerAddrs[MixAll.MASTER_ID]; if (!addr) { for (const id in broker.brokerAddrs) { return broker.brokerAddrs[id]; } } return addr; } } return null; } /** * find broker address * @param {String} brokerName - broker name * @return {Object} broker info */ findBrokerAddressInAdmin(brokerName) { const map = this._brokerAddrTable.get(brokerName); if (!map) { return null; } if (map[MixAll.MASTER_ID]) { return { brokerAddr: map[MixAll.MASTER_ID], slave: false, }; } for (const id in map) { if (map[id]) { return { brokerAddr: map[id], slave: true, }; } } return null; } findBrokerAddressInSubscribe(brokerName, brokerId, onlyThisBroker) { let brokerAddr = null; let slave = false; let found = false; const map = this._brokerAddrTable.get(brokerName); if (map) { brokerAddr = map[brokerId]; slave = Number(brokerId) !== MixAll.MASTER_ID; found = !is.nullOrUndefined(brokerAddr); // 尝试寻找其他Broker if (!found && !onlyThisBroker) { for (const id in map) { brokerAddr = map[id]; slave = Number(id) !== MixAll.MASTER_ID; found = true; break; } } } if (found) { return { brokerAddr, slave, }; } return null; } /** * get current offset of message queue * @param {MessageQueue} messageQueue - message queue * @return {Number} offset */ async maxOffset(messageQueue) { let brokerAddr = this.findBrokerAddressInPublish(messageQueue.brokerName); if (!brokerAddr) { await this.updateTopicRouteInfoFromNameServer(messageQueue.topic); brokerAddr = this.findBrokerAddressInPublish(messageQueue.brokerName); if (!brokerAddr) { throw new Error(`The broker[${messageQueue.brokerName}] not exist`); } else { return await this.getMaxOffset(brokerAddr, messageQueue.topic, messageQueue.queueId, 3000); } } else { return await this.getMaxOffset(brokerAddr, messageQueue.topic, messageQueue.queueId, 3000); } } // should be master findBrokerAddressInPublish(brokerName) { const map = this._brokerAddrTable.get(brokerName); return map && map[MixAll.MASTER_ID]; } async persistAllConsumerOffset() { for (const groupName of this._consumerTable.keys()) { const consumer = this._consumerTable.get(groupName); if (consumer) { await consumer.persistConsumerOffset(); } } } async searchOffset(messageQueue, timestamp) { let brokerAddr = this.findBrokerAddressInPublish(messageQueue.brokerName); if (!brokerAddr) { await this.updateTopicRouteInfoFromNameServer(messageQueue.topic); brokerAddr = this.findBrokerAddressInPublish(messageQueue.brokerName); if (!brokerAddr) { throw new Error('The broker[' + messageQueue.brokerName + '] not exist'); } else { return await super.searchOffset(brokerAddr, messageQueue.topic, messageQueue.queueId, timestamp, 3000); } } else { return await super.searchOffset(brokerAddr, messageQueue.topic, messageQueue.queueId, timestamp, 3000); } } static getAndCreateMQClient(clientConfig) { const clientId = clientConfig.clientId; let instance = instanceTable.get(clientId); if (!instance) { instance = new MQClient(clientConfig); instanceTable.set(clientId, instance); instance.once('close', () => { instanceTable.delete(clientId); }); } return instance; } } module.exports = MQClient;