mirror of
https://github.com/denoland/std.git
synced 2024-11-21 20:50:22 +00:00
feat(async): add jitter to retry exponential backoff (#3379)
This commit is contained in:
parent
9e21d9d047
commit
9daac3277a
@ -1,6 +1,8 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { assert } from "../_util/asserts.ts";
|
||||
|
||||
export class RetryError extends Error {
|
||||
constructor(cause: unknown, count: number) {
|
||||
super(`Exceeded max retry count (${count})`);
|
||||
@ -20,7 +22,7 @@ export interface RetryOptions {
|
||||
minTimeout?: number;
|
||||
}
|
||||
|
||||
const defaultRetryOptions = {
|
||||
const defaultRetryOptions: Required<RetryOptions> = {
|
||||
multiplier: 2,
|
||||
maxTimeout: 60000,
|
||||
maxAttempts: 5,
|
||||
@ -46,7 +48,7 @@ const defaultRetryOptions = {
|
||||
* maxAttempts: 5,
|
||||
* minTimeout: 100,
|
||||
* });
|
||||
```
|
||||
* ```
|
||||
*/
|
||||
export async function retry<T>(
|
||||
fn: (() => Promise<T>) | (() => T),
|
||||
@ -57,9 +59,11 @@ export async function retry<T>(
|
||||
...opts,
|
||||
};
|
||||
|
||||
if (options.maxTimeout >= 0 && options.minTimeout > options.maxTimeout) {
|
||||
throw new RangeError("minTimeout is greater than maxTimeout");
|
||||
}
|
||||
assert(options.maxTimeout >= 0, "maxTimeout is less than 0");
|
||||
assert(
|
||||
options.minTimeout <= options.maxTimeout,
|
||||
"minTimeout is greater than maxTimeout",
|
||||
);
|
||||
|
||||
let timeout = options.minTimeout;
|
||||
let error: unknown;
|
||||
@ -69,14 +73,26 @@ export async function retry<T>(
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
await new Promise((r) => setTimeout(r, timeout));
|
||||
timeout *= options.multiplier;
|
||||
timeout = Math.max(timeout, options.minTimeout);
|
||||
if (options.maxTimeout >= 0) {
|
||||
timeout = Math.min(timeout, options.maxTimeout);
|
||||
}
|
||||
|
||||
timeout = _exponentialBackoffWithJitter(
|
||||
options.maxTimeout,
|
||||
options.minTimeout,
|
||||
i,
|
||||
options.multiplier,
|
||||
);
|
||||
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RetryError(error, options.maxAttempts);
|
||||
}
|
||||
|
||||
export function _exponentialBackoffWithJitter(
|
||||
cap: number,
|
||||
base: number,
|
||||
attempt: number,
|
||||
multiplier: number,
|
||||
) {
|
||||
return Math.random() * Math.min(cap, base * multiplier ** attempt);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
import { retry } from "./retry.ts";
|
||||
import { assert, assertRejects } from "../testing/asserts.ts";
|
||||
import { _exponentialBackoffWithJitter, retry } from "./retry.ts";
|
||||
import { assertEquals, assertRejects } from "../testing/asserts.ts";
|
||||
|
||||
function generateErroringFunction(errorsBeforeSucceeds: number) {
|
||||
let errorCount = 0;
|
||||
@ -19,7 +19,7 @@ Deno.test("[async] retry", async function () {
|
||||
const result = await retry(threeErrors, {
|
||||
minTimeout: 100,
|
||||
});
|
||||
assert(result === 3);
|
||||
assertEquals(result, 3);
|
||||
});
|
||||
|
||||
Deno.test("[async] retry fails after max errors is passed", async function () {
|
||||
@ -31,11 +31,80 @@ Deno.test("[async] retry fails after max errors is passed", async function () {
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("[async] retry throws if minTimeout is less than maxTimeout", async function () {
|
||||
await assertRejects(() =>
|
||||
retry(() => {}, {
|
||||
minTimeout: 1000,
|
||||
maxTimeout: 100,
|
||||
})
|
||||
);
|
||||
Deno.test(
|
||||
"[async] retry throws if minTimeout is less than maxTimeout",
|
||||
async function () {
|
||||
await assertRejects(() =>
|
||||
retry(() => {}, {
|
||||
minTimeout: 1000,
|
||||
maxTimeout: 100,
|
||||
})
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Deno.test(
|
||||
"[async] retry throws if minTimeout is less than 0",
|
||||
async function () {
|
||||
await assertRejects(() =>
|
||||
retry(() => {}, {
|
||||
maxTimeout: -1,
|
||||
})
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// test util to ensure deterministic results during testing of backoff function by polyfilling Math.random
|
||||
function prngMulberry32(seed: number) {
|
||||
return function () {
|
||||
let t = (seed += 0x6d2b79f5);
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return ((t ^ (t >>> 14)) >>> 0);
|
||||
};
|
||||
}
|
||||
|
||||
// random seed generated with crypto.getRandomValues(new Uint32Array(1))[0]
|
||||
const INITIAL_SEED = 3460544849;
|
||||
|
||||
const expectedTimings: readonly (readonly number[] & { length: 10 })[] & {
|
||||
length: 10;
|
||||
} = [
|
||||
[69, 83, 56, 791, 131, 2140, 5480, 7706, 6036, 17908],
|
||||
[54, 16, 23, 381, 145, 2717, 3195, 4374, 3149, 21390],
|
||||
[32, 183, 334, 155, 391, 2954, 2890, 8202, 25202, 38387],
|
||||
[54, 89, 26, 174, 741, 1245, 1021, 12191, 19834, 17559],
|
||||
[74, 71, 113, 43, 496, 3196, 3843, 7860, 8943, 44312],
|
||||
[20, 129, 52, 555, 857, 3072, 3955, 7078, 5640, 1339],
|
||||
[75, 154, 59, 302, 998, 851, 5034, 8401, 23920, 41925],
|
||||
[86, 26, 211, 491, 139, 2263, 4502, 10713, 15976, 32328],
|
||||
[35, 10, 18, 449, 774, 698, 743, 8833, 24537, 7446],
|
||||
[11, 122, 178, 132, 573, 1803, 5107, 4505, 11523, 17598],
|
||||
] as const;
|
||||
|
||||
Deno.test("[async] retry - backoff function timings", async (t) => {
|
||||
const originalMathRandom = Math.random;
|
||||
|
||||
await t.step("_exponentialBackoffWithJitter", () => {
|
||||
let nextSeed = INITIAL_SEED;
|
||||
|
||||
for (const row of expectedTimings) {
|
||||
const randUint32 = prngMulberry32(nextSeed);
|
||||
nextSeed = prngMulberry32(nextSeed)();
|
||||
Math.random = () => randUint32() / 0x100000000;
|
||||
|
||||
const results: number[] = [];
|
||||
const base = 100;
|
||||
const cap = Infinity;
|
||||
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
const result = _exponentialBackoffWithJitter(cap, base, i, 2);
|
||||
results.push(Math.round(result));
|
||||
}
|
||||
|
||||
assertEquals(results as typeof row, row);
|
||||
}
|
||||
});
|
||||
|
||||
Math.random = originalMathRandom;
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user