ForcePilot/backend/package/yuxi/channels/auth/backoff.py

66 lines
1.7 KiB
Python
Raw Normal View History

from __future__ import annotations
import asyncio
import random
import time
from dataclasses import dataclass
@dataclass
class BackoffConfig:
base_seconds: float = 5.0
max_seconds: float = 300.0
jitter_pct: float = 0.2
multiplier: float = 2.0
class ExponentialBackoff:
def __init__(self, config: BackoffConfig | None = None):
self._config = config or BackoffConfig()
self._attempt = 0
self._last_attempt_time: float = 0
@property
def attempt(self) -> int:
return self._attempt
@property
def next_delay(self) -> float:
if self._attempt == 0:
return 0
base = min(
self._config.base_seconds * (self._config.multiplier ** (self._attempt - 1)),
self._config.max_seconds,
)
jitter = base * self._config.jitter_pct * (random.random() * 2 - 1)
return base + jitter
def reset(self) -> None:
self._attempt = 0
self._last_attempt_time = 0
def mark_attempt(self) -> None:
self._attempt += 1
self._last_attempt_time = time.monotonic()
async def wait(self) -> None:
delay = self.next_delay
if delay > 0:
await asyncio.sleep(delay)
async def execute(self, func, *args, **kwargs):
while True:
try:
self.mark_attempt()
return await func(*args, **kwargs)
except Exception as e:
if not _is_retryable(e):
raise
if self.next_delay >= self._config.max_seconds:
raise
await self.wait()
def _is_retryable(exception: Exception) -> bool:
return getattr(exception, "retryable", False)