Skip to content

Commit

Permalink
feat: support enhanced retry settings (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenplusplus committed Jun 24, 2021
1 parent 8df9887 commit 0184676
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 27 deletions.
73 changes: 51 additions & 22 deletions index.js
Expand Up @@ -6,6 +6,29 @@ var debug = require('debug')('retry-request');
var 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) {
Expand Down Expand Up @@ -42,33 +65,16 @@ function retryRequest(requestOpts, opts, callback) {
callback = opts;
}

opts = opts || DEFAULTS;
var manualCurrentRetryAttemptWasSet = opts && typeof opts.currentRetryAttempt === 'number';
opts = Object.assign({}, DEFAULTS, opts);

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;
}

var manualCurrentRetryAttemptWasSet = typeof opts.currentRetryAttempt === 'number';
if (!manualCurrentRetryAttemptWasSet) {
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;

Expand All @@ -93,6 +99,7 @@ function retryRequest(requestOpts, opts, callback) {
retryStream.abort = resetStreams;
}

var timeOfFirstRequest = Date.now();
if (currentRetryAttempt > 0) {
retryAfterDelay(currentRetryAttempt);
} else {
Expand Down Expand Up @@ -167,7 +174,13 @@ function retryRequest(requestOpts, opts, callback) {
resetStreams();
}

var nextRetryDelay = getNextRetryDelay(currentRetryAttempt);
var nextRetryDelay = getNextRetryDelay({
maxRetryDelay: opts.maxRetryDelay,
retryDelayMultiplier: opts.retryDelayMultiplier,
retryNumber: currentRetryAttempt,
timeOfFirstRequest,
totalTimeout: opts.totalTimeout,
});
debug(`Next retry delay: ${nextRetryDelay}`);

setTimeout(makeRequest, nextRetryDelay);
Expand Down Expand Up @@ -218,8 +231,24 @@ function retryRequest(requestOpts, opts, callback) {

module.exports = retryRequest;

function getNextRetryDelay(retryNumber) {
return (Math.pow(2, retryNumber) * 1000) + Math.floor(Math.random() * 1000);
function getNextRetryDelay(config) {
var {
maxRetryDelay,
retryDelayMultiplier,
retryNumber,
timeOfFirstRequest,
totalTimeout,
} = config;

var maxRetryDelayMs = maxRetryDelay * 1000;
var totalTimeoutMs = totalTimeout * 1000;

var jitter = Math.floor(Math.random() * 1000);
var calculatedNextRetryDelay = Math.pow(retryDelayMultiplier, retryNumber) * 1000 + jitter;

var maxAllowableDelayMs = totalTimeoutMs - (Date.now() - timeOfFirstRequest);

return Math.min(calculatedNextRetryDelay, maxAllowableDelayMs, maxRetryDelayMs);
}

module.exports.getNextRetryDelay = getNextRetryDelay;
24 changes: 24 additions & 0 deletions readme.md
Expand Up @@ -168,6 +168,30 @@ request(urlThatReturns503, opts, function (err, resp, body) {
});
```

#### `opts.maxRetryDelay`

Type: `Number`

Default: `64`

The maximum time to delay in seconds. If retryDelayMultiplier results in a delay greater than maxRetryDelay, retries should delay by maxRetryDelay seconds instead.

#### `opts.retryDelayMultiplier`

Type: `Number`

Default: `2`

The multiplier by which to increase the delay time between the completion of failed requests, and the initiation of the subsequent retrying request.

#### `opts.totalTimeout`

Type: `Number`

Default: `600`

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.

### cb *(optional)*

Passed directly to `request`. See the callback section: https://github.com/request/request/#requestoptions-callback.
Expand Down
94 changes: 89 additions & 5 deletions test.js
Expand Up @@ -413,20 +413,104 @@ describe('retry-request', function () {
});

describe('getNextRetryDelay', function () {
function secs(seconds) {
var maxRetryDelay = 64;
var retryDelayMultiplier = 2;
var timeOfFirstRequest;
var totalTimeout = 64;

function secondsToMs(seconds) {
return seconds * 1000;
}

beforeEach(() => {
timeOfFirstRequest = Date.now();
});

it('should return exponential retry delay', function () {
[1, 2, 3, 4, 5].forEach(assertTime);

function assertTime(retryNumber) {
var min = (Math.pow(2, retryNumber) * secs(1));
var max = (Math.pow(2, retryNumber) * secs(1)) + secs(1);
var min = (Math.pow(2, retryNumber) * secondsToMs(1));
var max = (Math.pow(2, retryNumber) * secondsToMs(1)) + secondsToMs(1);

var delay = retryRequest.getNextRetryDelay({
maxRetryDelay,
retryDelayMultiplier,
retryNumber,
timeOfFirstRequest,
totalTimeout,
});

assert(delay >= min && delay <= max);
}
});

it('should allow overriding the multiplier', function () {
[1, 2, 3, 4, 5].forEach(assertTime);

function assertTime(multiplier) {
var min = (Math.pow(multiplier, 1) * secondsToMs(1));
var max = (Math.pow(multiplier, 1) * secondsToMs(1)) + secondsToMs(1);

var time = retryRequest.getNextRetryDelay(retryNumber);
var delay = retryRequest.getNextRetryDelay({
maxRetryDelay,
retryDelayMultiplier: multiplier,
retryNumber: 1,
timeOfFirstRequest,
totalTimeout,
});

assert(time >= min && time <= max);
assert(delay >= min && delay <= max);
}
});

it('should honor total timeout setting', function () {
// This test passes settings to calculate an enormous retry delay, if it
// weren't for the timeout restrictions imposed by `totalTimeout`.
// So, even though this is pretending to be the 10th retry, and our
// `maxRetryDelay` is huge, the 60 second max timeout we have for all
// requests to complete by is honored.
// We tell the function that we have already been trying this request for
// 30 seconds, and we will only wait a maximum of 60 seconds. Therefore, we
// should end up with a retry delay of around 30 seconds.
var retryDelay = retryRequest.getNextRetryDelay({
// Allow 60 seconds maximum delay,
timeOfFirstRequest: Date.now() - secondsToMs(30), // 30 seconds ago.
totalTimeout: 60,

// Inflating these numbers to be sure the smaller timeout is chosen:
maxRetryDelay: 1e9,
retryDelayMultiplier: 10,
retryNumber: 10,
});

var min = retryDelay - 10;
var max = retryDelay + 10;
assert(retryDelay >= min && retryDelay <= max);
});

it('should return maxRetryDelay if calculated retry would be too high', function () {
var delayWithoutLowMaxRetryDelay = retryRequest.getNextRetryDelay({
maxRetryDelay,
retryDelayMultiplier,
retryNumber: 100,
timeOfFirstRequest,
totalTimeout,
});

var maxRetryDelayMs = secondsToMs(maxRetryDelay);
var min = maxRetryDelayMs - 10;
var max = maxRetryDelayMs + 10;
assert(delayWithoutLowMaxRetryDelay >= min && delayWithoutLowMaxRetryDelay <= max);

var lowMaxRetryDelay = 1;
var delayWithLowMaxRetryDelay = retryRequest.getNextRetryDelay({
maxRetryDelay: lowMaxRetryDelay,
retryDelayMultiplier,
retryNumber: 100,
timeOfFirstRequest,
totalTimeout,
});
assert.strictEqual(delayWithLowMaxRetryDelay, secondsToMs(lowMaxRetryDelay));
});
});

0 comments on commit 0184676

Please sign in to comment.