← все задачи
Python · задача 6 из 10
Декоратор retry с экспоненциальной паузой
Средний
20 минут
декораторыfunctools.wrapsобработка ошибок
Условие
Напишите декоратор retry: он повторяет вызов функции при исключении заданных типов, делая паузу, которая растёт с каждой попыткой. После исчерпания попыток исключение пробрасывается наружу.
Что требуется
- Число попыток и стартовая пауза настраиваются
- Повторяем только указанные типы исключений, остальные пробрасываем сразу
- Имя и docstring исходной функции сохраняются
Пример
@retry(attempts=3, delay=0.5, exceptions=(ConnectionError,))
def fetch(url):
return requests.get(url, timeout=2).json()
# паузы между попытками: 0.5, 1.0 — потом исключение наружу
Сначала уточните
Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.
- Что делать с последней неудачей: пробрасывать исходное исключение или своё?
- Нужен ли джиттер — случайная добавка к паузе, чтобы клиенты не ретраились синхронно?
- Нужен ли предел паузы, чтобы она не выросла до минут?
Показать решение Скрыть решение
Решение
import functools
import random
import time
def retry(attempts=3, delay=0.5, backoff=2, max_delay=30, exceptions=(Exception,), jitter=True):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
current_delay = delay
for attempt in range(1, attempts + 1):
try:
return func(*args, **kwargs)
except exceptions:
if attempt == attempts:
raise # попытки кончились — отдаём исходную ошибку
pause = min(current_delay, max_delay)
if jitter:
pause *= random.uniform(0.5, 1.5)
time.sleep(pause)
current_delay *= backoff
return wrapper
return decorator
Почему так
Почему три уровня вложенности
- У декоратора есть параметры, поэтому нужен ещё один уровень: retry(...) возвращает декоратор, тот — обёртку
- Без параметров хватило бы двух уровней — об этом стоит сказать, объясняя структуру
- Просьба «сделай так, чтобы работало и с параметрами, и без» — частое продолжение; решается проверкой, что первый аргумент вызываемый
Зачем functools.wraps
- Без него fetch.__name__ станет "wrapper", а docstring исчезнет — сломаются логи, Sphinx и интроспекция
- Особенно больно с Django и Celery: они опираются на имя функции при регистрации задач
- Отсутствие wraps — первое, к чему придерётся интервьюер, и придирка справедливая
Почему пауза растёт и зачем джиттер
- Если сервис лежит, частые повторы добивают его — растущая пауза даёт время подняться
- Джиттер разводит клиентов во времени: без него сотня инстансов ретраится синхронно и создаёт всплеск нагрузки
- max_delay нужен, чтобы экспонента не выросла до минут на пятой попытке
Почему повторяем не всё подряд
- Повторять имеет смысл только временные ошибки: таймаут, обрыв соединения, 503
- Повтор на 400 или ValueError бессмысленен — ответ не изменится, вы просто утроите нагрузку
- Отдельная опасность — неидемпотентные операции: повтор списания денег недопустим, и про идемпотентность спросят обязательно
Что спросят дальше
- Спросят про async-версию: та же структура, но await func(...) и asyncio.sleep
- Спросят, почему не готовая библиотека (tenacity) — правильный ответ: в проде взял бы её, здесь показываю механику
- Могут попросить логировать попытки — покажите, что логируете номер попытки и тип ошибки, а не «сломалось»
Следующая задача
Контекстный менеджер для замера времени — Проверяют протокол with, поведение при исключении и знание contextlib.