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.
		
		
		
		
		
			
		
			
				
					274 lines
				
				7.1 KiB
			
		
		
			
		
	
	
					274 lines
				
				7.1 KiB
			| 
								 
											2 years ago
										 
									 | 
							
								'use strict';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const {PassThrough} = require('stream');
							 | 
						||
| 
								 | 
							
								const debug = require('debug')('retry-request');
							 | 
						||
| 
								 | 
							
								const extend = require('extend');
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const DEFAULTS = {
							 | 
						||
| 
								 | 
							
								  objectMode: false,
							 | 
						||
| 
								 | 
							
								  retries: 2,
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /*
							 | 
						||
| 
								 | 
							
								    The maximum time to delay in seconds. If retryDelayMultiplier results in a
							 | 
						||
| 
								 | 
							
								    delay greater than maxRetryDelay, retries should delay by maxRetryDelay
							 | 
						||
| 
								 | 
							
								    seconds instead.
							 | 
						||
| 
								 | 
							
								  */
							 | 
						||
| 
								 | 
							
								  maxRetryDelay: 64,
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /*
							 | 
						||
| 
								 | 
							
								    The multiplier by which to increase the delay time between the completion of
							 | 
						||
| 
								 | 
							
								    failed requests, and the initiation of the subsequent retrying request.
							 | 
						||
| 
								 | 
							
								  */
							 | 
						||
| 
								 | 
							
								  retryDelayMultiplier: 2,
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /*
							 | 
						||
| 
								 | 
							
								    The length of time to keep retrying in seconds. The last sleep period will
							 | 
						||
| 
								 | 
							
								    be shortened as necessary, so that the last retry runs at deadline (and not
							 | 
						||
| 
								 | 
							
								    considerably beyond it).  The total time starting from when the initial
							 | 
						||
| 
								 | 
							
								    request is sent, after which an error will be returned, regardless of the
							 | 
						||
| 
								 | 
							
								    retrying attempts made meanwhile.
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  totalTimeout: 600,
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  noResponseRetries: 2,
							 | 
						||
| 
								 | 
							
								  currentRetryAttempt: 0,
							 | 
						||
| 
								 | 
							
								  shouldRetryFn: function (response) {
							 | 
						||
| 
								 | 
							
								    const retryRanges = [
							 | 
						||
| 
								 | 
							
								      // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
							 | 
						||
| 
								 | 
							
								      // 1xx - Retry (Informational, request still processing)
							 | 
						||
| 
								 | 
							
								      // 2xx - Do not retry (Success)
							 | 
						||
| 
								 | 
							
								      // 3xx - Do not retry (Redirect)
							 | 
						||
| 
								 | 
							
								      // 4xx - Do not retry (Client errors)
							 | 
						||
| 
								 | 
							
								      // 429 - Retry ("Too Many Requests")
							 | 
						||
| 
								 | 
							
								      // 5xx - Retry (Server errors)
							 | 
						||
| 
								 | 
							
								      [100, 199],
							 | 
						||
| 
								 | 
							
								      [429, 429],
							 | 
						||
| 
								 | 
							
								      [500, 599],
							 | 
						||
| 
								 | 
							
								    ];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const statusCode = response.statusCode;
							 | 
						||
| 
								 | 
							
								    debug(`Response status: ${statusCode}`);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    let range;
							 | 
						||
| 
								 | 
							
								    while ((range = retryRanges.shift())) {
							 | 
						||
| 
								 | 
							
								      if (statusCode >= range[0] && statusCode <= range[1]) {
							 | 
						||
| 
								 | 
							
								        // Not a successful status or redirect.
							 | 
						||
| 
								 | 
							
								        return true;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  },
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function retryRequest(requestOpts, opts, callback) {
							 | 
						||
| 
								 | 
							
								  const streamMode = typeof arguments[arguments.length - 1] !== 'function';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (typeof opts === 'function') {
							 | 
						||
| 
								 | 
							
								    callback = opts;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const manualCurrentRetryAttemptWasSet =
							 | 
						||
| 
								 | 
							
								    opts && typeof opts.currentRetryAttempt === 'number';
							 | 
						||
| 
								 | 
							
								  opts = extend({}, DEFAULTS, opts);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (typeof opts.request === 'undefined') {
							 | 
						||
| 
								 | 
							
								    try {
							 | 
						||
| 
								 | 
							
								      // eslint-disable-next-line node/no-unpublished-require
							 | 
						||
| 
								 | 
							
								      opts.request = require('request');
							 | 
						||
| 
								 | 
							
								    } catch (e) {
							 | 
						||
| 
								 | 
							
								      throw new Error('A request library must be provided to retry-request.');
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let currentRetryAttempt = opts.currentRetryAttempt;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let numNoResponseAttempts = 0;
							 | 
						||
| 
								 | 
							
								  let streamResponseHandled = false;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let retryStream;
							 | 
						||
| 
								 | 
							
								  let requestStream;
							 | 
						||
| 
								 | 
							
								  let delayStream;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let activeRequest;
							 | 
						||
| 
								 | 
							
								  const retryRequest = {
							 | 
						||
| 
								 | 
							
								    abort: function () {
							 | 
						||
| 
								 | 
							
								      if (activeRequest && activeRequest.abort) {
							 | 
						||
| 
								 | 
							
								        activeRequest.abort();
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    },
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (streamMode) {
							 | 
						||
| 
								 | 
							
								    retryStream = new PassThrough({objectMode: opts.objectMode});
							 | 
						||
| 
								 | 
							
								    retryStream.abort = resetStreams;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const timeOfFirstRequest = Date.now();
							 | 
						||
| 
								 | 
							
								  if (currentRetryAttempt > 0) {
							 | 
						||
| 
								 | 
							
								    retryAfterDelay(currentRetryAttempt);
							 | 
						||
| 
								 | 
							
								  } else {
							 | 
						||
| 
								 | 
							
								    makeRequest();
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (streamMode) {
							 | 
						||
| 
								 | 
							
								    return retryStream;
							 | 
						||
| 
								 | 
							
								  } else {
							 | 
						||
| 
								 | 
							
								    return retryRequest;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  function resetStreams() {
							 | 
						||
| 
								 | 
							
								    delayStream = null;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (requestStream) {
							 | 
						||
| 
								 | 
							
								      requestStream.abort && requestStream.abort();
							 | 
						||
| 
								 | 
							
								      requestStream.cancel && requestStream.cancel();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (requestStream.destroy) {
							 | 
						||
| 
								 | 
							
								        requestStream.destroy();
							 | 
						||
| 
								 | 
							
								      } else if (requestStream.end) {
							 | 
						||
| 
								 | 
							
								        requestStream.end();
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  function makeRequest() {
							 | 
						||
| 
								 | 
							
								    currentRetryAttempt++;
							 | 
						||
| 
								 | 
							
								    debug(`Current retry attempt: ${currentRetryAttempt}`);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (streamMode) {
							 | 
						||
| 
								 | 
							
								      streamResponseHandled = false;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      delayStream = new PassThrough({objectMode: opts.objectMode});
							 | 
						||
| 
								 | 
							
								      requestStream = opts.request(requestOpts);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      setImmediate(() => {
							 | 
						||
| 
								 | 
							
								        retryStream.emit('request');
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      requestStream
							 | 
						||
| 
								 | 
							
								        // gRPC via google-cloud-node can emit an `error` as well as a `response`
							 | 
						||
| 
								 | 
							
								        // Whichever it emits, we run with-- we can't run with both. That's what
							 | 
						||
| 
								 | 
							
								        // is up with the `streamResponseHandled` tracking.
							 | 
						||
| 
								 | 
							
								        .on('error', err => {
							 | 
						||
| 
								 | 
							
								          if (streamResponseHandled) {
							 | 
						||
| 
								 | 
							
								            return;
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          streamResponseHandled = true;
							 | 
						||
| 
								 | 
							
								          onResponse(err);
							 | 
						||
| 
								 | 
							
								        })
							 | 
						||
| 
								 | 
							
								        .on('response', (resp, body) => {
							 | 
						||
| 
								 | 
							
								          if (streamResponseHandled) {
							 | 
						||
| 
								 | 
							
								            return;
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          streamResponseHandled = true;
							 | 
						||
| 
								 | 
							
								          onResponse(null, resp, body);
							 | 
						||
| 
								 | 
							
								        })
							 | 
						||
| 
								 | 
							
								        .on('complete', retryStream.emit.bind(retryStream, 'complete'));
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      requestStream.pipe(delayStream);
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      activeRequest = opts.request(requestOpts, onResponse);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  function retryAfterDelay(currentRetryAttempt) {
							 | 
						||
| 
								 | 
							
								    if (streamMode) {
							 | 
						||
| 
								 | 
							
								      resetStreams();
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const nextRetryDelay = getNextRetryDelay({
							 | 
						||
| 
								 | 
							
								      maxRetryDelay: opts.maxRetryDelay,
							 | 
						||
| 
								 | 
							
								      retryDelayMultiplier: opts.retryDelayMultiplier,
							 | 
						||
| 
								 | 
							
								      retryNumber: currentRetryAttempt,
							 | 
						||
| 
								 | 
							
								      timeOfFirstRequest,
							 | 
						||
| 
								 | 
							
								      totalTimeout: opts.totalTimeout,
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								    debug(`Next retry delay: ${nextRetryDelay}`);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (nextRetryDelay <= 0) {
							 | 
						||
| 
								 | 
							
								      numNoResponseAttempts = opts.noResponseRetries + 1;
							 | 
						||
| 
								 | 
							
								      return;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    setTimeout(makeRequest, nextRetryDelay);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  function onResponse(err, response, body) {
							 | 
						||
| 
								 | 
							
								    // An error such as DNS resolution.
							 | 
						||
| 
								 | 
							
								    if (err) {
							 | 
						||
| 
								 | 
							
								      numNoResponseAttempts++;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (numNoResponseAttempts <= opts.noResponseRetries) {
							 | 
						||
| 
								 | 
							
								        retryAfterDelay(numNoResponseAttempts);
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        if (streamMode) {
							 | 
						||
| 
								 | 
							
								          retryStream.emit('error', err);
							 | 
						||
| 
								 | 
							
								          retryStream.end();
							 | 
						||
| 
								 | 
							
								        } else {
							 | 
						||
| 
								 | 
							
								          callback(err, response, body);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      return;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // Send the response to see if we should try again.
							 | 
						||
| 
								 | 
							
								    // NOTE: "currentRetryAttempt" isn't accurate by default, as it counts
							 | 
						||
| 
								 | 
							
								    // the very first request sent as the first "retry". It is only accurate
							 | 
						||
| 
								 | 
							
								    // when a user provides their own "currentRetryAttempt" option at
							 | 
						||
| 
								 | 
							
								    // instantiation.
							 | 
						||
| 
								 | 
							
								    const adjustedCurrentRetryAttempt = manualCurrentRetryAttemptWasSet
							 | 
						||
| 
								 | 
							
								      ? currentRetryAttempt
							 | 
						||
| 
								 | 
							
								      : currentRetryAttempt - 1;
							 | 
						||
| 
								 | 
							
								    if (
							 | 
						||
| 
								 | 
							
								      adjustedCurrentRetryAttempt < opts.retries &&
							 | 
						||
| 
								 | 
							
								      opts.shouldRetryFn(response)
							 | 
						||
| 
								 | 
							
								    ) {
							 | 
						||
| 
								 | 
							
								      retryAfterDelay(currentRetryAttempt);
							 | 
						||
| 
								 | 
							
								      return;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // No more attempts need to be made, just continue on.
							 | 
						||
| 
								 | 
							
								    if (streamMode) {
							 | 
						||
| 
								 | 
							
								      retryStream.emit('response', response);
							 | 
						||
| 
								 | 
							
								      delayStream.pipe(retryStream);
							 | 
						||
| 
								 | 
							
								      requestStream.on('error', err => {
							 | 
						||
| 
								 | 
							
								        retryStream.destroy(err);
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      callback(err, response, body);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								module.exports = retryRequest;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function getNextRetryDelay(config) {
							 | 
						||
| 
								 | 
							
								  const {
							 | 
						||
| 
								 | 
							
								    maxRetryDelay,
							 | 
						||
| 
								 | 
							
								    retryDelayMultiplier,
							 | 
						||
| 
								 | 
							
								    retryNumber,
							 | 
						||
| 
								 | 
							
								    timeOfFirstRequest,
							 | 
						||
| 
								 | 
							
								    totalTimeout,
							 | 
						||
| 
								 | 
							
								  } = config;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const maxRetryDelayMs = maxRetryDelay * 1000;
							 | 
						||
| 
								 | 
							
								  const totalTimeoutMs = totalTimeout * 1000;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const jitter = Math.floor(Math.random() * 1000);
							 | 
						||
| 
								 | 
							
								  const calculatedNextRetryDelay =
							 | 
						||
| 
								 | 
							
								    Math.pow(retryDelayMultiplier, retryNumber) * 1000 + jitter;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const maxAllowableDelayMs =
							 | 
						||
| 
								 | 
							
								    totalTimeoutMs - (Date.now() - timeOfFirstRequest);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return Math.min(
							 | 
						||
| 
								 | 
							
								    calculatedNextRetryDelay,
							 | 
						||
| 
								 | 
							
								    maxAllowableDelayMs,
							 | 
						||
| 
								 | 
							
								    maxRetryDelayMs
							 | 
						||
| 
								 | 
							
								  );
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								module.exports.getNextRetryDelay = getNextRetryDelay;
							 |