vibe/vibe/core/utils/retry.py
Mathias Gesbert 3f8487f761
v2.14.0 (#743)
Co-authored-by: Alexis Tacnet <alexis@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com>
Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Val <102326092+vdeva@users.noreply.github.com>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: p.vezia <166131032+le-codeur-rapide@users.noreply.github.com>
Co-authored-by: Hiba Chaabnia <Hiba-Chaabnia@users.noreply.github.com>
Co-authored-by: Nikhil Bhima <nikhilbhima@users.noreply.github.com>
Co-authored-by: Nkipohcs <Nkipohcs@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-06-04 18:26:35 +02:00

138 lines
4.8 KiB
Python

from __future__ import annotations
import asyncio
from collections.abc import AsyncGenerator, Awaitable, Callable
import functools
import logging
import httpx
logger = logging.getLogger("vibe")
_RETRYABLE_REQUEST_ERRORS: tuple[type[httpx.RequestError], ...] = (
httpx.TimeoutException,
httpx.ConnectError,
httpx.ReadError,
httpx.WriteError,
httpx.RemoteProtocolError,
)
def _is_retryable_http_error(e: Exception) -> bool:
if isinstance(e, httpx.HTTPStatusError):
return e.response.status_code in {408, 409, 425, 429, 500, 502, 503, 504, 529}
if isinstance(e, _RETRYABLE_REQUEST_ERRORS):
return True
return False
def async_retry[T, **P](
tries: int = 3,
delay_seconds: float = 0.5,
backoff_factor: float = 2.0,
is_retryable: Callable[[Exception], bool] = _is_retryable_http_error,
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
"""Args:
tries: Number of retry attempts
delay_seconds: Initial delay between retries in seconds
backoff_factor: Multiplier for delay on each retry
is_retryable: Function to determine if an exception should trigger a retry
(defaults to checking for retryable HTTP errors from both urllib and httpx)
Returns:
Decorated function with retry logic
"""
def decorator(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
@functools.wraps(func)
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
last_exc = None
for attempt in range(tries):
try:
return await func(*args, **kwargs)
except Exception as e:
last_exc = e
if attempt < tries - 1 and is_retryable(e):
current_delay = (delay_seconds * (backoff_factor**attempt)) + (
0.05 * attempt
)
logger.warning(
"Retrying %s after error attempt=%d/%d delay=%.2fs error=%r",
func.__qualname__,
attempt + 1,
tries,
current_delay,
e,
)
await asyncio.sleep(current_delay)
continue
raise e
raise RuntimeError(
f"Retries exhausted. Last error: {last_exc}"
) from last_exc
return wrapper
return decorator
def async_generator_retry[T, **P](
tries: int = 3,
delay_seconds: float = 0.5,
backoff_factor: float = 2.0,
is_retryable: Callable[[Exception], bool] = _is_retryable_http_error,
) -> Callable[[Callable[P, AsyncGenerator[T]]], Callable[P, AsyncGenerator[T]]]:
"""Retry decorator for async generators.
Args:
tries: Number of retry attempts
delay_seconds: Initial delay between retries in seconds
backoff_factor: Multiplier for delay on each retry
is_retryable: Function to determine if an exception should trigger a retry
(defaults to checking for retryable HTTP errors from both urllib and httpx)
Returns:
Decorated async generator function with retry logic
"""
def decorator(
func: Callable[P, AsyncGenerator[T]],
) -> Callable[P, AsyncGenerator[T]]:
@functools.wraps(func)
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> AsyncGenerator[T]:
last_exc = None
for attempt in range(tries):
generator = func(*args, **kwargs)
try:
first_item = await anext(generator)
except StopAsyncIteration:
return
except Exception as e:
last_exc = e
await generator.aclose()
if attempt < tries - 1 and is_retryable(e):
current_delay = (delay_seconds * (backoff_factor**attempt)) + (
0.05 * attempt
)
logger.warning(
"Retrying %s after error attempt=%d/%d delay=%.2fs error=%r",
func.__qualname__,
attempt + 1,
tries,
current_delay,
e,
)
await asyncio.sleep(current_delay)
continue
raise
yield first_item
async for item in generator:
yield item
return
raise RuntimeError(
f"Retries exhausted. Last error: {last_exc}"
) from last_exc
return wrapper
return decorator