Skip to content

Commit ab19baf

Browse files
fvanwijksindresorhus
andauthoredDec 12, 2022
Add retry.backoffLimit option (#454)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent bc7d9f4 commit ab19baf

File tree

5 files changed

+83
-3
lines changed

5 files changed

+83
-3
lines changed
 

‎readme.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,17 @@ Default:
170170
- `methods`: `get` `put` `head` `delete` `options` `trace`
171171
- `statusCodes`: [`408`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) [`413`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413) [`429`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) [`500`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) [`502`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) [`503`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) [`504`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504)
172172
- `maxRetryAfter`: `undefined`
173+
- `backoffLimit`: `undefined`
173174

174175
An object representing `limit`, `methods`, `statusCodes` and `maxRetryAfter` fields for maximum retry count, allowed methods, allowed status codes and maximum [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) time.
175176

176177
If `retry` is a number, it will be used as `limit` and other defaults will remain in place.
177178

178179
If `maxRetryAfter` is set to `undefined`, it will use `options.timeout`. If [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header is greater than `maxRetryAfter`, it will cancel the request.
179180

180-
Delays between retries is calculated with the function `0.3 * (2 ** (retry - 1)) * 1000`, where `retry` is the attempt number (starts from 1).
181+
The `backoffLimit` option is the upper limit of the delay per retry in milliseconds.
182+
To clamp the delay, set `backoffLimit` to 1000, for example.
183+
By default, the delay is calculated with `0.3 * (2 ** (attemptCount - 1)) * 1000`. The delay increases exponentially.
181184

182185
Retries are not triggered following a [timeout](#timeout).
183186

@@ -188,7 +191,8 @@ const json = await ky('https://example.com', {
188191
retry: {
189192
limit: 10,
190193
methods: ['get'],
191-
statusCodes: [413]
194+
statusCodes: [413],
195+
backoffLimit: 3000
192196
}
193197
}).json();
194198
```

‎source/core/Ky.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ export class Ky {
235235
}
236236

237237
const BACKOFF_FACTOR = 0.3;
238-
return BACKOFF_FACTOR * (2 ** (this._retryCount - 1)) * 1000;
238+
return Math.min(this._options.retry.backoffLimit, BACKOFF_FACTOR * (2 ** (this._retryCount - 1)) * 1000);
239239
}
240240

241241
return 0;

‎source/types/retry.ts

+16
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,20 @@ export interface RetryOptions {
3333
@default Infinity
3434
*/
3535
maxRetryAfter?: number;
36+
37+
/**
38+
The upper limit of the delay per retry in milliseconds.
39+
To clamp the delay, set `backoffLimit` to 1000, for example.
40+
41+
By default, the delay is calculated in the following way:
42+
43+
```
44+
0.3 * (2 ** (attemptCount - 1)) * 1000
45+
```
46+
47+
The delay increases exponentially.
48+
49+
@default Infinity
50+
*/
51+
backoffLimit?: number;
3652
}

‎source/utils/normalize.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const defaultRetryOptions: Required<RetryOptions> = {
1717
statusCodes: retryStatusCodes,
1818
afterStatusCodes: retryAfterStatusCodes,
1919
maxRetryAfter: Number.POSITIVE_INFINITY,
20+
backoffLimit: Number.POSITIVE_INFINITY,
2021
};
2122

2223
export const normalizeRetryOptions = (retry: number | RetryOptions = {}): Required<RetryOptions> => {

‎test/retry.ts

+59
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {performance, PerformanceObserver} from 'node:perf_hooks';
2+
import process from 'node:process';
13
import test from 'ava';
24
import ky from '../source/index.js';
35
import {createHttpTestServer} from './helpers/create-http-test-server.js';
@@ -440,3 +442,60 @@ test('throws when retry.statusCodes is not an array', async t => {
440442

441443
await server.close();
442444
});
445+
446+
test('respect maximum backoff', async t => {
447+
const retryCount = 5;
448+
let requestCount = 0;
449+
450+
const server = await createHttpTestServer();
451+
server.get('/', (_request, response) => {
452+
requestCount++;
453+
454+
if (requestCount === retryCount) {
455+
response.end(fixture);
456+
} else {
457+
response.sendStatus(500);
458+
}
459+
});
460+
461+
// We allow the test to take more time on CI than locally, to reduce flakiness
462+
const allowedOffset = process.env.CI ? 1000 : 300;
463+
464+
// Register observer that asserts on duration when a measurement is performed
465+
const obs = new PerformanceObserver(items => {
466+
const measurements = items.getEntries();
467+
468+
const duration = measurements[0].duration ?? Number.NaN;
469+
const expectedDuration = {default: 300 + 600 + 1200 + 2400, custom: 300 + 600 + 1000 + 1000}[measurements[0].name] ?? Number.NaN;
470+
471+
t.true(Math.abs(duration - expectedDuration) < allowedOffset, `Duration of ${duration}ms is not close to expected duration ${expectedDuration}ms`); // Allow for 300ms difference
472+
473+
if (measurements[0].name === 'custom') {
474+
obs.disconnect();
475+
}
476+
});
477+
obs.observe({entryTypes: ['measure']});
478+
479+
// Start measuring
480+
performance.mark('start');
481+
t.is(await ky(server.url, {
482+
retry: retryCount,
483+
}).text(), fixture);
484+
performance.mark('end');
485+
486+
performance.mark('start-custom');
487+
requestCount = 0;
488+
t.is(await ky(server.url, {
489+
retry: {
490+
limit: retryCount,
491+
backoffLimit: 1000,
492+
},
493+
}).text(), fixture);
494+
495+
performance.mark('end-custom');
496+
497+
performance.measure('default', 'start', 'end');
498+
performance.measure('custom', 'start-custom', 'end-custom');
499+
500+
await server.close();
501+
});

0 commit comments

Comments
 (0)
Please sign in to comment.