UNPKG

ali-ons-sdk

Version:

Aliyun Open Notification Service Client

500 lines (452 loc) 16.3 kB
'use strict'; /* eslint no-bitwise: 0 */ const utils = require('../utils'); const logger = require('../logger'); const MixAll = require('../mix_all'); const MQClient = require('../mq_client'); const SendStatus = require('./send_status'); const ClientConfig = require('../client_config'); // const PermName = require('../protocol/perm_name'); const MessageConst = require('../message/message_const'); const TopicPublishInfo = require('./topic_publish_info'); const ResponseCode = require('../protocol/response_code'); const MessageSysFlag = require('../utils/message_sys_flag'); const MessageDecoder = require('../message/message_decoder'); const defaultOptions = { logger, producerGroup: MixAll.DEFAULT_PRODUCER_GROUP, createTopicKey: MixAll.DEFAULT_TOPIC, defaultTopicQueueNums: 4, sendMsgTimeout: 3000, compressMsgBodyOverHowmuch: 1024 * 4, retryTimesWhenSendFailed: 3, retryAnotherBrokerWhenNotStoreOK: false, maxMessageSize: 1024 * 128, connectTimeout: 10000, }; class MQProducer extends ClientConfig { /** * metaq message producer * @param {Object} options - * @constructor */ constructor(options) { super(Object.assign({ initMethod: 'init' }, defaultOptions, options)); this._topicPublishInfoTable = new Map(); this._inited = false; if (this.producerGroup !== MixAll.CLIENT_INNER_PRODUCER_GROUP) { this.changeInstanceNameToPID(); } this._mqClient = MQClient.getAndCreateMQClient(this); this._mqClient.on('error', err => this._error(err)); } set localTransactionChecker(checker) { this._localTransactionChecker = checker; } get localTransactionChecker() { return this._localTransactionChecker; } get logger() { return this.options.logger; } get producerGroup() { return this.options.producerGroup; } get createTopicKey() { return this.options.createTopicKey; } get defaultTopicQueueNums() { return this.options.defaultTopicQueueNums; } /** * topic list * @property {Array} MQProducer#publishTopicList */ get publishTopicList() { return this._topicPublishInfoTable.keys(); } /** * start the producer * @return {void} */ async init() { // this._topicPublishInfoTable.set(this.createTopicKey, new TopicPublishInfo()); await MQProducer.getDefaultProducer(this.options); this._mqClient.registerProducer(this.producerGroup, this); await this._mqClient.ready(); this.logger.info('[mq:producer] producer started'); this._inited = true; } /** * close the producer */ async close() { this._topicPublishInfoTable.clear(); await this._mqClient.unregisterProducer(this.producerGroup); await this._mqClient.close(); this.removeAllListeners(); this._inited = false; this.logger.info('[mq:producer] producer close'); } /** * default error handler * @param {Error} err - error * @return {void} */ _error(err) { setImmediate(() => { err.message = 'MQPushConsumer occurred an error' + err.message; this.emit('error', err); }); } /** * update topic info * @param {String} topic - topic * @param {Object} info - route info * @return {void} */ updateTopicPublishInfo(topic, info) { if (topic && info) { const prev = this._topicPublishInfoTable.get(topic); this._topicPublishInfoTable.set(topic, info); if (prev) { info.sendWhichQueue = prev.sendWhichQueue; this.logger.warn('[mq:producer] updateTopicPublishInfo() prev is not null, %j', prev); } } } /** * is publish topic info need update * @param {String} topic - topic * @return {Boolean} need update? */ isPublishTopicNeedUpdate(topic) { const info = this._topicPublishInfoTable.get(topic); return !info || !info.ok; } /** * create topic * @param {String} key - TBW102 * @param {String} newTopic - * @param {Number} queueNum - queue number * @param {Number} topicSysFlag - 0 * @return {void} */ // * createTopic(key, newTopic, queueNum, topicSysFlag) { // await this.ready(); // topicSysFlag = topicSysFlag || 0; // const topicRouteData = await this._mqClient.getTopicRouteInfoFromNameServer(key, 1000 * 3); // const brokerDataList = topicRouteData.brokerDatas; // if (brokerDataList && brokerDataList.length) { // // 排序原因:即使没有配置顺序消息模式,默认队列的顺序同配置的一致。 // brokerDataList.sort(compare); // await brokerDataList.map(brokerData => { // const addr = brokerData.brokerAddrs[MixAll.MASTER_ID]; // if (addr) { // const topicConfig = { // topicName: newTopic, // readQueueNums: queueNum, // writeQueueNums: queueNum, // topicSysFlag, // perm: PermName.PERM_READ | PermName.PERM_WRITE, // topicFilterType: 'SINGLE_TAG', // order: false, // }; // this.logger.info('[mq:producer] execute createTopic at broker: %s, topic: %s', addr, newTopic); // return this._mqClient.createTopic(addr, key, topicConfig, 1000 * 3); // } // return null; // }); // } else { // throw new Error('Not found broker, maybe key is wrong'); // } // } /** * send message * @param {Message} msg - message object * @return {Object} sendResult */ async send(msg) { await this.ready(); const topicPublishInfo = await this.tryToFindTopicPublishInfo(msg.topic); if (!topicPublishInfo) { throw new Error(`no publish router data for topic: ${msg.topic}`); } const maxTimeout = this.options.sendMsgTimeout + 1000; const timesTotal = 1 + this.options.retryTimesWhenSendFailed; const beginTimestamp = Date.now(); const brokersSent = []; let times = 0; let lastBrokerName; let sendResult; for (; times < timesTotal && Date.now() - beginTimestamp < maxTimeout; times++) { const messageQueue = topicPublishInfo.selectOneMessageQueue(lastBrokerName); if (!messageQueue) { continue; } lastBrokerName = messageQueue.brokerName; brokersSent[times] = lastBrokerName; try { sendResult = await this.sendKernelImpl(msg, messageQueue); if (sendResult.sendStatus === SendStatus.SEND_OK || !this.options.retryAnotherBrokerWhenNotStoreOK) { break; } } catch (err) { if (err.name === 'MQClientException') { switch (err.code) { case ResponseCode.TOPIC_NOT_EXIST: case ResponseCode.SERVICE_NOT_AVAILABLE: case ResponseCode.SYSTEM_ERROR: case ResponseCode.NO_PERMISSION: case ResponseCode.NO_BUYER_ID: case ResponseCode.NOT_IN_CURRENT_UNIT: break; default: throw err; } } } } if (!sendResult) { throw new Error(`Send [${times}] times, still failed, cost [${Date.now() - beginTimestamp}]ms, Topic: ${msg.topic}, BrokersSent: ${JSON.stringify(brokersSent)}`); } return sendResult; } /** * try to find topic info from name server * @param {String} topic - topic * @return {Object} route info */ async tryToFindTopicPublishInfo(topic) { let topicPublishInfo = this._topicPublishInfoTable.get(topic); if (!topicPublishInfo || !topicPublishInfo.ok) { this._topicPublishInfoTable.set(topic, new TopicPublishInfo()); await this._mqClient.updateTopicRouteInfoFromNameServer(topic); topicPublishInfo = this._topicPublishInfoTable.get(topic); } if (topicPublishInfo.haveTopicRouterInfo || topicPublishInfo && topicPublishInfo.ok) { return topicPublishInfo; } await this._mqClient.updateTopicRouteInfoFromNameServer(topic, true, this); return this._topicPublishInfoTable.get(topic); } async sendKernelImpl(msg, messageQueue) { let brokerAddr = this._mqClient.findBrokerAddressInPublish(messageQueue.brokerName); if (!brokerAddr) { await this.tryToFindTopicPublishInfo(messageQueue.topic); brokerAddr = this._mqClient.findBrokerAddressInPublish(messageQueue.brokerName); if (!brokerAddr) { const err = new Error(`The broker[${messageQueue.brokerName}] not exist`); err.name = 'MQClientException'; throw err; } } let sysFlag = 0; if (this.tryToCompressMessage(msg)) { sysFlag |= MessageSysFlag.CompressedFlag; } const tranMsg = msg.properties[MessageConst.PROPERTY_TRANSACTION_PREPARED]; if (tranMsg) { sysFlag |= MessageSysFlag.TransactionPreparedType; } const requestHeader = { producerGroup: this.producerGroup, topic: msg.topic, defaultTopic: this.createTopicKey, defaultTopicQueueNums: this.defaultTopicQueueNums, queueId: messageQueue.queueId, sysFlag, bornTimestamp: Date.now(), flag: msg.flag, properties: MessageDecoder.messageProperties2String(msg.properties), reconsumeTimes: 0, unitMode: this.unitMode, }; if (requestHeader.topic.indexOf(MixAll.RETRY_GROUP_TOPIC_PREFIX) === 0) { const reconsumeTimes = msg.properties[MessageConst.PROPERTY_RECONSUME_TIME]; if (reconsumeTimes) { requestHeader.reconsumeTimes = parseInt(reconsumeTimes, 10); delete msg.properties[MessageConst.PROPERTY_RECONSUME_TIME]; } const maxReconsumeTimes = msg.properties[MessageConst.PROPERTY_MAX_RECONSUME_TIMES]; if (maxReconsumeTimes) { requestHeader.maxReconsumeTimes = parseInt(maxReconsumeTimes, 10); delete msg.properties[MessageConst.PROPERTY_MAX_RECONSUME_TIMES]; } } return await this._mqClient.sendMessage(brokerAddr, messageQueue.brokerName, msg, requestHeader, this.options.sendMsgTimeout); } async sendMessageInTransaction(msg, tranExecuter, arg) { if (!this.localTransactionChecker) { throw new Error('[MQClientException].producer.localTransactionCheckListener is required'); } if (tranExecuter === null) { throw new Error('[MQClientException] tranExecutor is required'); } let sendResult = null; msg.properties[MessageConst.PROPERTY_TRANSACTION_PREPARED] = 'true'; msg.properties[MessageConst.PROPERTY_PRODUCER_GROUP] = this.producerGroup; try { sendResult = await this.send(msg); } catch (e) { throw new Error('[MQClientException] send message Exception ', e); } let localTransactionState = 'UNKNOW'; let localException = null; switch (sendResult.sendStatus) { case 'SEND_OK': if (sendResult.transactionId !== null && msg.properties['__transactionId__'] === null) { // eslint-disable-line msg.properties['__transactionId__'] = sendResult.transactionId; // eslint-disable-line } try { localTransactionState = await tranExecuter(msg, arg); } catch (e) { this.logger.info('executeLocalTransactionBranch exception', e); this.logger.info(msg); localException = e; } if (localTransactionState === null) { localTransactionState = 'UNKNOW'; } if (localTransactionState !== 'COMMIT_MESSAGE') { this.logger.info('executeLocalTransactionBranch return', localTransactionState); this.logger.info(msg); } break; case 'FLUSH_DISK_TIMEOUT': case 'FLUSH_SLAVE_TIMEOUT': case 'SLAVE_NOT_AVAILABLE': localTransactionState = 'ROLLBACK_MESSAGE'; break; default: break; } try { await this.endTransaction(sendResult, localTransactionState, localException); } catch (err) { this.logger.warn('local transaction execute ' + localTransactionState + ', but end broker transaction failed', err); } return Object.assign({}, sendResult, { localTransactionState, }); } async endTransaction(sendResult, localTransactionState, localException) { let id; if (sendResult.offsetMsgId != null) { id = MessageDecoder.decodeMessageId(sendResult.offsetMsgId); } else { id = MessageDecoder.decodeMessageId(sendResult.msgId); } const transactionId = sendResult.transactionId; const brokerAddr = this._mqClient.findBrokerAddressInPublish(sendResult.messageQueue.brokerName); const requestHeader = { transactionId, commitLogOffset: id.offset, }; switch (localTransactionState) { case 'COMMIT_MESSAGE': requestHeader.commitOrRollback = MessageSysFlag.TransactionCommitType; break; case 'ROLLBACK_MESSAGE': requestHeader.commitOrRollback = MessageSysFlag.TransactionRollbackType; break; case 'UNKNOW': requestHeader.commitOrRollback = MessageSysFlag.TransactionNotType; break; default: break; } requestHeader.producerGroup = this.producerGroup; requestHeader.tranStateTableOffset = sendResult.queueOffset; requestHeader.msgId = sendResult.msgId; const remark = localException != null ? ('executeLocalTransactionBranch exception: ' + localException.toString()) : null; await this._mqClient.endTransactionOneway(brokerAddr, requestHeader, remark); } async checkTransactionState(addr, msg, checkRequestHeader) { let localTransactionState; let exception; try { localTransactionState = await this.localTransactionChecker(msg); } catch (error) { this.logger.error('Broker call checkTransactionState, but checkLocalTransactionState exception', error); exception = error; } if (!localTransactionState) { localTransactionState = 'UNKNOW'; } const thisHeader = { commitLogOffset: checkRequestHeader.commitLogOffset, producerGroup: checkRequestHeader.producerGroup, tranStateTableOffset: checkRequestHeader.tranStateTableOffset, fromTransactionCheck: true, msgId: msg.properties[MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX] || msg.msgId, transactionId: checkRequestHeader.transactionId, }; switch (localTransactionState) { case 'COMMIT_MESSAGE': thisHeader.commitOrRollback = MessageSysFlag.TransactionCommitType; break; case 'ROLLBACK_MESSAGE': thisHeader.commitOrRollback = MessageSysFlag.TransactionRollbackType; this.logger.warn('when broker check, client rollback this transaction, {}', thisHeader); break; case 'UNKNOW': thisHeader.commitOrRollback = MessageSysFlag.TransactionNotType; this.logger.warn('when broker check, client does not know this transaction state, {}', thisHeader); break; default: break; } const remark = exception != null ? ('checkLocalTransactionState exception: ' + exception) : null; await this._mqClient.endTransactionOneway(addr, thisHeader, remark); } /** * 尝试压缩消息 * @param {Message} msg - 消息对象 * @return {Boolean} 是否压缩 */ tryToCompressMessage(msg) { const body = msg.body; if (body) { if (body.length >= this.options.compressMsgBodyOverHowmuch) { try { const data = utils.compress(body); if (data) { msg.body = data; return true; } } catch (err) { err.name = 'MetaQCompressError'; this._error(err); } } } return false; } /** * 获取默认 Producer 实例 * @param {Object} options 配置 * @return {MQProducer} 默认 Producer 实例 */ static async getDefaultProducer(options = {}) { if (!MQProducer.defaultProducer) { const opts = Object.assign({}, options); opts.producerGroup = MixAll.CLIENT_INNER_PRODUCER_GROUP; MQProducer.defaultProducer = new MQProducer(opts); await MQProducer.defaultProducer.ready(); return MQProducer.defaultProducer; } return MQProducer.defaultProducer; } } module.exports = MQProducer; // // Helper // // --------------- // function compare(routerA, routerB) { // if (routerA.brokerName > routerB.brokerName) { // return 1; // } else if (routerA.brokerName < routerB.brokerName) { // return -1; // } // return 0; // }