Skip to content

Commit

Permalink
feat: add additional retry configuration options (#634)
Browse files Browse the repository at this point in the history
* feat: add additional retry configuration options

* update value

* update comment
  • Loading branch information
ddelgrosso1 authored Jun 27, 2024
1 parent b19aee2 commit cb5c833
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 7 deletions.
28 changes: 28 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,34 @@ export interface RetryConfig {
* the `retryDelay`
*/
retryBackoff?: (err: GaxiosError, defaultBackoffMs: number) => Promise<void>;

/**
* Time that the initial request was made. Users should not set this directly.
*/
timeOfFirstRequest?: number;

/**
* The length of time to keep retrying in ms. 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. Defaults to Number.MAX_SAFE_INTEGER indicating to effectively
* ignore totalTimeout.
*/
totalTimeout?: number;

/*
* The maximum time to delay in ms. If retryDelayMultiplier results in a
* delay greater than maxRetryDelay, retries should delay by maxRetryDelay
* seconds instead. Defaults to Number.MAX_SAFE_INTEGER indicating to effectively ignore maxRetryDelay.
*/
maxRetryDelay?: number;

/*
* The multiplier by which to increase the delay time between the completion of
* failed requests, and the initiation of the subsequent retrying request. Defaults to 2.
*/
retryDelayMultiplier?: number;
}

export type FetchImplementation = (
Expand Down
43 changes: 36 additions & 7 deletions src/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {GaxiosError} from './common';
import {GaxiosError, RetryConfig} from './common';

export async function getRetryConfig(err: GaxiosError) {
let config = getConfig(err);
Expand All @@ -33,6 +33,18 @@ export async function getRetryConfig(err: GaxiosError) {
config.noResponseRetries === undefined || config.noResponseRetries === null
? 2
: config.noResponseRetries;
config.retryDelayMultiplier = config.retryDelayMultiplier
? config.retryDelayMultiplier
: 2;
config.timeOfFirstRequest = config.timeOfFirstRequest
? config.timeOfFirstRequest
: Date.now();
config.totalTimeout = config.totalTimeout
? config.totalTimeout
: Number.MAX_SAFE_INTEGER;
config.maxRetryDelay = config.maxRetryDelay
? config.maxRetryDelay
: Number.MAX_SAFE_INTEGER;

// If this wasn't in the list of status codes where we want
// to automatically retry, return.
Expand Down Expand Up @@ -61,12 +73,7 @@ export async function getRetryConfig(err: GaxiosError) {
return {shouldRetry: false, config: err.config};
}

// Calculate time to wait with exponential backoff.
// If this is the first retry, look for a configured retryDelay.
const retryDelay = config.currentRetryAttempt ? 0 : config.retryDelay ?? 100;
// Formula: retryDelay + ((2^c - 1 / 2) * 1000)
const delay =
retryDelay + ((Math.pow(2, config.currentRetryAttempt) - 1) / 2) * 1000;
const delay = getNextRetryDelay(config);

// We're going to retry! Incremenent the counter.
err.config.retryConfig!.currentRetryAttempt! += 1;
Expand Down Expand Up @@ -157,3 +164,25 @@ function getConfig(err: GaxiosError) {
}
return;
}

/**
* Gets the delay to wait before the next retry.
*
* @param {RetryConfig} config The current set of retry options
* @returns {number} the amount of ms to wait before the next retry attempt.
*/
function getNextRetryDelay(config: RetryConfig) {
// Calculate time to wait with exponential backoff.
// If this is the first retry, look for a configured retryDelay.
const retryDelay = config.currentRetryAttempt ? 0 : config.retryDelay ?? 100;
// Formula: retryDelay + ((retryDelayMultiplier^currentRetryAttempt - 1 / 2) * 1000)
const calculatedDelay =
retryDelay +
((Math.pow(config.retryDelayMultiplier!, config.currentRetryAttempt!) - 1) /
2) *
1000;
const maxAllowableDelay =
config.totalTimeout! - (Date.now() - config.timeOfFirstRequest!);

return Math.min(calculatedDelay, maxAllowableDelay, config.maxRetryDelay!);
}
64 changes: 64 additions & 0 deletions test/test.retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,68 @@ describe('🛸 retry & exponential backoff', () => {
assert.ok(delay > 500 && delay < 599);
scope.done();
});

it('should respect retryDelayMultiplier if configured', async () => {
const scope = nock(url)
.get('/')
.reply(500)
.get('/')
.reply(500)
.get('/')
.reply(200, {});
const start = Date.now();
await request({
url,
retryConfig: {
retryDelayMultiplier: 3,
},
});
const delay = Date.now() - start;
assert.ok(delay > 1000 && delay < 1999);
scope.done();
});

it('should respect totalTimeout if configured', async () => {
const scope = nock(url)
.get('/')
.reply(500)
.get('/')
.reply(500)
.get('/')
.reply(200, {});

const start = Date.now();
await request({
url,
retryConfig: {
retryDelayMultiplier: 100,
totalTimeout: 3000,
},
});
const delay = Date.now() - start;
assert.ok(delay > 3000 && delay < 3999);
scope.done();
});

it('should respect maxRetryDelay if configured', async