← все задачи
Async Python · задача 6 из 10
Повторы для асинхронного вызова
Средний
15 минут
декораторыasyncio.sleepотмена
Условие
Напишите асинхронный декоратор retry: повторяет корутину при сетевых ошибках, пауза растёт, отмена во время паузы должна работать мгновенно.
Что требуется
- Работает с любой корутиной
- Пауза между попытками растёт экспоненциально
- Отмена задачи во время паузы прерывает ожидание немедленно
Пример
@async_retry(attempts=3, delay=0.5, exceptions=(aiohttp.ClientError, asyncio.TimeoutError))
async def fetch(session, url):
async with session.get(url) as response:
return await response.json()
Сначала уточните
Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.
- Какие исключения считаем временными: только сетевые или ещё и 5xx?
- Нужен ли общий бюджет времени на все попытки, а не только число попыток?
- Идемпотентна ли операция — можно ли её вообще повторять?
Показать решение Скрыть решение
Решение
import asyncio
import functools
import random
def async_retry(attempts=3, delay=0.5, backoff=2, max_delay=30,
exceptions=(Exception,), jitter=True):
def decorator(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
current_delay = delay
for attempt in range(1, attempts + 1):
try:
return await func(*args, **kwargs)
except asyncio.CancelledError:
raise # отмена — не повод повторять
except exceptions:
if attempt == attempts:
raise
pause = min(current_delay, max_delay)
if jitter:
pause *= random.uniform(0.5, 1.5)
await asyncio.sleep(pause) # не блокирует цикл событий
current_delay *= backoff
return wrapper
return decorator
Почему так
Почему asyncio.sleep, а не time.sleep
- time.sleep блокирует весь поток: встанет не только эта корутина, а весь цикл событий вместе со всеми соединениями
- На одном воркере это означает, что сервис перестал отвечать всем клиентам на время паузы
- asyncio.sleep — точка await: цикл событий в это время обслуживает остальных
Почему CancelledError перехвачен отдельно
- Если в exceptions попадёт Exception, а CancelledError не выделить, отмена превратится в очередную попытку
- Отменённая задача обязана завершиться, а не начать новый запрос с паузой
- В 3.8+ CancelledError не наследуется от Exception, но явная ветка делает намерение читаемым и защищает от except BaseException
Почему отмена во время паузы срабатывает мгновенно
- await asyncio.sleep — точка, в которой корутина отдаёт управление, значит туда можно доставить CancelledError
- С time.sleep отмена ждала бы конца паузы: задача считается отменённой, а поток всё ещё спит
- Это хороший ответ на вопрос «чем плох блокирующий вызов в асинхронном коде», кроме очевидной потери конкурентности
Что спросят дальше
- Спросят про общий бюджет: deadline на все попытки честнее, чем число попыток, — особенно если сверху уже есть таймаут запроса
- Спросят про 429 и Retry-After: сервер сам говорит, сколько ждать, и это важнее вашей экспоненты
- Спросят, чем плохи повторы под нагрузкой: ретраи умножают трафик на упавший сервис — отсюда circuit breaker
Следующая задача
Блокирующий код внутри асинхронного — Любимый вопрос: в асинхронный обработчик попал синхронный вызов — что произошло и как чинить.