Tema representativo de entrevista

Entrevista técnica: ¿Cuándo debería Python asyncio utilizar un eager task factory?

CodingDifícil
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Un servicio asyncio llama a muchas corrutinas cacheadas. ¿Debería habilitar asyncio.eager_task_factory? Explica el orden de ejecución, las excepciones, la cancelación, TaskGroup, la compatibilidad de versiones, el despliegue y la reversión.

Planteamiento y contexto

Un servicio asíncrono a menudo responde solicitudes desde una caché en memoria sin realizar E/S; aun así, cada corrutina se convierte en una tarea y espera la planificación del bucle de eventos. El equipo propone configurar asyncio.eager_task_factory de forma global. Evalúa el balance costo-beneficio y propone un despliegue seguro.

La documentación de Python indica que la ejecución eager inicia una corrutina de forma síncrona mientras se construye su Task; la Task se planifica únicamente cuando la corrutina se bloquea. La API se añadió en Python 3.12 y modifica la semántica observable de planificación.

Qué evalúa el entrevistador

La clave está en distinguir la finalización síncrona del bloqueo asíncrono y explicar los efectos en la equidad (fairness), el orden, el momento de las excepciones, la cancelación y TaskGroup. Las respuestas sólidas restringen la optimización a límites medidos e incluyen comprobaciones de versión, feature flags, reversión y métricas del bucle de eventos.

Preguntas clarificatorias iniciales

  • ¿Cuál es la versión mínima de Python en todos los despliegues?
  • ¿Son estas corrutinas lecturas de caché en memoria o pueden realizar E/S de red, base de datos, archivos o bloqueos (locks)?
  • ¿Depende el código del orden de creación de tareas, de la equidad del bucle o del orden de call_soon?
  • ¿El manejo de fallos, la cancelación y los plazos límite (deadlines) pertenecen a un TaskGroup o al alcance de la solicitud?
  • ¿El objetivo es el uso de CPU al crear tareas, la latencia de cola (tail latency) o el throughput, y cuál es la línea base?

Estructura de respuesta en 30 segundos

“No la habilitaría globalmente de entrada. Un eager factory inicia una corrutina durante la construcción de la Task, por lo que un acierto en caché puede evitar un turno de planificación en el bucle de eventos; una corrutina que se bloquea se planifica normalmente. Esto cambia el orden, el momento de las excepciones y la equidad: un bucle que crea muchas tareas síncronas puede retrasar temporizadores y otras solicitudes. Yo verificaría la versión de Python, la desplegaría en puntos de llamada de caché medidos, compararía el retraso (lag) del bucle de eventos y la latencia de cola, y mantendría la fábrica predeterminada como reversión inmediata.”

Análisis detallado paso a paso

Paso 1: Establecer los modelos predeterminado y eager

Con la fábrica predeterminada, create_task planifica la corrutina para ejecutarse pronto. Con un eager factory, la construcción la avanza de inmediato hasta que retorna, lanza una excepción o alcanza su primer await bloqueante. Una finalización síncrona puede no llegar a entrar nunca en la cola del bucle de eventos.

python
loop.set_task_factory(asyncio.eager_task_factory)
task = asyncio.create_task(read_cached(key))

Por lo tanto, la ejecución eager es un cambio semántico observable, no simplemente un planificador más rápido.

Paso 2: Elegir el límite de la corrutina

Los buenos candidatos son operaciones cortas y con alta tasa de aciertos en caché en memoria o memoizadas. Las corrutinas que puedan realizar trabajo de red, base de datos, archivos, bloqueos o CPU no acotada deben mantener un límite de bloqueo predecible para que la construcción no monopolice la tarea actual.

Paso 3: Analizar el orden y la equidad

Cuando se crean tareas en un bucle, las corrutinas eager pueden completarse inmediatamente en el orden de creación, alterando una intercalación que antes dependía del bucle de eventos. Un lote síncrono grande puede retrasar temporizadores, callbacks de E/S y otras solicitudes. Monitorea el retraso del bucle de eventos y acota el tamaño de los lotes.

Paso 4: Manejar excepciones y cancelación

Una excepción lanzada antes del primer bloqueo puede aflorar cerca de create_task, cambiando el punto de captura y la forma del stack trace. Mantén referencias a las Task y deja que CancelledError se propague tras la limpieza. Nunca uses el modo eager para convertir una cancelación de solicitud o un fallo de caché en una respuesta exitosa.

Paso 5: Combinarlo con TaskGroup y plazos límite

TaskGroup sigue gestionando el árbol de tareas, la cancelación entre hermanas y la unión (join), pero un hijo puede haberse completado o fallado ya durante create_task. Utiliza un manejo estructurado de try/except* y un plazo límite exterior único; no asumas que cada hijo espera primero en una cola.

python
async with asyncio.TaskGroup() as group:
    user = group.create_task(read_cached("user"))
    orders = group.create_task(read_remote("orders"))

Paso 6: Verificar versiones y delimitar el alcance de la funcionalidad

La fábrica está disponible a partir de Python 3.12. Python 3.14 también expone una opción eager_start en create_task. Un servicio multiversión debe validar el entorno de ejecución al inicio y preferir un experimento local en puntos de llamada antes que un cambio global incondicional de la fábrica.

Paso 7: Desplegar con métricas de reversión

Comienza en puntos de llamada exclusivos de caché detrás de un interruptor (switch). Compara la CPU, el tiempo de construcción de tareas, la latencia de cola en aciertos de caché, el retraso del bucle de eventos, la tasa de excepciones, la tasa de cancelación y el QPS posterior frente a la fábrica predeterminada. Si la equidad o los errores sufren una regresión, restaura la predeterminada sin modificar el código de negocio.

Paso 8: Probar la semántica, no solo benchmarks

Cubre el retorno síncrono, las excepciones síncronas, el primer await, la cancelación externa, múltiples fallos en TaskGroup, plazos límite, creación recursiva de tareas, equidad de temporizadores y lotes mixtos de caché/remotos. Registra eventos y valida el orden; un benchmark de throughput por sí solo no puede validar la semántica de planificación.

Respuesta modelo de alta calidad

“La ejecución eager se adapta a corrutinas de caché cortas y predecibles porque elimina un turno de planificación; es riesgosa para E/S impredecibles o rutas largas de CPU. El riesgo principal radica en la alteración del orden, la equidad y el momento de las excepciones. Yo verificaría las versiones, la habilitaría localmente detrás de un flag, conservaría la reversión al valor predeterminado y monitorearía el retraso del bucle de eventos, la latencia de cola, la cancelación y los errores. TaskGroup y un plazo límite compartido se mantienen, al igual que la cancelación y limpieza normales.”

Errores comunes

  • Tratar el modo eager como si fuera inocuo para la semántica → el orden y el momento de las excepciones cambian → documenta ambos modelos de ejecución y mide.
  • Habilitarlo para cada corrutina → E/S lentas o CPU bloquean la tarea actual → haz despliegues canario únicamente en rutas síncronas cortas.
  • Ignorar las versiones de Python → los despliegues más antiguos fallan al iniciar → valida el entorno de ejecución y conserva la fábrica predeterminada.
  • Observar únicamente el throughput promedio → la equidad sufre regresiones silenciosas → añade métricas de latencia de cola y de retraso del bucle.
  • Asumir que TaskGroup no cambia → los fallos en tiempo de construcción se manejan mal → prueba fallos de hijos eager y grupos de excepciones.
  • Ignorar errores de cancelación o de limpieza → las solicitudes filtran trabajo residual → preserva la cancelación y verifica las rutas de liberación de recursos.

Preguntas de seguimiento y respuestas sólidas

Pregunta de seguimiento 1: ¿Un acierto de caché síncrono sigue dejando una Task?

La Task ya puede estar completada durante la construcción. No requieras un turno del bucle de eventos; lee el objeto retornado y prueba la ruta de finalización eager explícitamente.

Pregunta de seguimiento 2: ¿El modo eager garantiza el orden de creación para la finalización?

No. Solo cambia el momento de inicio; una vez que una corrutina se bloquea, se aplica la planificación normal. Si el orden de negocio importa, coordínalo explícitamente u ordena los resultados por una clave de operación.

Pregunta de seguimiento 3: ¿Cómo evitas que un lote de aciertos de caché deje sin recursos a otras solicitudes?

Acota el lote, cede el control (yield) entre lotes cuando sea apropiado y monitorea el retraso del bucle. Aplicar el modo eager a un único punto de llamada de bajo costo es más seguro que una política global.

Pregunta de seguimiento 4: ¿Cómo se revierte tras una regresión?

Desactiva el interruptor, restaura la fábrica de tareas predeterminada, verifica que las métricas regresen a la línea base y conserva trazas que contengan la versión del entorno de ejecución y el orden de eventos para el diagnóstico.

Pregunta de seguimiento 5: ¿Cómo se relaciona eager_start con la fábrica?

eager_start es una opción explícita para una Task individual; la fábrica es una política predeterminada a nivel del bucle de eventos. Confirma la firma exacta de la versión de Python y la precedencia antes de combinarlas.

Fuentes públicas

Preguntas relacionadas

Herramienta de entrevista relacionada

Usa Captura para un ejercicio de código

Captura el problema y luego aborda en orden las restricciones, la solución, el código, los casos extremos y la complejidad.

Ver la herramienta