Watch at EaseHistorias. Respuestas. Perspectiva.

Tecnología · GUÍA PRÁCTICA

¿ContextVar desaparece en un ejecutor de Python? Revise la propagación y el retorno de cambios

Distinga Task, to_thread, hilos y procesos. Copie el contexto en cada envío a un hilo y devuelva los cambios explícitamente.

Diagrama original de la propagación de ContextVar entre Task, to_thread y ejecutores
Ilustración: identifique primero el límite; la API de llamada determina la propagación. Watch at Ease

ContextVar no atraviesa del mismo modo todos los límites de concurrencia. Las tareas de asyncio conservan su contexto y asyncio.to_thread copia el Context actual al hilo de trabajo. Un envío directo a ThreadPoolExecutor o una llamada a loop.run_in_executor necesita una copia explícita cuando la biblioteca que lo envuelve no la proporciona.

Propagar el contexto hacia un hilo y devolver sus cambios son operaciones distintas. Capture una copia nueva en cada envío y ejecute una llamada dentro de ella. Devuelva los cambios como datos, trate por separado los procesos y compruebe el aislamiento con dos solicitudes concurrentes.

1. Identifique primero el límite de ejecución

Registre si la llamada permanece en una Task de asyncio, usa asyncio.to_thread, llama a loop.run_in_executor, se envía directamente a ThreadPoolExecutor o pasa a otro proceso. No deduzca el comportamiento solo por el nombre de la función o de la biblioteca.

Registre un identificador de solicitud sin datos secretos a ambos lados del límite. Su ausencia indica que la propagación falló; un valor de la solicitud anterior revela estado residual en el hilo. Si el valor esperado aparece solo después de copiarlo explícitamente, esa envoltura es la responsable de propagarlo.

2. Copie el contexto en cada envío al hilo

Al enviar directamente una tarea a un hilo, ejecute ctx=contextvars.copy_context() en la solicitud que la origina y envíe ctx.run junto con la función y sus argumentos. Capture el contexto en cada envío, no solo al crear un grupo de hilos de larga duración.

No haga que dos hilos entren a la vez en el mismo Context: Python genera RuntimeError si ya está en uso. Una copia nueva por llamada también conserva el estado de la solicitud correspondiente, en vez del estado que existía al iniciar el grupo.

3. Separe la propagación del retorno de cambios

asyncio.to_thread proporciona propagación hacia el hilo de trabajo. Una función auxiliar de la biblioteca puede envolver run_in_executor del mismo modo. Eso no convierte una asignación de ContextVar en el hilo en una actualización automática de la Task que espera su resultado.

Si el hilo calcula un nuevo identificador de inquilino, tramo de trazado o resultado de autorización, devuélvalo como datos normales. Quien hizo la llamada decidirá si establece o restaura su propio ContextVar. Para un cambio temporal, conserve el token de set() y llame a reset() en try/finally.

4. Distinga los hilos de los procesos

Una razón expuesta en la discusión de CPython para no dar a run_in_executor una regla automática única fue que también admite ProcessPoolExecutor, mientras que Context no puede serializarse con pickle. No aplique a un proceso una envoltura diseñada solo para hilos.

Pase a otro proceso un contexto de solicitud pequeño y serializable, valídelo allí y devuelva los resultados explícitamente. Las credenciales y los objetos mutables grandes deben seguir las reglas de seguridad y propiedad de la API de procesos, sin tratarlos como estado ContextVar disponible en cualquier lugar.

5. Compruebe el aislamiento en la ruta real

Lance dos llamadas solapadas con los identificadores A y B mediante la función auxiliar que usa en producción. Utilice una barrera para mantener activos ambos hilos. Compruebe que A solo ve A, B solo ve B y la llamada de control sin envoltura recibe el valor predeterminado documentado.

Haga que cada hilo establezca un valor local diferente y lo devuelva. Tras await o Future.result(), verifique que quien hizo la llamada conserva su identificador original hasta que lo cambie o restaure deliberadamente. Repita la comprobación en las rutas de error y cierre para evitar valores residuales de solicitudes anteriores.

Diagrama original de copia del contexto, ejecución con ctx.run y aislamiento de dos solicitudes
Ilustración: use una copia nueva por llamada al hilo y devuelva los cambios explícitamente. Watch at Ease