mq-http-sdk
Version:
Aliyun Message Queue(MQ) Nodejs Http SDK
315 lines (284 loc) • 9.45 kB
JavaScript
const assert = require('assert')
const debug = require('debug')('mq:client')
const httpx = require('httpx')
const { hash, base64, hmac } = require('pure-func/crypto')
const logger = require('./logger')
const MQConsumer = require('./MQConsumer')
const MQProducer = require('./MQProducer')
const MQTransProducer = require('./MQTransProducer')
const MessageProperties = require('./MessageProperties')
const sleep = timeout => new Promise(resolve => setTimeout(resolve, timeout))
require('dnscache')({
enable: true,
ttl: 300,
cachesize: 1000
})
const {
parseXML,
extract,
getCanonicalizedMQHeaders
} = require('./helper')
/**
* MQ的client,用于保存aliyun账号消息,以及发送http请求
*/
class MQClient {
/**
* MQClient构造函数
* @param {string} endpoint MQ的HTTP接入地址
* @param {string} accessKeyId 阿里云账号的AK
* @param {string} accessKeySecret 阿里云账号的SK
* @param {string} securityToken 阿里云RAM授权的STS TOKEN,可空
*
* @returns {MQClient}
*/
constructor (endpoint, accessKeyId, accessKeySecret, securityToken, options = {}) {
assert(endpoint, '"endpoint" must be passed in')
this.endpoint = endpoint
assert(accessKeyId, 'must pass in "accessKeyId"')
this.accessKeyId = accessKeyId
assert(accessKeySecret, 'must pass in "accessKeySecret"')
this.accessKeySecret = accessKeySecret
// security token
this.securityToken = securityToken
this.logger = options.logger || logger
this.options = {
pullInterval: 3000, // 长轮询时间,不填则为短轮询,取值范围:1~30s,单位:毫秒。
pullBatchSize: 16, // 一次最多拉取多少条消息,取值范围:1~16。
pullTimeDelayMillsWhenFlowControl: 3000, // 进入流控逻辑,延迟一段时间再拉
pullThresholdForQueue: 100, // 本地队列消息数超过此阀值,开始流控
retryOnTimeout: true, // 注意:如果设置为true消费消息时需要去重
...options
}
}
/**
* 发送http请求
* @param {string} method HTTP的请求方法GET/PUT/POST/DELETE...
* @param {string} resource HTTP请求URL的path
* @param {string} type 解析XML响应内容的元素名字
* @param {string} requestBody 请求的body
* @param {object} opts 额外请求的参数
*
* @returns {object}
* ```json
* {
* code: 200,
* requestId: "xxxxxxxxxxxxxx",
* body: {A:1,B:2,C:3}
* }
* ```json
*/
async request (method, resource, type, requestBody, opts = {}, retryCount = 3) {
const url = `${this.endpoint}${resource}`
debug('url: %s', url)
debug('method: %s', method)
const headers = this.buildHeaders(method, requestBody, resource, opts.headers)
debug('request headers: %j', headers)
debug('request body: %s', requestBody.toString())
const requestOpts = {
timeout: method === 'POST' ? 10000 : 6000,
...opts,
method,
headers,
data: requestBody
}
const response = await httpx.request(url, requestOpts).catch(err => {
if (this.options.retryOnTimeout && (err.message.startsWith('ReadTimeout') || err.message.includes('ECONNRESET'))) {
return httpx.request(url, requestOpts)
}
this.logger.error('MQ_REQUEST_FAILED', 'NETWORK', err, err.code)
if (err.code === 'ECONNRESET') {
return sleep(100).then(() => {
return httpx.request(url, requestOpts)
})
}
return Promise.reject(err)
})
debug('statusCode %s', response.statusCode)
debug('response headers: %j', response.headers)
const code = response.statusCode
const contentType = response.headers['content-type'] || ''
const responseBody = await httpx.read(response, 'utf8')
debug('response body: %s', responseBody)
let body
if (responseBody && (contentType.startsWith('text/xml') || contentType.startsWith('application/xml'))) {
const responseData = await parseXML(responseBody)
if (responseData.Error) {
const e = responseData.Error
const message = extract(e.Message)
const requestid = extract(e.RequestId)
// const hostid = extract(e.HostId);
const errorcode = extract(e.Code)
const err = new Error(`${method} ${url} failed with ${code}. `
+ `RequestId: ${requestid}, ErrorCode: ${errorcode}, ErrorMsg: ${message}`)
err.Code = errorcode
err.RequestId = requestid
if (errorcode !== 'MessageNotExist' || method !== 'GET') {
this.logger.error('MQ_REQUEST_FAILED', 'ONS', err)
}
// ONS 会出现网络抖动的情况,需要重试, 业务层需要自己维护消息id
if ((errorcode === 'InternalError' || errorcode === 'ECONNRESET') && retryCount > 0) {
await sleep(100)
return this.request(method, resource, type, requestBody, opts, retryCount - 1)
}
throw err
}
body = {}
Object.keys(responseData[type]).forEach(key => {
if (key !== '$') {
body[key] = extract(responseData[type][key])
}
})
}
return {
code,
requestId: response.headers['x-mq-request-id'],
body
}
}
/**
* 发送HTTP GET请求
*
* @param {string} resource HTTP请求URL的path
* @param {string} type 解析XML响应内容的元素名字
* @param {object} opts 额外请求的参数
*
* @returns {object}
* ```json
* {
* code: 200,
* requestId: "xxxxxxxxxxxxxx",
* body: {A:1,B:2,C:3}
* }
* ```
*/
get (resource, type, opts) {
return this.request('GET', resource, type, '', opts)
}
/**
* 发送HTTP POST请求
*
* @param {string} resource HTTP请求URL的path
* @param {string} type 解析XML响应内容的元素名字
* @param {string} requestBody 请求的body
* @returns {object}
* ```json
* {
* code: 200,
* requestId: "xxxxxxxxxxxxxx",
* body: {A:1,B:2,C:3}
* }
* ```
*/
post (resource, type, body, opts) {
return this.request('POST', resource, type, body, opts)
}
/**
* 发送HTTP DELETE请求
*
* @param {string} resource HTTP请求URL的path
* @param {string} type 解析XML响应内容的元素名字
* @param {string} requestBody 请求的body
* @returns {object}
* ```json
* {
* code: 200,
* requestId: "xxxxxxxxxxxxxx",
* body: {A:1,B:2,C:3}
* }
* ```
*/
delete (resource, type, body) {
return this.request('DELETE', resource, type, body)
}
/**
* 对请求的内容按照MQ的HTTP协议签名,sha1+base64
* @param {string} method 请求方法
* @param {object} headers 请求头
* @param {string} resource HTTP请求URL的path
*
* @returns {string} 签名
*/
sign (method, headers, resource) {
const canonicalizedMQHeaders = getCanonicalizedMQHeaders(headers)
const md5 = headers['content-md5'] || ''
const { date } = headers
const type = headers['content-type'] || ''
const toSignString = `${method}\n${md5}\n${type}\n${date}\n${canonicalizedMQHeaders}${resource}`
const buff = Buffer.from(toSignString, 'utf8')
return base64(
hmac(this.accessKeySecret, buff, 'sha1', 'binary'),
'binary'
)
}
/**
* 组装请求MQ需要的请求头
* @param {string} method 请求方法
* @param {string} body 请求内容
* @param {string} resource HTTP请求URL的path
*
* @returns {object} headers
*/
buildHeaders (method, body, resource) {
const date = new Date().toGMTString()
const headers = {
date,
'x-mq-version': '2015-06-06',
'content-type': 'text/xml;charset=utf-8',
'user-agent': 'mq-nodejs-sdk/1.0.0'
}
if (method !== 'GET' && method !== 'HEAD') {
Object.assign(headers, {
'content-length': body.length,
'content-md5': base64(hash(body))
})
}
const signature = this.sign(method, headers, resource)
headers.authorization = `MQ ${this.accessKeyId}:${signature}`
if (this.securityToken) {
headers['security-token'] = this.securityToken
}
return headers
}
/**
* 构造一个MQ的消费者
* @param {string} instanceId 实例ID
* @param {string} topic 主题名字
* @param {string} consumer 消费者名字
* @param {string} messageTag 消费的过滤标签,可空
*
* @returns {MQConsumer}
*/
getConsumer (instanceId, topic, consumer, messageTag) {
// eslint-disable-next-line no-use-before-define
return new MQConsumer(this, instanceId, topic, consumer, messageTag)
}
/**
* 构造一个MQ的生产者
* @param {string} instanceId 实例ID
* @param {string} topic 主题名字
*
* @returns {MQProducer}
*/
getProducer (instanceId, topic) {
// eslint-disable-next-line no-use-before-define
return new MQProducer(this, instanceId, topic)
}
/**
* 构造一个MQ的事务消息生产者
* @param {string} instanceId 实例ID
* @param {string} topic 主题名字
* @param {string} groupId 客户端GroupID
*
* @returns {MQTransProducer}
*/
getTransProducer (instanceId, topic, groupId) {
// eslint-disable-next-line no-use-before-define
return new MQTransProducer(this, instanceId, topic, groupId)
}
}
module.exports = {
MQClient,
MQConsumer,
MQProducer,
MessageProperties
}