Как изменить декоратор retry, чтобы он работал с синхронным и асинхронным кодом в Python?
Что проверяют
Знаете ли вы, что асинхронную функцию нельзя обернуть обычной обёрткой: она вернёт корутину, исключение возникнет уже при await, и повторы не сработают. Следом спрашивают, как декоратор отличает корутинную функцию и чем time.sleep опасен внутри event loop.
Как отвечать
Каркас: внутри декоратора две обёртки, а нужная выбирается один раз, в момент декорирования, а не при каждом вызове.
- проверка типа:
inspect.iscoroutinefunction(func). Вариант изasyncioс Python 3.14 объявлен устаревшим, в новом коде беритеinspect; - асинхронная обёртка:
async def, внутриawait func(...)иawait asyncio.sleep(delay). Обычныйtime.sleepздесь заблокирует весь цикл событий вместе со всеми остальными задачами; - синхронная обёртка: та же логика с
time.sleep; functools.wraps: на обеих обёртках, чтобы сохранить имя, докстринг и__wrapped__.
Параметры стоит сделать общими: число попыток, начальная задержка, множитель и кортеж exceptions, при которых повтор вообще имеет смысл. Задержку растите экспоненциально и добавляйте случайный разброс (jitter), иначе сотня клиентов повторит запрос синхронно и добьёт упавший сервис. ValueError от неверного аргумента повторять бессмысленно.
После последней попытки пробрасывайте исходное исключение через raise, а не новое Exception с текстом. Иначе теряются тип и трейсбек, и вызывающий код не поймает конкретную ошибку. Спать после последней неудачи тоже не нужно.
В продакшене часто берут готовую tenacity, она умеет и sync, и async, но на собеседовании всё равно просят написать своё.
Где спотыкаются
Ловят голый except: или BaseException. Тогда под повтор попадают KeyboardInterrupt и asyncio.CancelledError, и отменённую задачу декоратор упорно перезапускает. Ловить нужно Exception или явный список.
Определяют тип функции на каждом вызове: вызывают её и смотрят, вернулась ли корутина. Это ещё и порождает корутину, которую никто не дождётся, с предупреждением never awaited.
Пишут декоратор с параметрами без третьего уровня вложенности, и @retry(times=3) падает, потому что retry получает функцию вместо числа.