yc-acm-client
Version:
aliyun acm client
237 lines (218 loc) • 6.82 kB
JavaScript
'use strict';
const debug = require('debug')('diamond-client');
const co = require('co');
const path = require('path');
const assert = require('assert');
const Base = require('sdk-base');
const gather = require('co-gather');
const Constants = require('./const');
const { random } = require('utility');
const Snapshot = require('./snapshot');
const { sleep } = require('mz-modules');
const CURRENT_UNIT = Symbol('CURRENT_UNIT');
const DEFAULT_OPTIONS = {
endpoint: 'acm.aliyun.com',
refreshInterval: 30 * 1000, // 30s
};
class ServerListManager extends Base {
/**
* 服务地址列表管理器
*
* @param {Object} options
* - {HttpClient} httpclient - http 客户端
* - {Snapshot} [snapshot] - 快照对象
* - {String} nameServerAddr - 命名服务器地址 `hostname:port`
* @constructor
*/
constructor(options = {}) {
assert(options.httpclient, '[diamond#ServerListManager] options.httpclient is required');
if (!options.snapshot) {
options.snapshot = new Snapshot(options);
}
if (options.endpoint) {
const temp = options.endpoint.split(':');
options.endpoint = temp[0] + ':' + (temp[1] || '8080');
}
super(Object.assign({}, DEFAULT_OPTIONS, options));
this._isSync = false;
this._isClosed = false;
this._serverListCache = new Map(); // unit => { hosts: [ addr1, addr2 ], index }
this._syncServers();
this.ready(true);
}
get snapshot() {
return this.options.snapshot;
}
get httpclient() {
return this.options.httpclient;
}
get nameServerAddr() {
// jmenv.tbsite.net:8080
if (this.options.endpoint) {
return this.options.endpoint;
}
return this.options.nameServerAddr;
}
get refreshInterval() {
return this.options.refreshInterval;
}
/**
* 关闭地址列表服务
*/
close() {
this._isClosed = true;
}
* _request(url, options) {
const res = yield this.httpclient.request(url, options);
const { status, data } = res;
if (status !== 200) {
const err = new Error(`[diamond#ServerListManager] request url: ${url} failed with statusCode: ${status}`);
err.name = 'DiamondServerResponseError';
err.url = url;
err.params = options;
err.body = res.data;
throw err;
}
return data;
}
/*
* 获取当前机器所在单元
*/
* getCurrentUnit() {
if (!this[CURRENT_UNIT]) {
const url = `http://${this.nameServerAddr}/env`;
const data = yield this._request(url, {
timeout: this.options.requestTimeout,
dataType: 'text',
});
const unit = data && data.trim();
this[CURRENT_UNIT] = unit;
}
return this[CURRENT_UNIT];
}
/**
* 获取某个单元的地址
* @param {String} unit 单元名,默认为当前单元
* @return {String} address
*/
* getOne(unit = Constants.CURRENT_UNIT) {
let serverData = this._serverListCache.get(unit);
// 不存在则先尝试去更新一次
if (!serverData) {
serverData = yield this._fetchServerList(unit);
}
// 如果还没有,则返回 null
if (!serverData || !serverData.hosts.length) {
return null;
}
const choosed = serverData.hosts[serverData.index];
serverData.index += 1;
if (serverData.index >= serverData.hosts.length) {
serverData.index = 0;
}
return choosed;
}
/**
* 同步服务器列表
* @return {void}
*/
_syncServers() {
if (this._isSync) {
return;
}
co(function* () {
this._isSync = true;
while (!this._isClosed) {
yield sleep(this.refreshInterval);
const units = Array.from(this._serverListCache.keys());
debug('syncServers for units: %j', units);
const results = yield gather(units.map(unit => this._fetchServerList(unit)));
for (let i = 0, len = results.length; i < len; i++) {
if (results[i].isError) {
const err = new Error(results[i].error);
err.name = 'DiamondUpdateServersError';
this.emit('error', err);
}
}
}
this._isSync = false;
}.bind(this)).catch(err => { this.emit('error', err); });
}
// 获取某个单元的 diamond server 列表
* _fetchServerList(unit = Constants.CURRENT_UNIT) {
const key = this._formatKey(unit);
const url = this._getRequestUrl(unit);
let hosts;
try {
let data = yield this._request(url, {
timeout: this.options.requestTimeout,
dataType: 'text',
});
data = data || '';
hosts = data.split('\n').map(host => host.trim()).filter(host => !!host);
const length = hosts.length;
debug('got %d hosts, the serverlist is: %j', length, hosts);
if (!length) {
const err = new Error('[diamond#ServerListManager] Diamond return empty hosts');
err.name = 'DiamondServerHostEmptyError';
err.unit = unit;
throw err;
}
yield this.snapshot.save(key, JSON.stringify(hosts));
} catch (err) {
this.emit('error', err);
const data = yield this.snapshot.get(key);
if (data) {
try {
hosts = JSON.parse(data);
} catch (err) {
yield this.snapshot.delete(key);
err.name = 'ServerListSnapShotJSONParseError';
err.unit = unit;
err.data = data;
this.emit('error', err);
}
}
}
if (!hosts || !hosts.length) {
// 这里主要是为了让后面定时同步可以执行
this._serverListCache.set(unit, null);
return null;
}
const serverData = {
hosts,
index: random(hosts.length),
};
this._serverListCache.set(unit, serverData);
return serverData;
}
_formatKey(unit) {
return path.join('server_list', unit);
}
// 获取请求 url
_getRequestUrl(unit) {
return unit === Constants.CURRENT_UNIT ?
`http://${this.nameServerAddr}/diamond-server/diamond` :
`http://${this.nameServerAddr}/diamond-server/diamond-unit-${unit}?nofix=1`;
}
/**
* 获取单元列表
* @return {Array} units
*/
* fetchUnitLists() {
const url = `http://${this.nameServerAddr}/diamond-server/unit-list?nofix=1`;
let data = yield this._request(url, {
timeout: this.options.requestTimeout,
dataType: 'text',
});
data = data || '';
const units = data.split('\n').map(unit => unit.trim()).filter(unit => unit);
// 这个逻辑和 @彦林 确认去掉,java 下个版本也将去掉
// http://gitlab.alibaba-inc.com/middleware/diamond/blob/master/diamond-client/src/main/java/com/taobao/diamond/client/impl/DiamondUnitSite.java#L126
// if (units.indexOf('center') === -1) {
// units.push('center');
// }
return units;
}
}
module.exports = ServerListManager;