UNPKG

acm-client

Version:

aliyun acm client

544 lines 17.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const const_1 = require("./const"); const utils_1 = require("./utils"); const Base = require('sdk-base'); const debug = require('debug')('diamond-client:diamond_env'); const co = require('co'); const path = require('path'); const is = require('is-type-of'); const gather = require('co-gather'); const { sleep } = require('mz-modules'); const crypto = require('crypto'); const DEFAULT_OPTIONS = { serverPort: 8080, refreshInterval: 30 * 1000, requestTimeout: 5000, unit: const_1.CURRENT_UNIT, ssl: false, }; class DiamondEnv extends Base { /** * Diamond Client. * * @param {Object} options * - {Number} [refreshInterval] data refresh interval time, default is 30000 ms * - {Number} [requestTimeout] diamond request timeout, default is 5000 ms * - {String} [unit] unit name * - {HttpClient} httpclient http request client * - {Snapshot} snapshot snapshot instance * @constructor */ constructor(options = { unit: const_1.CURRENT_UNIT }) { super(Object.assign({}, DEFAULT_OPTIONS, options)); this.uuid = Math.random(); this.isClose = false; this.isLongPulling = false; this.subscriptions = new Map(); this.currentServer = null; // 同一个key可能会被多次订阅,避免不必要的 `warning` this.setMaxListeners(100); this.ready(true); debug(this.uuid); } get appName() { return this.options.appName; } get appKey() { return this.options.appKey; } get secretKey() { return this.options.secretKey; } get snapshot() { return this.options.snapshot; } get serverMgr() { return this.options.serverMgr; } get unit() { return this.options.unit; } /** * HTTP 请求客户端 * @property {HttpClient} DiamondEnv#httpclient */ get httpclient() { return this.options.httpclient; } close() { this.isClose = true; this.removeAllListeners(); } /** * 更新 当前服务器 */ async updateCurrentServer() { this.currentServer = await this.serverMgr.getOne(this.unit); if (!this.currentServer) { const err = new Error('[DiamondEnv] Diamond server unavailable'); err.name = 'DiamondServerUnavailableError'; err.unit = this.unit; throw err; } } /** * 订阅 * @param {Object} info * - {String} dataId - id of the data you want to subscribe * - {String} [group] - group name of the data * @param {Function} listener - listener * @return {DiamondEnv} self */ subscribe(info, listener) { const { dataId, group } = info; const key = this.formatKey(info); this.on(key, listener); let item = this.subscriptions.get(key); if (!item) { item = { dataId, group, md5: null, content: null, }; this.subscriptions.set(key, item); (async () => { try { await this.syncConfigs([item]); this.startLongPulling(); } catch (err) { this._error(err); } })(); } else if (!is.nullOrUndefined(item.md5)) { process.nextTick(() => listener(item.content)); } return this; } /** * 同步配置 * @param {Array} list - 需要同步的配置列表 * @return {void} */ async syncConfigs(list) { const tasks = list.map(({ dataId, group }) => this.getConfig(dataId, group)); const results = await gather(tasks, 5); for (let i = 0, len = results.length; i < len; i++) { const key = this.formatKey(list[i]); const item = this.subscriptions.get(key); const result = results[i]; if (!item) { debug('item %s not exist', key); // maybe removed by user continue; } if (result.isError) { const err = new Error(`[DiamondEnv] getConfig failed for dataId: ${item.dataId}, group: ${item.group}, error: ${result.error}`); err.name = 'DiamondSyncConfigError'; err.dataId = item.dataId; err.group = item.group; this._error(err); continue; } const content = result.value; const md5 = utils_1.getMD5String(content); // 防止应用启动时,并发请求,导致同一个 key 重复触发 if (item.md5 !== md5) { item.md5 = md5; item.content = content; // 异步化,避免处理逻辑异常影响到 diamond 内部 setImmediate(() => this.emit(key, content)); } } } /** * 请求 * @param {String} path - 请求 path * @param {Object} [options] - 参数 * @return {String} value */ async request(path, options = {}) { // 默认为当前单元 const unit = this.unit; const ts = String(Date.now()); const { encode = false, method = 'GET', data, timeout = this.options.requestTimeout, headers = {} } = options; if (!this.currentServer) { await this.updateCurrentServer(); } let url = `http://${this.currentServer}:${8080}/diamond-server${path}`; if (this.options.ssl) { url = `https://${this.currentServer}:${443}/diamond-server${path}`; } debug('request unit: [%s] with url: %s', unit, url); let signStr = data.tenant; if (data.group && data.tenant) { signStr = data.tenant + '+' + data.group; } else if (data.group) { signStr = data.group; } const signature = crypto.createHmac('sha1', this.secretKey) .update(signStr + '+' + ts).digest() .toString('base64'); // 携带统一的头部信息 Object.assign(headers, { 'Client-Version': const_1.VERSION, 'Content-Type': 'application/x-www-form-urlencoded; charset=GBK', 'Spas-AccessKey': this.options.accessKey, timeStamp: ts, exConfigInfo: 'true', 'Spas-Signature': signature, }); let requestData = data; if (encode) { requestData = utils_1.encodingParams(data); } try { const res = await this.httpclient.request(url, { rejectUnauthorized: false, httpsAgent: false, method, data: requestData, dataType: 'text', headers, timeout, secureProtocol: 'TLSv1_2_method', }); let err; const resData = res.data; debug('%s %s, got %s, body: %j', method, url, res.status, resData); switch (res.status) { case const_1.HTTP_OK: return resData; case const_1.HTTP_NOT_FOUND: return null; case const_1.HTTP_CONFLICT: err = new Error(`[DiamondEnv] Diamond server config being modified concurrently, data: ${JSON.stringify(data)}`); err.name = 'DiamondServerConflictError'; throw err; default: err = new Error(`Diamond Server Error Status: ${res.status}, url: ${url}, request data: ${JSON.stringify(data)}, response data: ${resData && resData.toString()}`); err.name = 'DiamondServerResponseError'; err.body = res.data; throw err; } } catch (err) { err.url = `${method} ${url}`; err.data = data; err.headers = headers; await this.updateCurrentServer(); throw err; } } /** * 开启长轮询 * @return {void} * @private */ startLongPulling() { // 防止重入 if (this.isLongPulling) { return; } this.isLongPulling = true; co(async () => { while (!this.isClose && this.subscriptions.size > 0) { try { await this.checkServerConfigInfo(); } catch (err) { err.name = 'DiamondLongPullingError'; this._error(err); await sleep(2000); } } }).then(() => { this.isLongPulling = false; }).catch(err => { this.isLongPulling = false; this._error(err); }); } async checkServerConfigInfo() { debug('start to check update config list'); if (this.subscriptions.size === 0) { return; } const beginTime = Date.now(); const tenant = this.options.namespace; const probeUpdate = []; for (const { dataId, group, md5 } of this.subscriptions.values()) { probeUpdate.push(dataId, const_1.WORD_SEPARATOR); probeUpdate.push(group, const_1.WORD_SEPARATOR); if (tenant) { probeUpdate.push(md5, const_1.WORD_SEPARATOR); probeUpdate.push(tenant, const_1.LINE_SEPARATOR); } else { probeUpdate.push(md5, const_1.LINE_SEPARATOR); } } const content = await this.request('/config.co', { method: 'POST', data: { 'Probe-Modify-Request': probeUpdate.join(''), }, headers: { longPullingTimeout: '30000', }, timeout: 40000, }); debug('long pulling takes %ds', (Date.now() - beginTime) / 1000); const updateList = this.parseUpdateDataIdResponse(content); if (updateList && updateList.length) { await this.syncConfigs(updateList); } } // 解析 diamond 返回的 long pulling 结果 parseUpdateDataIdResponse(content) { const updateList = []; decodeURIComponent(content) .split(const_1.LINE_SEPARATOR) .forEach(dataIdAndGroup => { if (dataIdAndGroup) { const keyArr = dataIdAndGroup.split(const_1.WORD_SEPARATOR); if (keyArr.length >= 2) { const dataId = keyArr[0]; const group = keyArr[1]; updateList.push({ dataId, group, }); } } }); return updateList; } /** * 退订 * @param {Object} info * - {String} dataId - id of the data you want to subscribe * - {String} group - group name of the data * @param {Function} listener - listener * @return {DiamondEnv} self */ unSubscribe(info, listener) { const key = this.formatKey(info); if (listener) { this.removeListener(key, listener); } else { this.removeAllListeners(key); } // 没有人订阅了,从长轮询里拿掉 if (this.listeners(key).length === 0) { this.subscriptions.delete(key); } return this; } /** * 默认异常处理 * @param {Error} err - 异常 * @return {void} * @private */ _error(err) { if (err) { setImmediate(() => this.emit('error', err)); } } formatKey(info) { return `${info.dataId}@${info.group}@${this.unit}`; } getSnapshotKey(dataId, group, tenant) { tenant = tenant || this.options.namespace || 'default_tenant'; return path.join('config', this.unit, tenant, group, dataId); } /** * 获取配置 * @param {String} dataId - id of the data * @param {String} group - group name of the data * @return {String} value */ async getConfig(dataId, group) { debug('calling getConfig, dataId: %s, group: %s', dataId, group); let content; const key = this.getSnapshotKey(dataId, group); try { content = await this.request('/config.co', { data: { dataId, group, tenant: this.options.namespace, }, }); } catch (err) { const cache = await this.snapshot.get(key); if (cache) { this._error(err); return cache; } throw err; } await this.snapshot.save(key, content); return content; } /** * 查询租户下的所有的配置 * @return {Array} config */ async getConfigs() { const configInfoPage = await this.getAllConfigInfoByTenantInner(1, 1); const total = configInfoPage.totalCount; const pageSize = 200; let configs = []; for (let i = 0; i * pageSize < total; i++) { const configInfo = await this.getAllConfigInfoByTenantInner(i + 1, pageSize); configs = configs.concat(configInfo.pageItems); } return configs; } async getAllConfigInfoByTenantInner(pageNo, pageSize) { const ret = await this.request('/basestone.do', { data: { pageNo, pageSize, method: 'getAllConfigByTenant', tenant: this.options.namespace, }, }); return JSON.parse(ret); } /** * 发布配置 * @param {String} dataId - id of the data * @param {String} group - group name of the data * @param {String} content - config value * @return {Boolean} success */ async publishSingle(dataId, group, content) { await this.request('/basestone.do?method=syncUpdateAll', { method: 'POST', encode: true, data: { dataId, group, content, tenant: this.options.namespace, }, }); return true; } /** * 删除配置 * @param {String} dataId - id of the data * @param {String} group - group name of the data * @return {Boolean} success */ async remove(dataId, group) { await this.request('/datum.do?method=deleteAllDatums', { method: 'POST', data: { dataId, group, tenant: this.options.namespace, }, }); return true; } async publishAggr(dataId, group, datumId, content) { const appName = this.appName; await this.request('/datum.do?method=addDatum', { method: 'POST', data: { dataId, group, datumId, content, appName, tenant: this.options.namespace, }, }); return true; } async removeAggr(dataId, group, datumId) { await this.request('/datum.do?method=deleteDatum', { method: 'POST', data: { dataId, group, datumId, tenant: this.options.namespace, }, }); return true; } /** * 批量获取配置 * @param {Array} dataIds - data id array * @param {String} group - group name of the data * @return {Array} result */ async batchGetConfig(dataIds, group) { const dataIdStr = dataIds.join(const_1.WORD_SEPARATOR); const content = await this.request('/config.co?method=batchGetConfig', { method: 'POST', data: { dataIds: dataIdStr, group, tenant: this.options.namespace, }, }); try { /** * data 结构 * [{status: 1, group: "test-group", dataId: 'test-dataId3', content: 'test-content'}] */ const data = JSON.parse(content); const savedData = data.filter(d => d.status === 1).map(d => { const r = {}; r.key = path.join(this.getSnapshotKey(d.dataId, d.group)); r.value = d.content; return r; }); await this.snapshot.batchSave(savedData); return data; } catch (err) { err.name = 'DiamondBatchDeserializeError'; err.data = content; throw err; } } /** * 批量查询 * @param {Array} dataIds - data id array * @param {String} group - group name of the data * @return {Object} result */ async batchQuery(dataIds, group) { const dataIdStr = dataIds.join(const_1.WORD_SEPARATOR); const content = await this.request('/admin.do?method=batchQuery', { method: 'POST', data: { dataIds: dataIdStr, group, tenant: this.options.namespace, }, }); try { return JSON.parse(content); } catch (err) { err.name = 'DiamondBatchDeserializeError'; err.data = content; throw err; } } } exports.DiamondEnv = DiamondEnv; //# sourceMappingURL=diamond_env.js.map