feat: introduce exponential backoff utility

This commit is contained in:
2025-12-17 07:52:48 -05:00
parent 286824e3a1
commit 243c279ca9
2 changed files with 54 additions and 3 deletions

View File

@@ -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 }>();

View File

@@ -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);
}