Files
skills/docs/api-stack/api/api-plugin-database/src/retry/retry.md
2026-05-29 08:20:56 +01:00

3.3 KiB

retry

(no submodules)

Purpose

Provides withRetry, a small exponential-backoff helper used by the connection pool, the migration runner, and any caller-supplied wrapping of adapter calls. The helper is intentionally minimal: it owns the loop, the backoff calculation, the optional jitter, and the abort plumbing. Higher-level concerns (circuit breakers, dead-letter routing, OTel spans) are layered on top by the caller — keeping retry stateless makes it composable.

Backoff follows the canonical formula min(baseBackoffMs * 2^(attempt-1), maxBackoffMs). Jittered mode multiplies the result by a [0, 1) random factor so concurrent callers don't synchronise their retry windows. Both the RNG and the setTimeout setter are injectable so tests run deterministically without time travel.

Features

  • Exponential backoff capped at maxBackoffMs.
  • Optional full-jitter randomisation through an injectable Rng.
  • isRetryable(err, attempt) predicate for bail-out on permanent failures.
  • AbortSignal propagation cancels pending sleeps and the loop.
  • Pure async — no global state, no side effects beyond the optional sleep.

Usage: Basic

import { withRetry } from "@suaveplan/api-plugin-database/retry/retry";

const result = await withRetry(() => db.query("SELECT 1"), {
    maxAttempts: 3,
    baseBackoffMs: 50,
    maxBackoffMs: 1_000,
    jitter: true,
});

Usage: Advanced

import { withRetry } from "@suaveplan/api-plugin-database";

const controller = new AbortController();
setTimeout(() => controller.abort(), 30_000);

const data = await withRetry(
    async (attempt) => {
        const adapter = await pool.acquire();
        try {
            return await adapter.query("SELECT * FROM users WHERE active");
        } finally {
            pool.release(adapter);
        }
    },
    {
        maxAttempts: 5,
        baseBackoffMs: 100,
        maxBackoffMs: 5_000,
        jitter: true,
        signal: controller.signal,
        isRetryable: (err) =>
            err instanceof Error && /timeout|reset/i.test(err.message),
    }
);

API Reference

withRetry<T>(fn, options): Promise<T>

Invokes fn(attempt) (1-indexed) until success or budget exhaustion. Returns the resolved value or rethrows the final error.

interface RetryOptions

  • maxAttempts (number, >=1) — total attempt budget.
  • baseBackoffMs (number, >=1) — base interval.
  • maxBackoffMs (number, >=1) — interval ceiling.
  • jitter (boolean) — multiply by random() for full jitter.
  • isRetryable? ((err, attempt) => boolean) — bail predicate.
  • rng? (Rng) — injectable random source.
  • signal? (AbortSignal) — cancels in-flight sleep + loop.
  • schedule? ((cb, ms) => void) — injectable setTimeout for tests.

Implementation Notes

  • Sleeps are interrupted promptly by AbortSignal; the inner promise rejects with the signal's reason (when an Error) or a generic Error("withRetry aborted").
  • Aborts at the top of the loop (before fn(attempt)) short-circuit the retry budget immediately.
  • The helper does not catch or rewrap thrown values — callers see the underlying error. This keeps the helper independent of the package's error registry, which composes downstream.
  • Helper functions are intentionally inline-sized; the file stays under the 300-line hard limit.