| 1 | import asyncio |
| 2 | import random |
| 3 | import time |
| 4 | |
| 5 | |
| 6 | class RateLimiter: |
| 7 | def __init__(self, max_per_second: float = 2): |
| 8 | if max_per_second <= 0: |
| 9 | max_per_second = 2 |
| 10 | self.max_per_second = max_per_second |
| 11 | self.min_interval = 1.0 / max_per_second |
| 12 | self.last_request = 0.0 |
| 13 | self._lock = asyncio.Lock() |
| 14 | |
| 15 | async def acquire(self): |
| 16 | async with self._lock: |
| 17 | current = time.time() |
| 18 | time_since_last = current - self.last_request |
| 19 | |
| 20 | if time_since_last < self.min_interval: |
| 21 | wait_time = self.min_interval - time_since_last |
| 22 | await asyncio.sleep(wait_time) |
| 23 | |
| 24 | # Jitter must run inside the lock so that the next caller waits |
| 25 | # min_interval since the actual fire time, not since the prior |
| 26 | # caller acquired the lock. |
| 27 | await asyncio.sleep(random.uniform(0, 0.5)) |
| 28 | self.last_request = time.time() |
| 29 |