mirror of
https://github.com/titanscouting/tra-analysis.git
synced 2024-12-30 18:59:10 +00:00
212 lines
5.2 KiB
JavaScript
212 lines
5.2 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
var through = require('through2');
|
||
|
|
||
|
var DEFAULTS = {
|
||
|
objectMode: false,
|
||
|
retries: 2,
|
||
|
noResponseRetries: 2,
|
||
|
currentRetryAttempt: 0,
|
||
|
shouldRetryFn: function (response) {
|
||
|
var 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]
|
||
|
];
|
||
|
|
||
|
var statusCode = response.statusCode;
|
||
|
|
||
|
var 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) {
|
||
|
var streamMode = typeof arguments[arguments.length - 1] !== 'function';
|
||
|
|
||
|
if (typeof opts === 'function') {
|
||
|
callback = opts;
|
||
|
}
|
||
|
|
||
|
opts = opts || DEFAULTS;
|
||
|
|
||
|
if (typeof opts.objectMode === 'undefined') {
|
||
|
opts.objectMode = DEFAULTS.objectMode;
|
||
|
}
|
||
|
if (typeof opts.request === 'undefined') {
|
||
|
try {
|
||
|
opts.request = require('request');
|
||
|
} catch (e) {
|
||
|
throw new Error('A request library must be provided to retry-request.');
|
||
|
}
|
||
|
}
|
||
|
if (typeof opts.retries !== 'number') {
|
||
|
opts.retries = DEFAULTS.retries;
|
||
|
}
|
||
|
if (typeof opts.currentRetryAttempt !== 'number') {
|
||
|
opts.currentRetryAttempt = DEFAULTS.currentRetryAttempt;
|
||
|
}
|
||
|
if (typeof opts.noResponseRetries !== 'number') {
|
||
|
opts.noResponseRetries = DEFAULTS.noResponseRetries;
|
||
|
}
|
||
|
if (typeof opts.shouldRetryFn !== 'function') {
|
||
|
opts.shouldRetryFn = DEFAULTS.shouldRetryFn;
|
||
|
}
|
||
|
|
||
|
var currentRetryAttempt = opts.currentRetryAttempt;
|
||
|
|
||
|
var numNoResponseAttempts = 0;
|
||
|
var streamResponseHandled = false;
|
||
|
|
||
|
var retryStream;
|
||
|
var requestStream;
|
||
|
var delayStream;
|
||
|
|
||
|
var activeRequest;
|
||
|
var retryRequest = {
|
||
|
abort: function () {
|
||
|
if (activeRequest && activeRequest.abort) {
|
||
|
activeRequest.abort();
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
if (streamMode) {
|
||
|
retryStream = through({ objectMode: opts.objectMode });
|
||
|
retryStream.abort = resetStreams;
|
||
|
}
|
||
|
|
||
|
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++;
|
||
|
|
||
|
if (streamMode) {
|
||
|
streamResponseHandled = false;
|
||
|
|
||
|
delayStream = through({ objectMode: opts.objectMode });
|
||
|
requestStream = opts.request(requestOpts);
|
||
|
|
||
|
setImmediate(function () {
|
||
|
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', function (err) {
|
||
|
if (streamResponseHandled) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
streamResponseHandled = true;
|
||
|
onResponse(err);
|
||
|
})
|
||
|
.on('response', function (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();
|
||
|
}
|
||
|
|
||
|
setTimeout(makeRequest, getNextRetryDelay(currentRetryAttempt));
|
||
|
}
|
||
|
|
||
|
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.
|
||
|
if (currentRetryAttempt <= 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', function (err) {
|
||
|
retryStream.destroy(err);
|
||
|
});
|
||
|
} else {
|
||
|
callback(err, response, body);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = retryRequest;
|
||
|
|
||
|
function getNextRetryDelay(retryNumber) {
|
||
|
return (Math.pow(2, retryNumber) * 1000) + Math.floor(Math.random() * 1000);
|
||
|
}
|
||
|
|
||
|
module.exports.getNextRetryDelay = getNextRetryDelay;
|