Написать декоратор rate_limit(max_calls, period), который ограничивает число вызовов функции скользящим окном по времени: не более max_calls вызовов за любые последние period секунд. При превышении лимита вызов не должен выполняться. Учесть, что функцию могут вызывать из нескольких потоков.

Храним времена допущенных вызовов в deque и под Lock удаляем записи вне окна, проверяем лимит и резервируем новый вызов. Для измерения интервалов нужен monotonic(), а функцию выполняем уже вне блокировки.

Короткий ответ

Что ответить на собеседовании

Храним времена допущенных вызовов в 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-функций следует отдельно определить момент учёта фактического исполнения.

ИИ-помощник для собеседований

Хочешь уверенно проходить собеседования?

Попробуй ИИ-помощник для собеседований: слышит вас и собеседника, анализирует экран, подсказывает ответы в реальном времени, работает без VPN и не попадает в захват экрана.

Подробнее