Skip to content

Commit 01c6033

Browse files
committed
Add retry logic with exponential backoff and rate limiting
1 parent 63d446b commit 01c6033

File tree

1 file changed

+106
-0
lines changed

1 file changed

+106
-0
lines changed

error_handling.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""
2+
Error handling utilities with retries and exponential backoff.
3+
"""
4+
5+
import asyncio
6+
import time
7+
from functools import wraps
8+
from typing import Any, Callable, Optional, TypeVar
9+
10+
T = TypeVar('T')
11+
12+
13+
async def retry_async(
14+
func: Callable[..., Any],
15+
max_retries: int = 3,
16+
initial_delay: float = 1.0,
17+
backoff_factor: float = 2.0,
18+
exceptions: tuple = (Exception,),
19+
) -> Any:
20+
"""
21+
Retry an async function with exponential backoff.
22+
23+
Args:
24+
func: Async function to retry.
25+
max_retries: Maximum number of retry attempts.
26+
initial_delay: Initial delay between retries in seconds.
27+
backoff_factor: Multiplier for delay after each retry.
28+
exceptions: Tuple of exception types to catch and retry.
29+
30+
Returns:
31+
Result of the function call.
32+
33+
Raises:
34+
Last exception if all retries are exhausted.
35+
"""
36+
delay = initial_delay
37+
last_exception = None
38+
39+
for attempt in range(max_retries + 1):
40+
try:
41+
return await func()
42+
except exceptions as e:
43+
last_exception = e
44+
if attempt < max_retries:
45+
print(f" Attempt {attempt + 1} failed: {e}. Retrying in {delay:.1f}s...")
46+
await asyncio.sleep(delay)
47+
delay *= backoff_factor
48+
else:
49+
print(f" All {max_retries + 1} attempts failed")
50+
raise last_exception
51+
52+
53+
def with_timeout(timeout_seconds: float):
54+
"""
55+
Decorator to add timeout to async functions.
56+
57+
Args:
58+
timeout_seconds: Timeout in seconds.
59+
60+
Returns:
61+
Decorated function that raises TimeoutError if exceeded.
62+
"""
63+
def decorator(func: Callable) -> Callable:
64+
@wraps(func)
65+
async def wrapper(*args, **kwargs):
66+
try:
67+
return await asyncio.wait_for(
68+
func(*args, **kwargs),
69+
timeout=timeout_seconds
70+
)
71+
except asyncio.TimeoutError:
72+
raise TimeoutError(f"{func.__name__} exceeded timeout of {timeout_seconds}s")
73+
return wrapper
74+
return decorator
75+
76+
77+
class RateLimiter:
78+
"""Simple rate limiter for API calls."""
79+
80+
def __init__(self, max_calls: int, time_window: float):
81+
"""
82+
Initialize rate limiter.
83+
84+
Args:
85+
max_calls: Maximum calls allowed in time window.
86+
time_window: Time window in seconds.
87+
"""
88+
self.max_calls = max_calls
89+
self.time_window = time_window
90+
self.calls = []
91+
92+
async def acquire(self):
93+
"""Wait if necessary to respect rate limit."""
94+
now = time.time()
95+
96+
self.calls = [t for t in self.calls if now - t < self.time_window]
97+
98+
if len(self.calls) >= self.max_calls:
99+
sleep_time = self.time_window - (now - self.calls[0])
100+
if sleep_time > 0:
101+
print(f" Rate limit: waiting {sleep_time:.1f}s...")
102+
await asyncio.sleep(sleep_time)
103+
now = time.time()
104+
self.calls = [t for t in self.calls if now - t < self.time_window]
105+
106+
self.calls.append(now)

0 commit comments

Comments
 (0)