request_id перепутался между задачами Python: передаём контекст через contextvars
В журнале сервиса рядом стоят две строки: начало обработки заказа A и ошибка заказа B. У обеих один request_id. Сами заказы обработаны правильно, но по логам невозможно восстановить ход обращения. Частая причина — идентификатор хранится в обычной глобальной переменной, которую каждая корутина перезаписывает перед очередным await. Пока один запрос ждёт, другой успевает изменить общее значение.
Где возникает смешение
Представим обработчик, который записывает request_id, вызывает зависимость и после её ответа пишет результат в журнал. Между этими действиями цикл событий запускает другой обработчик. Глобальная переменная описывает последнее присваивание во всём процессе. Она никак не связана с тем, какой запрос сейчас продолжил работу. Локальная переменная остаётся безопасной, но её пришлось бы передавать через все промежуточные функции.
Для служебного контекста, который нужен журналированию глубоко в стеке, удобен ContextVar. Он хранит значение в текущем контексте выполнения. Это позволяет функции журнала получить идентификатор без дополнительного параметра в каждой функции расчёта. При этом бизнес-данные заказа разумно по-прежнему передавать явно: скрытое окружение не должно превращаться в обязательный источник параметров предметной логики.
Два обращения в одном цикле событий
import asyncio
from contextvars import ContextVar
request_id = ContextVar("request_id", default="-")
def log_line(message):
return f"{request_id.get()}: {message}"
async def handle(value, pause):
token = request_id.set(value)
try:
await asyncio.sleep(pause)
return log_line("done")
finally:
request_id.reset(token)
async def main():
result = await asyncio.gather(handle("A", 0.02), handle("B", 0))
assert result == ["A: done", "B: done"]
assert request_id.get() == "-"
print(result)
print(request_id.get())
asyncio.run(main())Результат содержит A: done и B: done в порядке аргументов gather, хотя B завершает ожидание раньше. После работы значение во внешнем контексте снова равно дефису. Token нужен для возврата предыдущего значения: обработчик может выполняться внутри другого контекста, где идентификатор уже был установлен. Безусловное присваивание дефиса в finally уничтожило бы это внешнее значение.
Подключение к настоящему запросу
Устанавливайте контекст на входной границе запроса и сбрасывайте в finally. Функция или фильтр журнала читает request_id.get() при формировании записи. Если идентификатор приходит от клиента, сначала ограничьте длину и допустимый формат; иначе одна строка может превратиться в несколько или занять неожиданно много места. Если входное значение неприемлемо, создайте своё и верните клиенту по принятому в API правилу.
ContextVar создавайте на уровне модуля, как в примере. Сами сохранённые значения лучше делать простыми и неизменяемыми. Если положить в контекст общий словарь, независимость контекстов не превратит этот словарь в отдельную копию для каждого запроса. Изменяемый объект может снова стать местом смешения. Используйте reset с полученным token для восстановления значения на выходе из обработчика.
Почему фоновые задачи требуют отдельного решения
Новая asyncio-задача обычно получает текущий контекст при создании. Поэтому задача, запущенная внутри обработки A, может продолжить писать A даже после ответа пользователю. Для короткого продолжения это бывает удобно. Для самостоятельной фоновой работы полезнее назначить job_id и явно записать связь с исходным запросом. Иначе по журналу будет казаться, что HTTP-запрос длится столько же, сколько многочасовая выгрузка.
При передаче задания в другой процесс или через брокер локального контекста уже нет. Нужные идентификаторы должны стать частью сообщения, а принимающая сторона установит свой контекст на время обработки. Сначала определите, какой идентификатор обозначает попытку обработки, а какой — само задание. Это позволит увидеть повторную доставку как несколько попыток одной работы, а не несколько независимых заявок.
Как убедиться, что исправлен журнал
Проверьте одновременную обработку двух разных идентификаторов, исключение внутри обработчика и вложенную установку значения. Важно проверить и конкретную функцию формирования лог-записи: если она захватила request_id один раз при создании logger, корректный контекст уже не поможет. Ищите чтение значения в момент записи, когда известно, какая задача сейчас исполняется. После этого свяжите записи входа, обращения к зависимости и результата по одному идентификатору.
Нередко ошибка обнаруживается только при включённой конкурентности, а одиночный тест проходит. Поэтому сохраните пример с двумя запросами как регрессионный тест. Добавьте в одну ветку исключение после await и убедитесь, что идентификатор другой ветки не изменился. Для вложенного вызова установите внешнее значение до handle и проверьте его после возврата. Эти сценарии моделируют повторное использование инфраструктуры журналирования гораздо лучше, чем проверка одного set и одного get подряд без переключения задач.
Для HTTP-входа пригодится разбор приёма и возврата X-Request-ID в FastAPI. А для связанных задач полезно понять поведение TaskGroup при ошибке: контекст объясняет принадлежность записи, но не управляет временем жизни дочерней работы.
Продолжить изучение асинхронного Python
В платном курсе «Python для профессионалов» есть цикл событий asyncio, асинхронная обработка веб-запросов, генераторы и менеджеры контекста. Вы отработаете устройство конкурентного кода, чтобы понимать место установки контекста и время жизни ресурсов. Contextvars здесь служит прикладной задачей для следующего шага после базового async/await. Откройте программу курса и перейдите к разделу асинхронного программирования.
Продолжить на практике
- Python для профессионалов ↗
Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.
