| 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; | |
| };
 |