Храним времена допущенных вызовов в deque и под Lock удаляем записи вне окна, проверяем лимит и резервируем новый вызов. Для измерения интервалов нужен monotonic(), а функцию выполняем уже вне блокировки.
Написать декоратор rate_limit(max_calls, period), который ограничивает число вызовов функции скользящим окном по времени: не более max_calls вызовов за любые последние period секунд. При превышении лимита вызов не должен выполняться. Учесть, что функцию могут вызывать из нескольких потоков.
Храним времена допущенных вызовов в deque и под Lock удаляем записи вне окна, проверяем лимит и резервируем новый вызов. Для измерения интервалов нужен monotonic(), а функцию выполняем уже вне блокировки.
Короткий ответ
Что ответить на собеседовании
Подробный разбор
Ответ с пояснениями
Условие
Написать декоратор rate_limit(max_calls, period), который ограничивает число вызовов функции скользящим окном по времени: не более max_calls вызовов за любые последние period секунд. При превышении лимита вызов не должен выполняться. Учесть, что функцию могут вызывать из нескольких потоков.
Пример: @rate_limit(max_calls=5, period=60) шестой вызов в течение минуты должен быть отклонён.
Ответ
Храним времена допущенных вызовов в deque и под Lock удаляем записи вне окна, проверяем лимит и резервируем новый вызов. Для измерения интервалов нужен monotonic(), а функцию выполняем уже вне блокировки.
В этом варианте окно имеет вид (now − period, now], отклонение вызывает исключение, а начавшийся вызов учитывается даже при ошибке функции.
from collections import deque
from functools import wraps
from math import isfinite
from threading import Lock
from time import monotonic
class RateLimitExceeded(Exception):
pass
def rate_limit(max_calls, period):
if type(max_calls) is not int or max_calls <= 0:
raise ValueError('max_calls must be positive')
if not isfinite(period) or period <= 0:
raise ValueError('period must be finite and positive')
def decorate(func):
calls = deque()
lock = Lock()
@wraps(func)
def wrapper(*args, **kwargs):
with lock:
now = monotonic()
while calls and calls[0] <= now - period:
calls.popleft()
if len(calls) >= max_calls:
raise RateLimitExceeded('Too many calls')
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorate
Лимит общий для одной декорированной функции в одном процессе. Для нескольких workers нужен общий координатор; для async-функций следует отдельно определить момент учёта фактического исполнения.