Prompt y contexto
Una API asíncrona carga datos de usuario, pedidos y recomendaciones de forma concurrente. Un fallo en una tarea requerida debe cancelar a las tareas hermanas, mientras que una desconexión del cliente o un tiempo de espera total no deben dejar corrutinas huérfanas. Diseñe la implementación con asyncio.TaskGroup y explique la agregación de excepciones, la cancelación y la limpieza de recursos.
La documentación de Python describe TaskGroup como concurrencia estructurada: las tareas finalizan antes de que el alcance termine; una excepción que no sea de cancelación cancela las tareas restantes y se lanza como un ExceptionGroup. La entrevista evalúa el razonamiento sobre el ciclo de vida en lugar de una acumulación de llamadas a create_task().
Qué evalúa el entrevistador
Se busca evaluar el árbol de tareas padre-hijo, la cancelación de tareas hermanas, CancelledError, ExceptionGroup y la unión mediante async with. El candidato debe propagar la cancelación hacia bases de datos, HTTP y archivos, y trasladar el trabajo que debe sobrevivir a la respuesta hacia una cola duradera.
Preguntas para clarificar
- ¿Son obligatorias las tres lecturas o pueden degradarse las recomendaciones?
- ¿Qué tareas abren conexiones, cursores o archivos temporales?
- ¿Quién crea el tiempo de espera total y cómo llega la cancelación del invocador?
- ¿Qué errores necesitan clasificación, reintentos o una respuesta segura para el usuario?
- ¿Qué trabajos deben continuar después de la respuesta HTTP?
Respuesta en 30 segundos
“Cree tres tareas dentro de async with TaskGroup() y espere a que finalice el alcance. Una excepción que no sea de cancelación cancela a las hermanas y lanza un grupo de excepciones; except* clasifica los errores conocidos. Cada tarea cierra recursos en finally. Envuelva el grupo en un asyncio.timeout() o en la cancelación del invocador. El trabajo que deba sobrevivir a la solicitud se envía a una cola duradera.”
Solución paso a paso
Paso 1: Definir el árbol de tareas y el contrato de resultados
Declare cada resultado y su nivel de criticidad. El usuario y los pedidos pueden ser obligatorios mientras que las recomendaciones son opcionales; esa elección determina si una excepción cancela el grupo. Cada tarea pertenece al alcance de la solicitud.
async with asyncio.TaskGroup() as group:
user_task = group.create_task(load_user(user_id))
order_task = group.create_task(load_orders(user_id))
rec_task = group.create_task(load_recommendations(user_id))Salir del alcance une las tareas. Leer una tarea no finalizada fuera del bloque no sustituye su unión formal.
Paso 2: Manejar fallos con ExceptionGroup
Una excepción distinta de CancelledError cancela a las hermanas, y TaskGroup lanza un ExceptionGroup después de que todas las tareas finalizan. Utilice except* para errores de dependencias esperados y preserve los errores desconocidos para el manejador externo.
try:
async with asyncio.TaskGroup() as group:
user = group.create_task(load_user(user_id))
orders = group.create_task(load_orders(user_id))
except* RetryableDependencyError as errors:
record_dependency_failures(errors.exceptions)
raise ServiceUnavailable from errorsNo capture BaseException suprimiendo silenciosamente la cancelación.
Paso 3: Propagar la cancelación a la E/S real
TaskGroup cancela tareas de Python; los controladores de HTTP, bases de datos y archivos necesitan soporte para cancelación o tiempo de espera. Cada tarea cierra conexiones, libera semáforos, elimina archivos temporales y detiene el consumo en finally.
async def load_orders(user_id: str):
conn = await pool.acquire()
try:
return await conn.fetch("SELECT ...", user_id, timeout=1.5)
finally:
await pool.release(conn)Si un controlador no puede interrumpir una consulta, use un tiempo de espera de sentencia, una conexión aislada o un trabajo en segundo plano acotado en lugar de depender únicamente de la cancelación de la tarea.
Paso 4: Establecer un tiempo de espera total y distinguir la cancelación externa
Envuelva todo el grupo en asyncio.timeout() para que todo el trabajo comparta un único presupuesto de tiempo.
try:
async with asyncio.timeout(2.0):
result = await aggregate(user_id)
except TimeoutError:
return degraded_response("deadline")
except asyncio.CancelledError:
raiseLa cancelación externa debe continuar hacia arriba, no convertirse en una respuesta exitosa. Registre el tiempo de espera y la cancelación del usuario como causas diferentes.
Paso 5: Comprender TaskGroup frente a gather
asyncio.gather() normalmente propaga la primera excepción a quien lo espera, pero las tareas hermanas no necesariamente se cancelan; retornar inmediatamente puede dejarlas huérfanas. TaskGroup vincula el ciclo de vida de la tarea a un alcance y cancela a las hermanas ante un fallo.
gather(return_exceptions=True) es útil para un contrato explícito de fallo parcial, pero cada resultado debe inspeccionarse. No es un sustituto de la limpieza estructurada, y un grupo no debe crear tareas en segundo plano sin límites.
Paso 6: Probar el orden de fallos y la limpieza
Inyecte fallo primero en usuario, fallo primero en recomendaciones, fallos simultáneos, cancelación del invocador, tiempo de espera total, tiempo de espera del controlador y un finally que falle. Valide que las hermanas reciban la cancelación, los recursos se liberen, no queden tareas pendientes y cada excepción pueda vincularse a una tarea hija.
Registre el nombre de la tarea, ID de solicitud, duración, causa de la cancelación, tipo de excepción y llamada descendente sin incluir cargas útiles privadas. Repita pruebas de E/S lenta para exponer condiciones de carrera; las pruebas donde todo es exitoso son insuficientes.
Respuesta modelo
“Creo tres tareas dentro de TaskGroup, hago que usuario y pedidos sean obligatorios, y permito que las recomendaciones se degraden. El alcance une las tareas; un fallo que no sea de cancelación cancela a las hermanas y lanza un ExceptionGroup, que except* clasifica. Cada tarea de E/S pasa un tiempo de espera y libera conexiones en finally.”
“Un tiempo de espera externo proporciona un presupuesto compartido y CancelledError se propaga. El trabajo posterior a la respuesta se envía a una cola duradera. Inyecto fallos simultáneos, cancelación, tiempo de espera y E/S lenta, y verifico que no haya tareas huérfanas ni fugas de recursos.”
Errores comunes
- Retornar tras un simple
create_task→ tareas huérfanas → mantener las tareas dentro del alcance de un TaskGroup. - Suprimir
CancelledError→ el padre no puede detenerse → limpiar y volver a lanzar. - Dar a cada tarea un tiempo de espera completo → la latencia total se descontrola → compartir un solo presupuesto.
- Tratar
ExceptionGroupcomo un solo error → se pierde la evidencia paralela → clasificar conexcept*. - Cancelar solo tareas de Python → el trabajo en base de datos o HTTP continúa → usar cancelación del controlador o tiempos de espera.
- Usar gather para todos los casos → el fallo parcial y la limpieza se vuelven ambiguos → definir el contrato de degradación.
Preguntas de seguimiento y respuestas
Pregunta de seguimiento 1: ¿TaskGroup detiene instantáneamente la E/S subyacente?
No. El controlador debe admitir la cancelación o un tiempo de espera de sentencia; de lo contrario, aísle la conexión o traslade el trabajo a un proceso en segundo plano acotado.
Pregunta de seguimiento 2: ¿Por qué no cancelar el grupo cuando fallan las recomendaciones?
Utilice el contrato de negocio. Una recomendación opcional se puede capturar y sustituir por un resultado vacío; el trabajo crítico para la seguridad o la facturación debe permitir que la excepción escape y cancele a las hermanas.
Pregunta de seguimiento 3: ¿Cómo se asigna un ExceptionGroup a una respuesta de API?
Mapee los errores de dependencias conocidos a un 503 seguro o a un resultado degradado y registre los errores hijos. Los errores desconocidos van al manejador global; nunca devuelva trazas de pila internas al cliente.
Pregunta de seguimiento 4: ¿Qué trabajo pertenece fuera de TaskGroup?
El envío de correos electrónicos, la indexación y las escrituras por lotes que deben continuar tras la respuesta deben persistirse en una cola y ser reintentados por un worker. El alcance de la solicitud solo debe contener trabajo con ciclo de vida de solicitud.
Pregunta de seguimiento 5: ¿Qué sucede si finally lanza una excepción durante la cancelación?
Mantenga la limpieza pequeña y observable, adjunte los fallos de limpieza como contexto y preserve la cancelación original o la excepción del negocio para que no se reemplace la causa raíz.