cluster.js 6.0 KB

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