const Base = require('sdk-base'); const util = require('util'); const ready = require('get-ready'); const copy = require('copy-to'); const currentIP = require('address').ip(); const RR = 'roundRobin'; const MS = 'masterSlave'; module.exports = function (OssClient) { function Client(options) { if (!(this instanceof Client)) { return new Client(options); } if (!options || !Array.isArray(options.cluster)) { throw new Error('require options.cluster to be an array'); } Base.call(this); this.clients = []; this.availables = {}; for (let i = 0; i < options.cluster.length; i++) { const opt = options.cluster[i]; copy(options).pick('timeout', 'agent', 'urllib').to(opt); this.clients.push(new OssClient(opt)); this.availables[i] = true; } this.schedule = options.schedule || RR; // only read from master, default is false this.masterOnly = !!options.masterOnly; this.index = 0; const heartbeatInterval = options.heartbeatInterval || 10000; this._checkAvailableLock = false; this._timerId = this._deferInterval(this._checkAvailable.bind(this, true), heartbeatInterval); this._ignoreStatusFile = options.ignoreStatusFile || false; this._init(); } util.inherits(Client, Base); const proto = Client.prototype; ready.mixin(proto); const GET_METHODS = ['head', 'get', 'getStream', 'list', 'getACL']; const PUT_METHODS = ['put', 'putStream', 'delete', 'deleteMulti', 'copy', 'putMeta', 'putACL']; GET_METHODS.forEach(method => { proto[method] = async function (...args) { const client = this.chooseAvailable(); let lastError; try { return await client[method](...args); } catch (err) { if (err.status && err.status >= 200 && err.status < 500) { // 200 ~ 499 belong to normal response, don't try again throw err; } // < 200 || >= 500 need to retry from other cluser node lastError = err; } for (let i = 0; i < this.clients.length; i++) { const c = this.clients[i]; if (c !== client) { try { return await c[method].apply(client, args); } catch (err) { if (err.status && err.status >= 200 && err.status < 500) { // 200 ~ 499 belong to normal response, don't try again throw err; } // < 200 || >= 500 need to retry from other cluser node lastError = err; } } } lastError.message += ' (all clients are down)'; throw lastError; }; }); // must cluster node write success PUT_METHODS.forEach(method => { proto[method] = async function (...args) { const res = await Promise.all(this.clients.map(client => client[method](...args))); return res[0]; }; }); proto.signatureUrl = function signatureUrl(/* name */ ...args) { const client = this.chooseAvailable(); return client.signatureUrl(...args); }; proto.getObjectUrl = function getObjectUrl(/* name, baseUrl */ ...args) { const client = this.chooseAvailable(); return client.getObjectUrl(...args); }; proto._init = function _init() { const that = this; (async () => { await that._checkAvailable(that._ignoreStatusFile); that.ready(true); })().catch(err => { that.emit('error', err); }); }; proto._checkAvailable = async function _checkAvailable(ignoreStatusFile) { const name = `._ali-oss/check.status.${currentIP}.txt`; if (!ignoreStatusFile) { // only start will try to write the file await this.put(name, Buffer.from(`check available started at ${Date()}`)); } if (this._checkAvailableLock) { return; } this._checkAvailableLock = true; const downStatusFiles = []; for (let i = 0; i < this.clients.length; i++) { const client = this.clients[i]; // check 3 times let available = await this._checkStatus(client, name); if (!available) { // check again available = await this._checkStatus(client, name); } if (!available) { // check again /* eslint no-await-in-loop: [0] */ available = await this._checkStatus(client, name); if (!available) { downStatusFiles.push(client._objectUrl(name)); } } this.availables[i] = available; } this._checkAvailableLock = false; if (downStatusFiles.length > 0) { const err = new Error( `${downStatusFiles.length} data node down, please check status file: ${downStatusFiles.join(', ')}` ); err.name = 'CheckAvailableError'; this.emit('error', err); } }; proto._checkStatus = async function _checkStatus(client, name) { let available = true; try { await client.head(name); } catch (err) { // 404 will be available too if (!err.status || err.status >= 500 || err.status < 200) { available = false; } } return available; }; proto.chooseAvailable = function chooseAvailable() { if (this.schedule === MS) { // only read from master if (this.masterOnly) { return this.clients[0]; } for (let i = 0; i < this.clients.length; i++) { if (this.availables[i]) { return this.clients[i]; } } // all down, try to use this first one return this.clients[0]; } // RR let n = this.clients.length; while (n > 0) { const i = this._nextRRIndex(); if (this.availables[i]) { return this.clients[i]; } n--; } // all down, try to use this first one return this.clients[0]; }; proto._nextRRIndex = function _nextRRIndex() { const index = this.index++; if (this.index >= this.clients.length) { this.index = 0; } return index; }; proto._error = function error(err) { if (err) throw err; }; proto._createCallback = function _createCallback(ctx, gen, cb) { return () => { cb = cb || this._error; gen.call(ctx).then(() => { cb(); }, cb); }; }; proto._deferInterval = function _deferInterval(gen, timeout, cb) { return setInterval(this._createCallback(this, gen, cb), timeout); }; proto.close = function close() { clearInterval(this._timerId); this._timerId = null; }; return Client; };