Когда asyncio-задача “зависает”, стектрейс часто пуст или уводит в Event Loop. В production с многотысячными коннектами это тихая катастрофа: задача не падает, но и не завершается, ресурсы утекают. Разбираем три приёма, которые превращают отладку из гадания в детерминированный процесс.
TaskGroup и asyncio.timeout: границы времени жизни
С
asyncio.TaskGroup (Python 3.11+) каждая задача имеет явный контекст. Комбинируя его с asyncio.timeout, получаем детектор зависаний:async def safe_polling():
async with asyncio.TaskGroup() as tg:
async with asyncio.timeout(5.0):
task = tg.create_task(long_pipeline())
Плюс: при превышении лимита –
TimeoutError с отменой корутины. Минус: нужно явно оборачивать каждую группу.Timeout-декоратор: защита на уровне функции
Для всех внешних вызовов (API, базы, очереди) декоратор автоматически ставит таймаут:
import asyncio
from functools import wraps
def timeout(max_time: float):
def decorator(coro):
@wraps(coro)
async def wrapper(*args, **kwargs):
try:
return await asyncio.wait_for(
coro(*args, **kwargs), timeout=max_time)
except asyncio.TimeoutError:
log.warning(f"{coro.__name__} exceeded {max_time}s")
raise
return wrapper
return decorator
@timeout(3.0)
async def fetch_external_data(): ...
asyncio.wait_for корректно отменяет корутину, не оставляя её в состоянии “in progress”.Context Vars для трейсинга: кто вызвал задачу
contextvars.ContextVar хранит идентификатор запроса или таски. При таймауте логгер выводит полную цепочку:request_id = contextvars.ContextVar('request_id', default=None)
async def handler(call_id: str):
request_id.set(call_id)
async with asyncio.TaskGroup() as tg:
tg.create_task(process())В логах видно
request_id зависшей задачи — это ключ к поиску в трейсинге (OpenTelemetry, Jaeger).Что ещё проверить
* Блокирующий синхронный код (
requests.get вместо aiohttp)?* Забытый
await – asyncio пускает корутину без ошибки.* Обилие таймаутов на разных уровнях: один для HTTP, другой для всей группы.
Вывод:
Замороженные asyncio-задачи отлавливаются только комбинацией явных границ времени (TaskGroup + timeout) и трейсинга исполнения (context vars), а не надеждой на “авось завершится”.