From cb5c833a9750bf6d0c0f8e27992bb44bd953566c Mon Sep 17 00:00:00 2001 From: Denis DelGrosso <85250797+ddelgrosso1@users.noreply.github.com> Date: Thu, 27 Jun 2024 12:13:53 -0400 Subject: [PATCH] feat: add additional retry configuration options (#634) * feat: add additional retry configuration options * update value * update comment --- src/common.ts | 28 ++++++++++++++++++++ src/retry.ts | 43 ++++++++++++++++++++++++++----- test/test.retry.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 7 deletions(-) diff --git a/src/common.ts b/src/common.ts index d4a2678..b1bedf6 100644 --- a/src/common.ts +++ b/src/common.ts @@ -329,6 +329,34 @@ export interface RetryConfig { * the `retryDelay` */ retryBackoff?: (err: GaxiosError, defaultBackoffMs: number) => Promise; + + /** + * 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 = ( diff --git a/src/retry.ts b/src/retry.ts index 28ad6d7..4f28f94 100644 --- a/src/retry.ts +++ b/src/retry.ts @@ -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); @@ -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. @@ -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; @@ -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!); +} diff --git a/test/test.retry.ts b/test/test.retry.ts index 53a0773..7412da5 100644 --- a/test/test.retry.ts +++ b/test/test.retry.ts @@ -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 () => { + const scope = nock(url) + .get('/') + .reply(500) + .get('/') + .reply(500) + .get('/') + .reply(200, {}); + + const start = Date.now(); + await request({ + url, + retryConfig: { + retryDelayMultiplier: 100, + maxRetryDelay: 4000, + }, + }); + const delay = Date.now() - start; + assert.ok(delay > 4000 && delay < 4999); + scope.done(); + }); });