Типичная проблема в асинхронных middleware-цепях (FastAPI, aiohttp, Sanic) — race condition при записи в контекст: несколько корутин перезатирают
request_id друг друга, а утечка контекста после ошибки ломает последующие запросы. Решение — contextvars.ContextVar с asyncio.Lock для атомарности и finally для гарантированной очистки.Датакласс TraceContext с блокировкой
Храним
request_id, user_id и встроенный asyncio.Lock. Lock сериализует доступ к общему состоянию внутри одной middleware-цепи, исключая гонки.from contextvars import ContextVar
from dataclasses import dataclass, field
import asyncio
@dataclass
class TraceContext:
request_id: str = ''
user_id: int | None = None
_lock: asyncio.Lock = field(default_factory=asyncio.Lock, compare=False)
current_trace = ContextVar('current_trace', default=TraceContext())
Middleware с тайм-аутом и очисткой
Каждый middleware делает
set() один раз, Lock гарантирует, что параллельные корутины не испортят контекст. finally сбрасывает токен — утечка исключена. Тайм-аут в 30 секунд обрезает зависшие цепочки.async def tracing_middleware(request, call_next):
token = current_trace.set(TraceContext(request_id=str(uuid4())))
try:
async with asyncio.timeout(30):
async with current_trace.get()._lock:
return await call_next(request)
except asyncio.TimeoutError:
raise
finally:
current_trace.reset(token)
Чтение без прокидывания
AuthMiddleware и LoggingMiddleware читают
current_trace.get() — никаких параметров через request.state или сигнатуру. Lock не даёт двум параллельным задачам испортить один trace: если одна корутина ждёт I/O, другая не перезатрёт request_id.Вывод: Связка
contextvars + asyncio.Lock в middleware даёт потокобезопасный контекст с гарантированной очисткой и тайм-аутом, устраняя race conditions и утечки в асинхронных production-сервисах.