Make jitter subtract only
This commit is contained in:
@@ -5,7 +5,7 @@ import { ExponentialBackoffMaxRetriesHitError } from './errors.ts';
|
||||
*
|
||||
* The delay increases exponentially with each attempt, up to a maximum delay.
|
||||
*
|
||||
* The jitter is a random amount of time added to the delay to prevent thundering herd problems.
|
||||
* The jitter is a random amount of time subtracted from the delay to prevent thundering herd problems.
|
||||
*
|
||||
* The growth rate is the factor by which the delay increases with each attempt.
|
||||
*/
|
||||
@@ -51,7 +51,7 @@ export class ExponentialBackoff {
|
||||
* @param options.maxAttempts - Maximum number of attempts; `0` retries indefinitely. Default: `10`.
|
||||
* @param options.baseDelay - Delay used as the basis for the first retry. Default: `1_000` ms.
|
||||
* @param options.growthRate - Multiplier applied to the delay after each attempt. Default: `2`.
|
||||
* @param options.jitter - Random proportional variation applied to each delay. Default: `0.1`.
|
||||
* @param options.jitter - Maximum proportional reduction subtracted from each delay (0–1). Default: `0.1`.
|
||||
*/
|
||||
constructor(options: Partial<ExponentialBackoffOptions> = {}) {
|
||||
this.options = {
|
||||
@@ -114,8 +114,6 @@ export class ExponentialBackoff {
|
||||
/**
|
||||
* Calculate the delay before we should attempt to retry
|
||||
*
|
||||
* NOTE: The maximum delay is (maxDelay * (1 + jitter))
|
||||
*
|
||||
* @param attempt
|
||||
* @returns The time in milliseconds before another attempt should be made
|
||||
*/
|
||||
@@ -129,14 +127,14 @@ export class ExponentialBackoff {
|
||||
// Cap the delay to the maximum. Do this before the jitter so jitter does not become larger than delay
|
||||
const cappedDelay = Math.min(rawDelay, options.maxDelay);
|
||||
|
||||
// Get the jitter direction. This will be between -1 and 1
|
||||
const jitterDirection = 2 * Math.random() - 1;
|
||||
// Get a random number for the amount to "jitter" the delay by
|
||||
const jitterAmount = Math.random();
|
||||
|
||||
// Calculate the jitter
|
||||
const jitter = jitterDirection * options.jitter * cappedDelay;
|
||||
const jitter = jitterAmount * options.jitter * cappedDelay;
|
||||
|
||||
// Add the jitter to the delay
|
||||
return cappedDelay + jitter;
|
||||
// Subtract the jitter from the delay
|
||||
return cappedDelay - jitter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +161,7 @@ export type ExponentialBackoffOptions = {
|
||||
growthRate: number;
|
||||
|
||||
/**
|
||||
* The jitter of the delay as a percentage of growthRate
|
||||
* The jitter of the delay as a percentage of growthRate. The jitter is subtracted from the delay.
|
||||
*/
|
||||
jitter: number;
|
||||
};
|
||||
|
||||
@@ -9,8 +9,8 @@ import { ExponentialBackoffMaxRetriesHitError } from '../source/errors.ts';
|
||||
const testExponentialBackoffRunUsesDefaultOptions = async (): Promise<void> => {
|
||||
// Fake timers let us advance time without waiting real seconds between retries.
|
||||
vi.useFakeTimers();
|
||||
// Pin Math.random so jitter does not randomize the delay we are about to measure.
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0.5);
|
||||
// Pin Math.random to 0 so jitter does not reduce the default delay.
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0);
|
||||
|
||||
try {
|
||||
// The wrapped function fails on its first invocation and succeeds on the second.
|
||||
@@ -315,11 +315,11 @@ const testExponentialBackoffCapsDelayAtMaxDelay = async (): Promise<void> => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that jitter scales the capped delay up or down by jitter * cappedDelay based on Math.random.
|
||||
* Tests that jitter subtracts up to jitter * cappedDelay from the capped delay based on Math.random.
|
||||
*/
|
||||
const testExponentialBackoffAppliesJitter = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
// random = 1 → jitterDirection = 2*1 - 1 = +1 → full positive 10% jitter on the delay.
|
||||
// random = 1 → full 10% reduction: 1000 - (1 * 0.1 * 1000) = 900ms.
|
||||
vi.spyOn(Math, 'random').mockReturnValue(1);
|
||||
|
||||
try {
|
||||
@@ -337,8 +337,11 @@ const testExponentialBackoffAppliesJitter = async (): Promise<void> => {
|
||||
await Promise.resolve();
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// cappedDelay = 1000; with +10% jitter the retry fires after 1100ms, not 1000ms.
|
||||
await vi.advanceTimersByTimeAsync(1_100);
|
||||
// Advancing 899ms is one ms short of the jittered delay; 900ms triggers the retry.
|
||||
await vi.advanceTimersByTimeAsync(899);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
|
||||
await expect(promise).resolves.toBe('success');
|
||||
@@ -364,7 +367,7 @@ const runTests = async (): Promise<void> => {
|
||||
test('ExponentialBackoff: retries indefinitely when maxAttempts is 0', testExponentialBackoffRetriesIndefinitelyWhenMaxAttemptsIsZero);
|
||||
test('ExponentialBackoff: increases delay exponentially between attempts', testExponentialBackoffIncreasesDelayExponentially);
|
||||
test('ExponentialBackoff: caps delay at maxDelay', testExponentialBackoffCapsDelayAtMaxDelay);
|
||||
test('ExponentialBackoff: applies jitter around the capped delay', testExponentialBackoffAppliesJitter);
|
||||
test('ExponentialBackoff: subtracts jitter from the capped delay', testExponentialBackoffAppliesJitter);
|
||||
};
|
||||
|
||||
await runTests();
|
||||
|
||||
Reference in New Issue
Block a user