Source code for roost._core.retry

from __future__ import annotations

import random
from typing import Protocol


[docs] class BackoffStrategy(Protocol): """Maps an attempt number (1-indexed) to seconds-until-next-attempt.""" def __call__(self, attempt: int) -> float: ...
[docs] def exponential(base: float = 2.0, *, jitter: bool = True, cap: float = 24 * 60 * 60) -> BackoffStrategy: """``base ** attempt`` seconds, optionally jittered, capped at ``cap``. Defaults to Oban's behavior: ``2, 4, 8, 16, …`` capped at one day. """ def _strategy(attempt: int) -> float: delay = min(base ** max(attempt, 1), cap) if jitter: delay *= 0.5 + random.random() # noqa: S311 — jitter, not crypto return delay return _strategy
[docs] def linear(step: float = 60.0, *, jitter: bool = False) -> BackoffStrategy: """Constant linear growth: ``step * attempt`` seconds.""" def _strategy(attempt: int) -> float: delay = step * max(attempt, 1) if jitter: delay *= 0.5 + random.random() # noqa: S311 return delay return _strategy
[docs] def fixed(seconds: float = 60.0) -> BackoffStrategy: """Always wait ``seconds``.""" def _strategy(attempt: int) -> float: return seconds return _strategy
DEFAULT_STRATEGY: BackoffStrategy = exponential() def resolve(strategy: BackoffStrategy | None) -> BackoffStrategy: """Pick ``strategy`` if provided, otherwise the package default. Plain ``Callable[[int], float]`` callables are structurally compatible with :class:`BackoffStrategy` and may be passed directly. """ return strategy if strategy is not None else DEFAULT_STRATEGY