from __future__ import annotations import asyncio import random from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import Any from yuxi.utils.logging_config import logger @dataclass class RetryConfig: max_retries: int = 3 base_delay: float = 1.0 max_delay: float = 60.0 backoff_factor: float = 2.0 jitter: bool = True retryable_exceptions: tuple[type[Exception], ...] = (Exception,) async def with_retry( func: Callable[[], Awaitable[Any]], config: RetryConfig | None = None, ) -> Any: cfg = config or RetryConfig() last_exception: Exception | None = None for attempt in range(cfg.max_retries + 1): try: return await func() except cfg.retryable_exceptions as e: last_exception = e if attempt == cfg.max_retries: break delay = min(cfg.base_delay * (cfg.backoff_factor**attempt), cfg.max_delay) if cfg.jitter: delay *= random.uniform(0.5, 1.5) logger.debug(f"[SDK/Retry] Attempt {attempt + 1}/{cfg.max_retries} failed: {e}, retrying in {delay:.2f}s") await asyncio.sleep(delay) raise last_exception # type: ignore[misc]