From 5a3f915641362ec7d9cd8fe02837a53f1f21117f Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Thu, 29 Jan 2026 11:38:18 -0800 Subject: [PATCH] feat(gateway): add backoff calculation utilities --- src/cli/gateway-cli/backoff.test.ts | 48 +++++++++++++++++++++++++++++ src/cli/gateway-cli/backoff.ts | 30 ++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/cli/gateway-cli/backoff.test.ts create mode 100644 src/cli/gateway-cli/backoff.ts diff --git a/src/cli/gateway-cli/backoff.test.ts b/src/cli/gateway-cli/backoff.test.ts new file mode 100644 index 000000000..3af4bafc2 --- /dev/null +++ b/src/cli/gateway-cli/backoff.test.ts @@ -0,0 +1,48 @@ +// src/cli/gateway-cli/backoff.test.ts +import { describe, it, expect } from "vitest"; +import { calculateBackoffMs, applyJitter } from "./backoff.js"; + +describe("calculateBackoffMs", () => { + it("returns 0 for zero consecutive failures", () => { + expect(calculateBackoffMs(0)).toBe(0); + }); + + it("returns 2000ms for first failure", () => { + expect(calculateBackoffMs(1)).toBe(2000); + }); + + it("returns 4000ms for second failure", () => { + expect(calculateBackoffMs(2)).toBe(4000); + }); + + it("returns 32000ms for fifth failure", () => { + expect(calculateBackoffMs(5)).toBe(32000); + }); + + it("caps at 60000ms for high failure counts", () => { + expect(calculateBackoffMs(10)).toBe(60000); + expect(calculateBackoffMs(100)).toBe(60000); + }); +}); + +describe("applyJitter", () => { + it("returns 0 for 0 input", () => { + expect(applyJitter(0)).toBe(0); + }); + + it("returns value within +/- 10% of input", () => { + const input = 10000; + const minExpected = Math.floor(input * 0.9); + const maxExpected = Math.ceil(input * 1.1); + for (let i = 0; i < 100; i++) { + const result = applyJitter(input); + expect(result).toBeGreaterThanOrEqual(minExpected); + expect(result).toBeLessThanOrEqual(maxExpected); + } + }); + + it("returns an integer", () => { + const result = applyJitter(2000); + expect(Number.isInteger(result)).toBe(true); + }); +}); diff --git a/src/cli/gateway-cli/backoff.ts b/src/cli/gateway-cli/backoff.ts new file mode 100644 index 000000000..b52a77b82 --- /dev/null +++ b/src/cli/gateway-cli/backoff.ts @@ -0,0 +1,30 @@ +// src/cli/gateway-cli/backoff.ts + +const BASE_BACKOFF_MS = 2000; +const MAX_BACKOFF_MS = 60_000; +const JITTER_FACTOR = 0.1; // +/- 10% + +/** + * Calculate exponential backoff based on consecutive failures. + * Formula: min(BASE * 2^(failures-1), MAX) + * + * @param consecutiveFailures - Number of consecutive failures (0 = no backoff) + * @returns Backoff duration in milliseconds + */ +export function calculateBackoffMs(consecutiveFailures: number): number { + if (consecutiveFailures <= 0) return 0; + const exponential = BASE_BACKOFF_MS * Math.pow(2, consecutiveFailures - 1); + return Math.min(exponential, MAX_BACKOFF_MS); +} + +/** + * Apply random jitter to a backoff value to prevent thundering herd. + * + * @param baseMs - Base backoff in milliseconds + * @returns Backoff with +/- 10% jitter applied, rounded to integer + */ +export function applyJitter(baseMs: number): number { + if (baseMs <= 0) return 0; + const jitter = (Math.random() - 0.5) * 2 * JITTER_FACTOR * baseMs; + return Math.round(baseMs + jitter); +}