From 243c279ca9efffc77aa5f36c1b6645f7bab66f8f Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Wed, 17 Dec 2025 07:52:48 -0500 Subject: [PATCH] feat: introduce exponential backoff utility --- src/index.ts | 8 ++-- src/libs/calculateExponentialBackoff.ts | 49 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 src/libs/calculateExponentialBackoff.ts diff --git a/src/index.ts b/src/index.ts index 60453d2..9fe99a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,9 +4,11 @@ import { OpenAPIHono } from "@hono/zod-openapi"; import { maybeUpdateLastConnectedAt } from "~/controllers/maybeUpdateLastConnectedAt"; import type { QueueName } from "~/libs/tasks/queueName.ts"; -import { onNewEpisode } from "./controllers/internal/new-episode"; -import { AnilistUpdateType } from "./libs/anilist/updateType"; -import type { QueueBody } from "./libs/tasks/queueTask"; +import { onNewEpisode } from "~/controllers/internal/new-episode"; +import { AnilistUpdateType } from "~/libs/anilist/updateType"; +import type { QueueBody } from "~/libs/tasks/queueTask"; +import { calculateExponentialBackoff } from "~/libs/calculateExponentialBackoff"; +import { Duration, type DurationLike } from "luxon"; export const app = new OpenAPIHono<{ Bindings: Env }>(); diff --git a/src/libs/calculateExponentialBackoff.ts b/src/libs/calculateExponentialBackoff.ts new file mode 100644 index 0000000..7c47dfd --- /dev/null +++ b/src/libs/calculateExponentialBackoff.ts @@ -0,0 +1,49 @@ +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({ seconds: 1 }), absCap: absCapDuration = Duration.fromObject({ seconds: 60 }), 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); +} \ No newline at end of file