Программирование с ИИ
ENRU
6.4

Асинхронность на практике

таймауты, параллельные запросы и как это ломается

Что это

Статья 2.10 объясняла, что такое async и await. Здесь — три вещи, которые нужны в реальном коде и которые ИИ пишет неправильно чаще всего: параллельный запуск, обработка ошибок при нём и таймауты.

Тема практическая: почти весь асинхронный код в боте или сервисе состоит из этих трёх вещей.

Как выглядит в коде

import asyncio


async def fetch(city: str, delay: float) -> str:
    await asyncio.sleep(delay)
    if city == "Bergen":
        raise RuntimeError("сервер вернул 500")
    return f"{city}: +4"

Параллельный запуск и его ловушка

try:
    await asyncio.gather(fetch("Oslo", 0.1), fetch("Bergen", 0.1))
except RuntimeError as e:
    print("gather упал целиком:", e)
измерено
gather упал целиком: сервер вернул 500

Вот что здесь важно: одна неудача убила весь набор. Запрос к Осло прошёл успешно, но его результат потерян — gather поднял первое исключение и выбросил остальное.

Для бота, который опрашивает десять городов, это значит: один недоступный сервер — и пользователь не получает ничего.

Как надо

results = await asyncio.gather(
    fetch("Oslo", 0.1), fetch("Bergen", 0.1), fetch("Tromsø", 0.1),
    return_exceptions=True,
)
for r in results:
    print("  ошибка:" if isinstance(r, Exception) else "  ок:", r)
измерено
  ок: Oslo: +4
  ошибка: сервер вернул 500
  ок: Tromsø: +4

return_exceptions=True превращает исключения в обычные элементы списка. Теперь успешные результаты сохранены, а неудачные можно обработать по отдельности.

Плата за это — проверка isinstance(r, Exception) при разборе. Без неё исключение уйдёт дальше как обычное значение и всплывёт в неожиданном месте.

Таймауты

Запрос, который не вернётся никогда, остановит обработчик навсегда:

try:
    await asyncio.wait_for(fetch("Oslo", 2.0), timeout=0.3)
except TimeoutError:
    print("не дождались за 0.3 с")
измерено
не дождались за 0.3 с

Любой сетевой вызов должен иметь таймаут. Библиотеки обычно дают свой (aiohttp.ClientTimeout), но если его не задать явно, значение по умолчанию может оказаться минутами.

Три правила

ПравилоПочему
Независимые запросы — через gatherвтрое быстрее на трёх запросах (статья 2.10)
gather — с return_exceptions=Trueодна ошибка не должна терять остальные
Любой сетевой вызов — с таймаутоминаче обработчик висит вечно

Как сказать ИИ

Запрос
Эти три запроса независимы. Сделай их через asyncio.gather
с return_exceptions=True, разбери результаты: успешные собери
в список, неудачные запиши в лог с названием города.
Запрос
Добавь таймаут 5 секунд на каждый сетевой вызов.
При таймауте — предупреждение в лог и пропуск этого города,
а не падение всей команды.

Проверка готового кода:

Запрос
Что произойдёт в этом коде, если один из трёх серверов
не ответит? А если не ответит вообще никогда?

Второй вопрос ловит отсутствие таймаута — то, чего не видно ни при запуске, ни в тестах.

На что смотреть

gather без return_exceptions. Главный признак. Проверяется взглядом.

Нет таймаута. Ищи ClientTimeout, timeout=, wait_for. Если ничего нет — таймаута нет.

Цикл с await вместо gather. Запросы идут по очереди, хотя могли бы разом. Не ошибка, но втрое медленнее (статья 2.10).

Синхронная библиотека внутри async def. requests, time.sleep, обычный open — блокируют всё. Асинхронные замены: aiohttp, httpx, aiosqlite, aiofiles.

asyncio.run внутри уже запущенного цикла. Ошибка RuntimeError: asyncio.run() cannot be called from a running event loop означает, что код скопирован из примера для скрипта в обработчик бота.

Задача создана, но не дождались. asyncio.create_task(...) без await где-либо дальше: задача может не успеть выполниться до завершения программы.

Шпаргалка

  • Независимые запросы — через gather, и обязательно return_exceptions=True.

  • Без него одна ошибка теряет все успешные результаты.

  • Разбирая результаты gather, проверяй isinstance(r, Exception).

  • Любой сетевой вызов — с таймаутом, иначе обработчик висит вечно.

  • Проверочный вопрос: «что если один сервер не ответит никогда?»

  • requests и time.sleep внутри async def блокируют всё.