Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support enhanced retry settings #35

Merged
merged 4 commits into from
Jun 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
73 changes: 51 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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));
});
});