Skip to content

Commit 6987760

Browse files
authored
feat: add additional retry configuration options (#634)
* feat: add additional retry configuration options * update value * update comment
1 parent 81a6572 commit 6987760

3 files changed

Lines changed: 128 additions & 7 deletions

File tree

core/packages/gaxios/src/common.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,34 @@ export interface RetryConfig {
329329
* the `retryDelay`
330330
*/
331331
retryBackoff?: (err: GaxiosError, defaultBackoffMs: number) => Promise<void>;
332+
333+
/**
334+
* Time that the initial request was made. Users should not set this directly.
335+
*/
336+
timeOfFirstRequest?: number;
337+
338+
/**
339+
* The length of time to keep retrying in ms. The last sleep period will
340+
* be shortened as necessary, so that the last retry runs at deadline (and not
341+
* considerably beyond it). The total time starting from when the initial
342+
* request is sent, after which an error will be returned, regardless of the
343+
* retrying attempts made meanwhile. Defaults to Number.MAX_SAFE_INTEGER indicating to effectively
344+
* ignore totalTimeout.
345+
*/
346+
totalTimeout?: number;
347+
348+
/*
349+
* The maximum time to delay in ms. If retryDelayMultiplier results in a
350+
* delay greater than maxRetryDelay, retries should delay by maxRetryDelay
351+
* seconds instead. Defaults to Number.MAX_SAFE_INTEGER indicating to effectively ignore maxRetryDelay.
352+
*/
353+
maxRetryDelay?: number;
354+
355+
/*
356+
* The multiplier by which to increase the delay time between the completion of
357+
* failed requests, and the initiation of the subsequent retrying request. Defaults to 2.
358+
*/
359+
retryDelayMultiplier?: number;
332360
}
333361

334362
export type FetchImplementation = (

core/packages/gaxios/src/retry.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// See the License for the specific language governing permissions and
1212
// limitations under the License.
1313

14-
import {GaxiosError} from './common';
14+
import {GaxiosError, RetryConfig} from './common';
1515

1616
export async function getRetryConfig(err: GaxiosError) {
1717
let config = getConfig(err);
@@ -33,6 +33,18 @@ export async function getRetryConfig(err: GaxiosError) {
3333
config.noResponseRetries === undefined || config.noResponseRetries === null
3434
? 2
3535
: config.noResponseRetries;
36+
config.retryDelayMultiplier = config.retryDelayMultiplier
37+
? config.retryDelayMultiplier
38+
: 2;
39+
config.timeOfFirstRequest = config.timeOfFirstRequest
40+
? config.timeOfFirstRequest
41+
: Date.now();
42+
config.totalTimeout = config.totalTimeout
43+
? config.totalTimeout
44+
: Number.MAX_SAFE_INTEGER;
45+
config.maxRetryDelay = config.maxRetryDelay
46+
? config.maxRetryDelay
47+
: Number.MAX_SAFE_INTEGER;
3648

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

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

7178
// We're going to retry! Incremenent the counter.
7279
err.config.retryConfig!.currentRetryAttempt! += 1;
@@ -157,3 +164,25 @@ function getConfig(err: GaxiosError) {
157164
}
158165
return;
159166
}
167+
168+
/**
169+
* Gets the delay to wait before the next retry.
170+
*
171+
* @param {RetryConfig} config The current set of retry options
172+
* @returns {number} the amount of ms to wait before the next retry attempt.
173+
*/
174+
function getNextRetryDelay(config: RetryConfig) {
175+
// Calculate time to wait with exponential backoff.
176+
// If this is the first retry, look for a configured retryDelay.
177+
const retryDelay = config.currentRetryAttempt ? 0 : config.retryDelay ?? 100;
178+
// Formula: retryDelay + ((retryDelayMultiplier^currentRetryAttempt - 1 / 2) * 1000)
179+
const calculatedDelay =
180+
retryDelay +
181+
((Math.pow(config.retryDelayMultiplier!, config.currentRetryAttempt!) - 1) /
182+
2) *
183+
1000;
184+
const maxAllowableDelay =
185+
config.totalTimeout! - (Date.now() - config.timeOfFirstRequest!);
186+
187+
return Math.min(calculatedDelay, maxAllowableDelay, config.maxRetryDelay!);
188+
}

core/packages/gaxios/test/test.retry.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,4 +303,68 @@ describe('🛸 retry & exponential backoff', () => {
303303
assert.ok(delay > 500 && delay < 599);
304304
scope.done();
305305
});
306+
307+
it('should respect retryDelayMultiplier if configured', async () => {
308+
const scope = nock(url)
309+
.get('/')
310+
.reply(500)
311+
.get('/')
312+
.reply(500)
313+
.get('/')
314+
.reply(200, {});
315+
const start = Date.now();
316+
await request({
317+
url,
318+
retryConfig: {
319+
retryDelayMultiplier: 3,
320+
},
321+
});
322+
const delay = Date.now() - start;
323+
assert.ok(delay > 1000 && delay < 1999);
324+
scope.done();
325+
});
326+
327+
it('should respect totalTimeout if configured', async () => {
328+
const scope = nock(url)
329+
.get('/')
330+
.reply(500)
331+
.get('/')
332+
.reply(500)
333+
.get('/')
334+
.reply(200, {});
335+
336+
const start = Date.now();
337+
await request({
338+
url,
339+
retryConfig: {
340+
retryDelayMultiplier: 100,
341+
totalTimeout: 3000,
342+
},
343+
});
344+
const delay = Date.now() - start;
345+
assert.ok(delay > 3000 && delay < 3999);
346+
scope.done();
347+
});
348+
349+
it('should respect maxRetryDelay if configured', async () => {
350+
const scope = nock(url)
351+
.get('/')
352+
.reply(500)
353+
.get('/')
354+
.reply(500)
355+
.get('/')
356+
.reply(200, {});
357+
358+
const start = Date.now();
359+
await request({
360+
url,
361+
retryConfig: {
362+
retryDelayMultiplier: 100,
363+
maxRetryDelay: 4000,
364+
},
365+
});
366+
const delay = Date.now() - start;
367+
assert.ok(delay > 4000 && delay < 4999);
368+
scope.done();
369+
});
306370
});

0 commit comments

Comments
 (0)