Rate limiter на Python: декоратор со скользящим окном запросов

Напишите решение на Python и проверьте его на странице.

Другие упражнения по этой теме: Декораторы Python: задачи на retry, rate limiter, кэш и Circuit Breaker.

Скользящее окно запросов

Модуль взаимодействия с внешним платёжным шлюзом защищает целевую операцию от перегрузки с помощью скользящего окна вызовов. Ограничение накладывается параметризованным декоратором, который настраивается допустимым лимитом обращений, длительностью временного окна и источником монотонных часов, сохраняя метаданные функции через wraps и поддерживая для каждой декорированной функции собственную изолированную историю слотов.

В момент очередного запроса декоратор считывает текущее время и освобождает устаревшие слоты: из очереди удаляются только те временные метки, для которых разность между текущим временем и отметкой строго больше интервала окна. Если эта разность в точности равна длительности окна, метка всё ещё считается активной.

Если после проверки число занятых слотов оказывается строго меньше лимита, новое время сразу фиксируется в очереди и декоратор запускает целевую функцию. Слот остаётся занятым даже в том случае, если целевая операция завершилась ошибкой, а само возникшее исключение беспрепятственно пробрасывается дальше. Когда свободных мест нет, функция вовсе не вызывается: декоратор немедленно возбуждает RuntimeError, передавая в него точную паузу до освобождения старейшего слота — разность между моментом старейшей метки плюс интервал окна и текущим временем.

Реализуйте такой декоратор частотного ограничения вызовов для надёжного контроля сетевых запросов.

Что уже дано

В заготовке объявлена функция-фабрика window_limiter(limit, interval, clock), которую вам предстоит реализовать.

Вспомогательные классы и функции для тестирования работы декоратора доступны в тестовом окружении во время выполнения проверки; объявлять их заново в своём коде не требуется.

Что нужно сделать

Реализуйте функцию window_limiter(limit, interval, clock).

Параметры фабрики:

  • limit — положительное целое число от 1 до 20, задающее максимальное количество вызовов внутри скользящего окна;
  • interval — положительное конечное число (целое или с плавающей точкой) от 0.25 до 1 000 000, определяющее длительность временного окна;
  • clock — вызываемый объект без аргументов, возвращающий конечное неубывающее числовое время (int или float).

Функция window_limiter возвращает декоратор, принимающий целевую синхронную функцию и возвращающий функцию-обёртку.

Правила работы обёртки:

  • Функция-обёртка должна принимать произвольные позиционные и именованные аргументы (*args, **kwargs) и полностью сохранять метаданные декорируемой функции: атрибуты __name__, __doc__, __annotations__, __module__, __qualname__, сигнатуру и ссылку на исходную функцию в __wrapped__ (например, через functools.wraps).
  • При повторном применении одного и того же возвращённого декоратора к нескольким разным функциям (от 1 до 3 функций) для каждой функции должна вестись строго отдельная независимая история временных меток.
  • Источник времени clock() опрашивается ровно один раз при каждом поступлении вызова обёртки (включая отклонённые вызовы). При вызове фабрики window_limiter или в момент декорирования функции опрос часов производиться не должен.
  • Перед проверкой доступности слотов из истории удаляются только те метки, чей возраст строго больше интервала: now - timestamp > interval. Если now - timestamp == interval, слот остаётся занятым.
  • Если количество оставшихся активных меток строго меньше limit, зафиксированное время now сразу добавляется в историю слотов, после чего вызывается целевая функция с переданными аргументами. Не фиксируйте время завершения функции, не используйте sleep и не повторяйте вызов автоматически.
  • Слот остаётся занятым независимо от исхода целевой функции. Если целевая функция завершилась ошибкой, исходный объект исключения должен всплыть дальше без изменений.
  • Если лимит исчерпан, целевая функция не вызывается. Обёртка немедленно выбрасывает исключение RuntimeError(delay) (или совместимый подкласс), где единственный аргумент — число oldest_timestamp + interval - now (включая точный 0.0, если возраст старейшей метки совпал с размером окна).
  • Целевая функция во время исполнения может продвигать часы или рекурсивно вызывать собственную обёртку: слот текущего вызова уже должен быть зафиксирован к началу её работы.

Ввод и вывод

Стандартные потоки ввода stdin и вывода stdout в решении не используются: считывать ввод и вызывать print() не нужно. После выполнения кода учащегося проверяющая система считывает из пространства имён модуля функцию верхнего уровня window_limiter и вызывает её с проверочными аргументами.

Пример использования

now = [0.0]
def clock():
    return now[0]
@window_limiter(1, 2.0, clock)
def operation(value):
    return value * 2
print(operation(3))
now[0] = 2.0
try:
    operation(4)
except RuntimeError as error:
    print(error.args[0])
now[0] = 2.25
print(operation(4))

Вывод:

6
0.0
8
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Связанные задачи