Source code for peyk.transport.retry
"""Generic retry wrapper for transport operations.
Retries only the exception types explicitly listed in `retryable`.
`HTTPStatusError` is never in the default set and must never be retried:
a non-2xx application-level response is not a transient transport
condition.
"""
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from typing import Awaitable, Callable, Optional, Tuple, Type, TypeVar
from .errors import NetworkError, RateLimitedError, TimeoutError_, TransportError
from .logging_hook import TransportLogger, get_default_logger
T = TypeVar('T')
DEFAULT_RETRYABLE: Tuple[Type[TransportError], ...] = (NetworkError, TimeoutError_, RateLimitedError)
[docs]
@dataclass(frozen=True)
class RetryPolicy:
"""Configuration for `run_with_retry`.
Attributes:
max_attempts: Total number of attempts (including the first),
not the number of retries.
base_backoff_seconds: Base delay for exponential backoff between
attempts, used when the raised error doesn't carry its own
hint (e.g. `RateLimitedError.retry_after_seconds`).
"""
max_attempts: int = 3
base_backoff_seconds: float = 0.5
[docs]
async def run_with_retry(operation: Callable[[], Awaitable[T]], policy: RetryPolicy, retryable: Tuple[Type[TransportError], ...]=DEFAULT_RETRYABLE, logger: Optional[TransportLogger]=None) -> T:
"""Performs the run with retry operation for the transport client.
Args:
operation: Value used by this operation.
policy: Value used by this operation.
retryable: Value used by this operation.
logger: Value used by this operation.
Returns:
Result produced by the transport operation."""
"Run `operation()`, retrying on the exception types in `retryable`.\n \n - Only exceptions that are instances of one of `retryable` are retried;\n anything else (including `HTTPStatusError`) propagates immediately.\n - When a `RateLimitedError` carries `retry_after_seconds`, that value\n is used as the delay instead of the exponential backoff.\n - Backoff between attempts is `base_backoff_seconds * 2 ** (attempt - 1)`\n for the 1-indexed attempt number, when no explicit hint is present.\n - After the final attempt, the exception is re-raised rather than\n swallowed.\n - `logger` (added in Phase A2) is called once per retry via\n `log_retry(attempt, exception)`, and exactly once via\n `log_failure(exception)` when this function gives up — whether that's\n because retries were exhausted or because the exception wasn't\n retryable in the first place. Defaults to the shared stdlib-backed\n logger, so existing callers that never pass `logger` are unaffected.\n \n \n Args:\n operation: Value of the declared parameter type.\n policy: Value of the declared parameter type.\n retryable: Value of the declared parameter type.\n logger: Value of the declared parameter type.\n \n \n Returns:\n The operation result (``T``).\n\n Raises:\n TransportError: Re-raised after retries are exhausted or when the\n exception is not retryable.\n "
logger = logger or get_default_logger()
attempt = 0
while True:
attempt += 1
try:
return await operation()
except retryable as exc:
if attempt >= policy.max_attempts:
logger.log_failure(exc)
raise
logger.log_retry(attempt, exc)
if isinstance(exc, RateLimitedError) and exc.retry_after_seconds is not None:
delay = exc.retry_after_seconds
else:
delay = policy.base_backoff_seconds * 2 ** (attempt - 1)
await asyncio.sleep(delay)
except Exception as exc:
logger.log_failure(exc)
raise