index.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. 'use strict';
  2. const {PassThrough} = require('stream');
  3. const extend = require('extend');
  4. let debug = () => {};
  5. if (
  6. typeof process !== 'undefined' &&
  7. 'env' in process &&
  8. typeof process.env === 'object' &&
  9. process.env.DEBUG === 'retry-request'
  10. ) {
  11. debug = message => {
  12. console.log('retry-request:', message);
  13. };
  14. }
  15. const DEFAULTS = {
  16. objectMode: false,
  17. retries: 2,
  18. /*
  19. The maximum time to delay in seconds. If retryDelayMultiplier results in a
  20. delay greater than maxRetryDelay, retries should delay by maxRetryDelay
  21. seconds instead.
  22. */
  23. maxRetryDelay: 64,
  24. /*
  25. The multiplier by which to increase the delay time between the completion of
  26. failed requests, and the initiation of the subsequent retrying request.
  27. */
  28. retryDelayMultiplier: 2,
  29. /*
  30. The length of time to keep retrying in seconds. The last sleep period will
  31. be shortened as necessary, so that the last retry runs at deadline (and not
  32. considerably beyond it). The total time starting from when the initial
  33. request is sent, after which an error will be returned, regardless of the
  34. retrying attempts made meanwhile.
  35. */
  36. totalTimeout: 600,
  37. noResponseRetries: 2,
  38. currentRetryAttempt: 0,
  39. shouldRetryFn: function (response) {
  40. const retryRanges = [
  41. // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
  42. // 1xx - Retry (Informational, request still processing)
  43. // 2xx - Do not retry (Success)
  44. // 3xx - Do not retry (Redirect)
  45. // 4xx - Do not retry (Client errors)
  46. // 429 - Retry ("Too Many Requests")
  47. // 5xx - Retry (Server errors)
  48. [100, 199],
  49. [429, 429],
  50. [500, 599],
  51. ];
  52. const statusCode = response.statusCode;
  53. debug(`Response status: ${statusCode}`);
  54. let range;
  55. while ((range = retryRanges.shift())) {
  56. if (statusCode >= range[0] && statusCode <= range[1]) {
  57. // Not a successful status or redirect.
  58. return true;
  59. }
  60. }
  61. },
  62. };
  63. function retryRequest(requestOpts, opts, callback) {
  64. if (typeof requestOpts === 'string') {
  65. requestOpts = {url: requestOpts};
  66. }
  67. const streamMode = typeof arguments[arguments.length - 1] !== 'function';
  68. if (typeof opts === 'function') {
  69. callback = opts;
  70. }
  71. const manualCurrentRetryAttemptWasSet =
  72. opts && typeof opts.currentRetryAttempt === 'number';
  73. opts = extend({}, DEFAULTS, opts);
  74. if (typeof opts.request === 'undefined') {
  75. throw new Error('A request library must be provided to retry-request.');
  76. }
  77. let currentRetryAttempt = opts.currentRetryAttempt;
  78. let numNoResponseAttempts = 0;
  79. let streamResponseHandled = false;
  80. let retryStream;
  81. let requestStream;
  82. let delayStream;
  83. let activeRequest;
  84. const retryRequest = {
  85. abort: function () {
  86. if (activeRequest && activeRequest.abort) {
  87. activeRequest.abort();
  88. }
  89. },
  90. };
  91. if (streamMode) {
  92. retryStream = new PassThrough({objectMode: opts.objectMode});
  93. retryStream.abort = resetStreams;
  94. }
  95. const timeOfFirstRequest = Date.now();
  96. if (currentRetryAttempt > 0) {
  97. retryAfterDelay(currentRetryAttempt);
  98. } else {
  99. makeRequest();
  100. }
  101. if (streamMode) {
  102. return retryStream;
  103. } else {
  104. return retryRequest;
  105. }
  106. function resetStreams() {
  107. delayStream = null;
  108. if (requestStream) {
  109. requestStream.abort && requestStream.abort();
  110. requestStream.cancel && requestStream.cancel();
  111. if (requestStream.destroy) {
  112. requestStream.destroy();
  113. } else if (requestStream.end) {
  114. requestStream.end();
  115. }
  116. }
  117. }
  118. function makeRequest() {
  119. let finishHandled = false;
  120. currentRetryAttempt++;
  121. debug(`Current retry attempt: ${currentRetryAttempt}`);
  122. function handleFinish(args = []) {
  123. if (!finishHandled) {
  124. finishHandled = true;
  125. retryStream.emit('complete', ...args);
  126. }
  127. }
  128. if (streamMode) {
  129. streamResponseHandled = false;
  130. delayStream = new PassThrough({objectMode: opts.objectMode});
  131. requestStream = opts.request(requestOpts);
  132. setImmediate(() => {
  133. retryStream.emit('request');
  134. });
  135. requestStream
  136. // gRPC via google-cloud-node can emit an `error` as well as a `response`
  137. // Whichever it emits, we run with-- we can't run with both. That's what
  138. // is up with the `streamResponseHandled` tracking.
  139. .on('error', err => {
  140. if (streamResponseHandled) {
  141. return;
  142. }
  143. streamResponseHandled = true;
  144. onResponse(err);
  145. })
  146. .on('response', (resp, body) => {
  147. if (streamResponseHandled) {
  148. return;
  149. }
  150. streamResponseHandled = true;
  151. onResponse(null, resp, body);
  152. })
  153. .on('complete', (...params) => handleFinish(params))
  154. .on('finish', (...params) => handleFinish(params));
  155. requestStream.pipe(delayStream);
  156. } else {
  157. activeRequest = opts.request(requestOpts, onResponse);
  158. }
  159. }
  160. function retryAfterDelay(currentRetryAttempt) {
  161. if (streamMode) {
  162. resetStreams();
  163. }
  164. const nextRetryDelay = getNextRetryDelay({
  165. maxRetryDelay: opts.maxRetryDelay,
  166. retryDelayMultiplier: opts.retryDelayMultiplier,
  167. retryNumber: currentRetryAttempt,
  168. timeOfFirstRequest,
  169. totalTimeout: opts.totalTimeout,
  170. });
  171. debug(`Next retry delay: ${nextRetryDelay}`);
  172. if (nextRetryDelay <= 0) {
  173. numNoResponseAttempts = opts.noResponseRetries + 1;
  174. return;
  175. }
  176. setTimeout(makeRequest, nextRetryDelay);
  177. }
  178. function onResponse(err, response, body) {
  179. // An error such as DNS resolution.
  180. if (err) {
  181. numNoResponseAttempts++;
  182. if (numNoResponseAttempts <= opts.noResponseRetries) {
  183. retryAfterDelay(numNoResponseAttempts);
  184. } else {
  185. if (streamMode) {
  186. retryStream.emit('error', err);
  187. retryStream.end();
  188. } else {
  189. callback(err, response, body);
  190. }
  191. }
  192. return;
  193. }
  194. // Send the response to see if we should try again.
  195. // NOTE: "currentRetryAttempt" isn't accurate by default, as it counts
  196. // the very first request sent as the first "retry". It is only accurate
  197. // when a user provides their own "currentRetryAttempt" option at
  198. // instantiation.
  199. const adjustedCurrentRetryAttempt = manualCurrentRetryAttemptWasSet
  200. ? currentRetryAttempt
  201. : currentRetryAttempt - 1;
  202. if (
  203. adjustedCurrentRetryAttempt < opts.retries &&
  204. opts.shouldRetryFn(response)
  205. ) {
  206. retryAfterDelay(currentRetryAttempt);
  207. return;
  208. }
  209. // No more attempts need to be made, just continue on.
  210. if (streamMode) {
  211. retryStream.emit('response', response);
  212. delayStream.pipe(retryStream);
  213. requestStream.on('error', err => {
  214. retryStream.destroy(err);
  215. });
  216. } else {
  217. callback(err, response, body);
  218. }
  219. }
  220. }
  221. module.exports = retryRequest;
  222. function getNextRetryDelay(config) {
  223. const {
  224. maxRetryDelay,
  225. retryDelayMultiplier,
  226. retryNumber,
  227. timeOfFirstRequest,
  228. totalTimeout,
  229. } = config;
  230. const maxRetryDelayMs = maxRetryDelay * 1000;
  231. const totalTimeoutMs = totalTimeout * 1000;
  232. const jitter = Math.floor(Math.random() * 1000);
  233. const calculatedNextRetryDelay =
  234. Math.pow(retryDelayMultiplier, retryNumber) * 1000 + jitter;
  235. const maxAllowableDelayMs =
  236. totalTimeoutMs - (Date.now() - timeOfFirstRequest);
  237. return Math.min(
  238. calculatedNextRetryDelay,
  239. maxAllowableDelayMs,
  240. maxRetryDelayMs
  241. );
  242. }
  243. module.exports.defaults = DEFAULTS;
  244. module.exports.getNextRetryDelay = getNextRetryDelay;