Для точного скользящего окна храните очередь времён допущенных вызовов по clientid. Перед новым вызовом удалите события старше окна и сравните оставшееся количество с limit.
Реализуйте rate limiter для обработчика: у каждого клиента есть client_id, limit и interval; нужно ограничить число вызовов handler для каждого клиента в скользящем временном окне и выбрасывать RateLimitExceededError.
Для точного скользящего окна храните очередь времён допущенных вызовов по clientid. Перед новым вызовом удалите события старше окна и сравните оставшееся количество с limit.
Короткий ответ
Что ответить на собеседовании
Подробный разбор
Ответ с пояснениями
Для точного скользящего окна храните очередь времён допущенных вызовов по client_id. Перед новым вызовом удалите события старше окна и сравните оставшееся количество с limit.
Пример для одного процесса и одного asyncio event loop. limit и interval задаются при создании лимитера и не меняются между вызовами:
import time
from collections import defaultdict, deque
class RateLimitExceededError(Exception):
pass
class RateLimiter:
def __init__(self, limit, interval):
if not isinstance(limit, int) or limit < 1 or interval <= 0:
raise ValueError('Invalid rate limit')
self.limit = limit
self.interval = interval
self.calls = defaultdict(deque)
async def call(self, client_id, handler, *args, **kwargs):
now = time.monotonic()
calls = self.calls[client_id]
cutoff = now - self.interval
while calls and calls[0] <= cutoff:
calls.popleft()
if len(calls) >= self.limit:
raise RateLimitExceededError(client_id)
calls.append(now)
return await handler(*args, **kwargs)
Проверка и резервирование не содержат await и в этой модели не перемежаются с другими корутинами. Считаются допущенные попытки, даже если handler завершился ошибкой. Для нескольких процессов нужен общий атомарный счётчик или хранилище; для долгой работы также нужна очистка состояния неактивных клиентов. Граница окна здесь — (now − interval, now].