from __future__ import annotations import asyncio from dataclasses import dataclass, field from functools import wraps from typing import Any, Callable, TypeVar from src.core.exceptions import ConnectorException from src.core.logging import get_logger logger = get_logger("connector_retry") T = TypeVar("T") DEFAULT_RETRYABLE_EXCEPTIONS = (ConnectionError, TimeoutError, OSError) @dataclass class RetryPolicy: max_attempts: int = 3 base_delay: float = 1.0 max_delay: float = 30.0 exponential_base: float = 2.0 jitter: bool = True retryable_exceptions: tuple[type[Exception], ...] = field(default_factory=lambda: DEFAULT_RETRYABLE_EXCEPTIONS) def delay_for_attempt(self, attempt: int) -> float: import random delay = min(self.base_delay * (self.exponential_base ** (attempt - 1)), self.max_delay) if self.jitter: delay = delay * (0.5 + random.random() * 0.5) return delay def should_retry(self, exception: Exception) -> bool: if isinstance(exception, ConnectorException): return False return isinstance(exception, self.retryable_exceptions) async def with_retry( func: Callable[..., Any], *args: Any, policy: RetryPolicy | None = None, connector_type: str = "unknown", operation: str = "unknown", **kwargs: Any, ) -> Any: p = policy or RetryPolicy() last_exception: Exception | None = None for attempt in range(1, p.max_attempts + 1): try: result = await func(*args, **kwargs) if attempt > 1: logger.info("retry_succeeded", extra={"connector_type": connector_type, "operation": operation, "attempt": attempt}) return result except Exception as exc: last_exception = exc if not p.should_retry(exc) or attempt >= p.max_attempts: break delay = p.delay_for_attempt(attempt) logger.warning( "retry_attempt", extra={ "connector_type": connector_type, "operation": operation, "attempt": attempt, "delay_s": round(delay, 2), "error": str(exc), }, ) await asyncio.sleep(delay) raise last_exception # type: ignore[misc] def retry(policy: RetryPolicy | None = None): p = policy or RetryPolicy() def decorator(func: Callable[..., Any]) -> Callable[..., Any]: @wraps(func) async def wrapper(*args: Any, **kwargs: Any) -> Any: connector_type = getattr(args[0], "connector_type", "unknown") if args else "unknown" return await with_retry(func, *args, policy=p, connector_type=connector_type, operation=func.__name__, **kwargs) return wrapper return decorator