← все задачи
Async Python · задача 3 из 10
Таймаут и отмена
Средний
15–20 минут
wait_forCancelledErrorочистка ресурсов
Условие
Напишите функцию, которая вызывает медленную операцию с таймаутом. Если операция не уложилась, вернуть значение по умолчанию, а саму операцию корректно отменить и освободить занятые ей ресурсы.
Что требуется
- Таймаут задаётся параметром
- После таймаута операция действительно останавливается, а не продолжает работать в фоне
- Ресурсы (соединение, файл, блокировка) освобождаются
Пример
result = await with_timeout(slow_call(), timeout=2, default=None) # вернёт результат или None через 2 секунды
Сначала уточните
Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.
- Что важнее при таймауте: вернуть значение по умолчанию или пробросить ошибку наверх?
- Операция идемпотентна? Если это списание денег, «отменил, но оно выполнилось» — большая проблема
- Таймаут на одну операцию или общий на всю цепочку?
Показать решение Скрыть решение
Решение
import asyncio
import logging
logger = logging.getLogger(__name__)
async def with_timeout(coro, timeout, default=None):
try:
return await asyncio.wait_for(coro, timeout=timeout)
except asyncio.TimeoutError:
logger.warning("операция не уложилась в %sс", timeout)
return default
# Как правильно писать саму операцию, чтобы её можно было отменить:
async def slow_call(session, url):
try:
async with session.get(url) as response:
return await response.json()
except asyncio.CancelledError:
logger.info("запрос к %s отменён", url)
raise # обязательно пробрасываем дальше
finally:
# сюда попадаем и при отмене: закрываем то, что открыли
pass
# Python 3.11+: таймаут на блок целиком
async def with_timeout_block(session, urls, timeout):
async with asyncio.timeout(timeout):
return [await slow_call(session, url) for url in urls]
Почему так
Как устроена отмена в asyncio
- wait_for по истечении времени вызывает cancel у задачи: внутрь корутины на ближайшем await бросается CancelledError
- Это обычное исключение, поэтому отработают все finally и async with — ресурсы закроются
- Прерывание возможно только на точках await: блокирующий вызов посреди корутины отменить нельзя, он доработает до конца
Почему CancelledError нужно пробрасывать
- Если перехватить и не пробросить, задача формально продолжит выполнение, а вызывающий уже считает её отменённой
- В 3.8+ CancelledError наследуется от BaseException именно для того, чтобы его не ловил случайный except Exception
- Ловить его допустимо только ради логирования и очистки — и сразу raise
Почему таймаут не означает «операция не выполнилась»
- Запрос мог дойти до сервера и быть обработан — вы просто не дождались ответа
- Для неидемпотентных операций это означает возможное двойное выполнение при повторе
- Отсюда ключ идемпотентности в платёжных API: это ровно та причина, по которой он существует
Что спросят дальше
- Спросят про shield: он защищает критическую часть от отмены — и тут же спросят, когда это оправдано
- Спросят разницу wait_for и asyncio.timeout: второй ограничивает блок, а не одну корутину, и не заворачивает её в задачу
- Спросят, как отменить блокирующий вызов в to_thread — правильный ответ: никак, поток доработает
Следующая задача
Частичный успех: что делать с ошибками в gather — Нужно вернуть удачные результаты и внятно отчитаться по неудачным, а не упасть целиком.