diff --git a/source/exponential-backoff.ts b/source/exponential-backoff.ts index f2cd4b0..ec97505 100644 --- a/source/exponential-backoff.ts +++ b/source/exponential-backoff.ts @@ -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 = {}) { 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; }; diff --git a/test/exponential-backoff.test.ts b/test/exponential-backoff.test.ts index d49137c..23f30b3 100644 --- a/test/exponential-backoff.test.ts +++ b/test/exponential-backoff.test.ts @@ -9,8 +9,8 @@ import { ExponentialBackoffMaxRetriesHitError } from '../source/errors.ts'; const testExponentialBackoffRunUsesDefaultOptions = async (): Promise => { // 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 => { }; /** - * 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 => { 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 => { 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 => { 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();