34 lines
1.1 KiB
Python
34 lines
1.1 KiB
Python
|
|
import asyncio
|
||
|
|
import logging
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
MAX_RETRIES = 3
|
||
|
|
BASE_DELAY = 2.0
|
||
|
|
|
||
|
|
|
||
|
|
async def retry_with_backoff(coro_factory, max_retries: int = MAX_RETRIES):
|
||
|
|
for attempt in range(max_retries + 1):
|
||
|
|
try:
|
||
|
|
resp = await coro_factory()
|
||
|
|
if resp.status_code == 429:
|
||
|
|
retry_after = resp.headers.get("Retry-After", str(BASE_DELAY * (2**attempt)))
|
||
|
|
wait = float(retry_after)
|
||
|
|
logger.warning("clickup rate limited (429), retry after %.1fs, attempt %d", wait, attempt + 1)
|
||
|
|
await asyncio.sleep(wait)
|
||
|
|
continue
|
||
|
|
return resp
|
||
|
|
except httpx.HTTPStatusError:
|
||
|
|
if attempt < max_retries:
|
||
|
|
await asyncio.sleep(BASE_DELAY * (2**attempt))
|
||
|
|
else:
|
||
|
|
raise
|
||
|
|
except Exception:
|
||
|
|
if attempt < max_retries:
|
||
|
|
await asyncio.sleep(BASE_DELAY * (2**attempt))
|
||
|
|
else:
|
||
|
|
raise
|
||
|
|
raise RuntimeError("clickup rate limit exceeded after max retries")
|