OPEN SOURCE
software/src/utils/retry.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
1"""Retry utilities with exponential backoff."""23from __future__ import annotations45import asyncio6import functools7import logging8import random9import time10from dataclasses import dataclass, field11from collections.abc import Awaitable, Coroutine12from typing import Any, Callable, TypeVar1314from src.exceptions import RateLimitError, TranslationError1516logger = logging.getLogger(__name__)1718T = TypeVar("T")192021@dataclass22class RetryConfig:23"""Configuration for retry behavior."""2425max_attempts: int = 326base_delay: float = 1.027max_delay: float = 60.028exponential_base: float = 2.029jitter: bool = True30retryable_exceptions: tuple[type[Exception], ...] = field(31default_factory=lambda: (RateLimitError, ConnectionError, TimeoutError)32)3334def get_delay(self, attempt: int) -> float:35"""Calculate delay for the given attempt number.3637Uses exponential backoff with optional jitter.3839Args:40attempt: The attempt number (0-indexed).4142Returns:43Delay in seconds before the next retry.44"""45delay = self.base_delay * (self.exponential_base**attempt)46delay = min(delay, self.max_delay)4748if self.jitter:49# Add random jitter of +/- 25%50jitter_range = delay * 0.2551delay += random.uniform(-jitter_range, jitter_range)5253return max(0, delay)545556def retry_with_backoff(57config: RetryConfig | None = None,58on_retry: Callable[[Exception, int], None] | None = None,59) -> Callable[[Callable[..., T]], Callable[..., T]]:60"""Decorator for retrying functions with exponential backoff.6162Args:63config: Retry configuration. Uses defaults if not provided.64on_retry: Optional callback called before each retry with (exception, attempt).6566Returns:67Decorator function.68"""69if config is None:70config = RetryConfig()7172def decorator(func: Callable[..., T]) -> Callable[..., T]:73@functools.wraps(func)74def wrapper(*args: Any, **kwargs: Any) -> T:75last_exception: Exception | None = None7677for attempt in range(config.max_attempts):78try:79return func(*args, **kwargs)80except config.retryable_exceptions as e:81last_exception = e8283# Don't sleep after the last attempt84if attempt < config.max_attempts - 1:85# Check for rate limit specific retry-after86if isinstance(e, RateLimitError) and e.retry_after:87delay = e.retry_after88else:89delay = config.get_delay(attempt)9091logger.warning(92"Attempt %d/%d failed: %s. Retrying in %.1fs...",93attempt + 1,94config.max_attempts,95str(e),96delay,97)9899if on_retry:100on_retry(e, attempt)101102time.sleep(delay)103except Exception:104# Non-retryable exception, re-raise immediately105raise106107# All attempts exhausted108if last_exception:109raise last_exception110raise TranslationError("All retry attempts failed")111112return wrapper113114return decorator115116117def async_retry_with_backoff(118config: RetryConfig | None = None,119on_retry: Callable[[Exception, int], None] | None = None,120) -> Callable[[Callable[..., T]], Callable[..., T]]:121"""Async decorator for retrying functions with exponential backoff.122123Args:124config: Retry configuration. Uses defaults if not provided.125on_retry: Optional callback called before each retry with (exception, attempt).126127Returns:128Decorator function for async functions.129"""130if config is None:131config = RetryConfig()132133def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Coroutine[Any, Any, T]]:134@functools.wraps(func)135async def wrapper(*args: Any, **kwargs: Any) -> T:136last_exception: Exception | None = None137138for attempt in range(config.max_attempts):139try:140return await func(*args, **kwargs)141except config.retryable_exceptions as e:142last_exception = e143144# Don't sleep after the last attempt145if attempt < config.max_attempts - 1:146# Check for rate limit specific retry-after147if isinstance(e, RateLimitError) and e.retry_after:148delay = e.retry_after149else:150delay = config.get_delay(attempt)151152logger.warning(153"Attempt %d/%d failed: %s. Retrying in %.1fs...",154attempt + 1,155config.max_attempts,156str(e),157delay,158)159160if on_retry:161on_retry(e, attempt)162163await asyncio.sleep(delay)164except Exception:165# Non-retryable exception, re-raise immediately166raise167168# All attempts exhausted169if last_exception:170raise last_exception171raise TranslationError("All retry attempts failed")172173return wrapper174175return decorator176177178class RetryState:179"""Tracks retry state for manual retry loops."""180181def __init__(self, config: RetryConfig | None = None) -> None:182"""Initialize retry state.183184Args:185config: Retry configuration.186"""187self.config = config or RetryConfig()188self.attempt = 0189self.last_exception: Exception | None = None190191def should_retry(self, exception: Exception) -> bool:192"""Check if we should retry after an exception.193194Args:195exception: The exception that was raised.196197Returns:198True if we should retry, False otherwise.199"""200self.last_exception = exception201202if not isinstance(exception, self.config.retryable_exceptions):203return False204205if self.attempt >= self.config.max_attempts - 1:206return False207208return True209210def wait(self) -> None:211"""Wait before the next retry attempt."""212if isinstance(self.last_exception, RateLimitError):213delay = self.last_exception.retry_after or self.config.get_delay(self.attempt)214else:215delay = self.config.get_delay(self.attempt)216217time.sleep(delay)218self.attempt += 1219220async def async_wait(self) -> None:221"""Async wait before the next retry attempt."""222if isinstance(self.last_exception, RateLimitError):223delay = self.last_exception.retry_after or self.config.get_delay(self.attempt)224else:225delay = self.config.get_delay(self.attempt)226227await asyncio.sleep(delay)228self.attempt += 1229230def reset(self) -> None:231"""Reset retry state for a new operation."""232self.attempt = 0233self.last_exception = None234