租房小程序前端代码
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

228 lines
6.3 KiB

3 months ago
  1. const Base = require('sdk-base');
  2. const util = require('util');
  3. const ready = require('get-ready');
  4. const copy = require('copy-to');
  5. const currentIP = require('address').ip();
  6. const RR = 'roundRobin';
  7. const MS = 'masterSlave';
  8. module.exports = function (OssClient) {
  9. function Client(options) {
  10. if (!(this instanceof Client)) {
  11. return new Client(options);
  12. }
  13. if (!options || !Array.isArray(options.cluster)) {
  14. throw new Error('require options.cluster to be an array');
  15. }
  16. Base.call(this);
  17. this.clients = [];
  18. this.availables = {};
  19. for (let i = 0; i < options.cluster.length; i++) {
  20. const opt = options.cluster[i];
  21. copy(options).pick('timeout', 'agent', 'urllib').to(opt);
  22. this.clients.push(new OssClient(opt));
  23. this.availables[i] = true;
  24. }
  25. this.schedule = options.schedule || RR;
  26. // only read from master, default is false
  27. this.masterOnly = !!options.masterOnly;
  28. this.index = 0;
  29. const heartbeatInterval = options.heartbeatInterval || 10000;
  30. this._checkAvailableLock = false;
  31. this._timerId = this._deferInterval(this._checkAvailable.bind(this, true), heartbeatInterval);
  32. this._ignoreStatusFile = options.ignoreStatusFile || false;
  33. this._init();
  34. }
  35. util.inherits(Client, Base);
  36. const proto = Client.prototype;
  37. ready.mixin(proto);
  38. const GET_METHODS = ['head', 'get', 'getStream', 'list', 'getACL'];
  39. const PUT_METHODS = ['put', 'putStream', 'delete', 'deleteMulti', 'copy', 'putMeta', 'putACL'];
  40. GET_METHODS.forEach(method => {
  41. proto[method] = async function (...args) {
  42. const client = this.chooseAvailable();
  43. let lastError;
  44. try {
  45. return await client[method](...args);
  46. } catch (err) {
  47. if (err.status && err.status >= 200 && err.status < 500) {
  48. // 200 ~ 499 belong to normal response, don't try again
  49. throw err;
  50. }
  51. // < 200 || >= 500 need to retry from other cluser node
  52. lastError = err;
  53. }
  54. for (let i = 0; i < this.clients.length; i++) {
  55. const c = this.clients[i];
  56. if (c !== client) {
  57. try {
  58. return await c[method].apply(client, args);
  59. } catch (err) {
  60. if (err.status && err.status >= 200 && err.status < 500) {
  61. // 200 ~ 499 belong to normal response, don't try again
  62. throw err;
  63. }
  64. // < 200 || >= 500 need to retry from other cluser node
  65. lastError = err;
  66. }
  67. }
  68. }
  69. lastError.message += ' (all clients are down)';
  70. throw lastError;
  71. };
  72. });
  73. // must cluster node write success
  74. PUT_METHODS.forEach(method => {
  75. proto[method] = async function (...args) {
  76. const res = await Promise.all(this.clients.map(client => client[method](...args)));
  77. return res[0];
  78. };
  79. });
  80. proto.signatureUrl = function signatureUrl(/* name */ ...args) {
  81. const client = this.chooseAvailable();
  82. return client.signatureUrl(...args);
  83. };
  84. proto.getObjectUrl = function getObjectUrl(/* name, baseUrl */ ...args) {
  85. const client = this.chooseAvailable();
  86. return client.getObjectUrl(...args);
  87. };
  88. proto._init = function _init() {
  89. const that = this;
  90. (async () => {
  91. await that._checkAvailable(that._ignoreStatusFile);
  92. that.ready(true);
  93. })().catch(err => {
  94. that.emit('error', err);
  95. });
  96. };
  97. proto._checkAvailable = async function _checkAvailable(ignoreStatusFile) {
  98. const name = `._ali-oss/check.status.${currentIP}.txt`;
  99. if (!ignoreStatusFile) {
  100. // only start will try to write the file
  101. await this.put(name, Buffer.from(`check available started at ${Date()}`));
  102. }
  103. if (this._checkAvailableLock) {
  104. return;
  105. }
  106. this._checkAvailableLock = true;
  107. const downStatusFiles = [];
  108. for (let i = 0; i < this.clients.length; i++) {
  109. const client = this.clients[i];
  110. // check 3 times
  111. let available = await this._checkStatus(client, name);
  112. if (!available) {
  113. // check again
  114. available = await this._checkStatus(client, name);
  115. }
  116. if (!available) {
  117. // check again
  118. /* eslint no-await-in-loop: [0] */
  119. available = await this._checkStatus(client, name);
  120. if (!available) {
  121. downStatusFiles.push(client._objectUrl(name));
  122. }
  123. }
  124. this.availables[i] = available;
  125. }
  126. this._checkAvailableLock = false;
  127. if (downStatusFiles.length > 0) {
  128. const err = new Error(
  129. `${downStatusFiles.length} data node down, please check status file: ${downStatusFiles.join(', ')}`
  130. );
  131. err.name = 'CheckAvailableError';
  132. this.emit('error', err);
  133. }
  134. };
  135. proto._checkStatus = async function _checkStatus(client, name) {
  136. let available = true;
  137. try {
  138. await client.head(name);
  139. } catch (err) {
  140. // 404 will be available too
  141. if (!err.status || err.status >= 500 || err.status < 200) {
  142. available = false;
  143. }
  144. }
  145. return available;
  146. };
  147. proto.chooseAvailable = function chooseAvailable() {
  148. if (this.schedule === MS) {
  149. // only read from master
  150. if (this.masterOnly) {
  151. return this.clients[0];
  152. }
  153. for (let i = 0; i < this.clients.length; i++) {
  154. if (this.availables[i]) {
  155. return this.clients[i];
  156. }
  157. }
  158. // all down, try to use this first one
  159. return this.clients[0];
  160. }
  161. // RR
  162. let n = this.clients.length;
  163. while (n > 0) {
  164. const i = this._nextRRIndex();
  165. if (this.availables[i]) {
  166. return this.clients[i];
  167. }
  168. n--;
  169. }
  170. // all down, try to use this first one
  171. return this.clients[0];
  172. };
  173. proto._nextRRIndex = function _nextRRIndex() {
  174. const index = this.index++;
  175. if (this.index >= this.clients.length) {
  176. this.index = 0;
  177. }
  178. return index;
  179. };
  180. proto._error = function error(err) {
  181. if (err) throw err;
  182. };
  183. proto._createCallback = function _createCallback(ctx, gen, cb) {
  184. return () => {
  185. cb = cb || this._error;
  186. gen.call(ctx).then(() => {
  187. cb();
  188. }, cb);
  189. };
  190. };
  191. proto._deferInterval = function _deferInterval(gen, timeout, cb) {
  192. return setInterval(this._createCallback(this, gen, cb), timeout);
  193. };
  194. proto.close = function close() {
  195. clearInterval(this._timerId);
  196. this._timerId = null;
  197. };
  198. return Client;
  199. };