54 lines
2.1 KiB
TypeScript
54 lines
2.1 KiB
TypeScript
import { Duration, type DurationLike } from "luxon";
|
|
|
|
interface CalculateExponentialBackoffOptions {
|
|
attempt: number;
|
|
baseMin?: DurationLike;
|
|
absCap?: DurationLike;
|
|
fuzzFactor?: number;
|
|
}
|
|
|
|
/**
|
|
* Generates a backoff time where both the Minimum floor and Maximum ceiling
|
|
* are "fuzzed" with jitter to prevent clustering at the edges.
|
|
*
|
|
* @param attempt - The current retry attempt (0-indexed).
|
|
* @param baseMin - The nominal minimum wait time (default: 1s).
|
|
* @param absCap - The absolute maximum wait time (default: 60s).
|
|
* @param fuzzFactor - How much to wobble the edges (0.1 = +/- 10%).
|
|
*
|
|
* @returns A random duration between the nominal minimum and maximum, in seconds.
|
|
*/
|
|
export function calculateExponentialBackoff({
|
|
attempt,
|
|
baseMin: baseMinDuration = Duration.fromObject({ minutes: 1 }),
|
|
absCap: absCapDuration = Duration.fromObject({ hours: 1 }),
|
|
fuzzFactor = 0.2,
|
|
}: CalculateExponentialBackoffOptions): number {
|
|
const baseMin = Duration.fromDurationLike(baseMinDuration).as("seconds");
|
|
const absCap = Duration.fromDurationLike(absCapDuration).as("seconds");
|
|
|
|
// 1. Calculate nominal boundaries
|
|
// Example: If baseMin is 1s, the nominal boundaries are 1s, 2s, 4s, 8s... (The 'ceiling' grows exponentially)
|
|
const nominalMin = baseMin;
|
|
const nominalCeiling = Math.min(baseMin * Math.pow(2, attempt), absCap);
|
|
|
|
// 2. Fuzz the Min (The Floor)
|
|
// Example: If min is 1s and fuzz is 0.2, the floor becomes random between 0.8s and 1.2s
|
|
const minFuzz = nominalMin * fuzzFactor;
|
|
const fuzzedMin = nominalMin + (Math.random() * 2 * minFuzz - minFuzz);
|
|
|
|
// 3. Fuzz the Max (The Ceiling)
|
|
// Example: If ceiling is 4s (and fuzz is 0.2), it becomes random between 3.2s and 4.8s
|
|
const maxFuzz = nominalCeiling * fuzzFactor;
|
|
const fuzzedCeiling =
|
|
nominalCeiling + (Math.random() * 2 * maxFuzz - maxFuzz);
|
|
|
|
// Safety: Ensure we don't return a negative number or cross boundaries weirdly
|
|
// (e.g. if fuzz makes min > max, we swap or clamp)
|
|
const safeMin = Math.max(0, fuzzedMin);
|
|
const safeMax = Math.max(safeMin, fuzzedCeiling);
|
|
|
|
// 4. Return random value in the new fuzzy range
|
|
return safeMin + Math.random() * (safeMax - safeMin);
|
|
}
|