"""Bounded retry with exponential backoff + jitter.""" from __future__ import annotations import random import time from typing import Callable, Optional, Tuple, Type def retry( fn: Callable, *, max_attempts: int = 3, base_delay: float = 0.1, max_delay: float = 10.0, retryable: Tuple[Type[BaseException], ...] = (Exception,), sleep: Callable[[float], None] = time.sleep, rng: Optional[random.Random] = None, ) -> object: if max_attempts < 1: raise ValueError("max_attempts must be >= 1") if base_delay < 0 or max_delay < 0: raise ValueError("delays must be >= 0") r = rng if rng is not None else random.Random() last_exc = None for attempt in range(max_attempts): try: return fn() except retryable as e: last_exc = e if attempt == max_attempts - 1: break exp = min(max_delay, base_delay * (2 ** attempt)) delay = r.uniform(0, exp) if exp > 0 else 0.0 sleep(delay) except BaseException: raise assert last_exc is not None raise last_exc