Planteamiento y contexto
El entrevistador te presenta un manejador de solicitudes que ejecuta tres operaciones relacionadas en paralelo: obtener un perfil de usuario, consultar órdenes y generar recomendaciones. El cliente se desconecta, un subproceso hijo crítico falla o el límite de tiempo global expira. El sistema no debe dejar trabajo huérfano ejecutándose después de que la solicitud haya finalizado. Explica la concurrencia estructurada y luego trasládala a grupos de tareas (task groups), alcances de corrutinas (coroutine scopes) o alcances de tareas estructuradas sin supeditar la respuesta a una API específica.
Esta pregunta es ideal para roles de backend intermedio y senior, plataforma, móvil e infraestructura multilenguaje. El objetivo es el razonamiento sobre el ciclo de vida: entrada, salida, política de fallas y propiedad de los recursos.
Qué está evaluando el entrevistador
El entrevistador quiere saber si tratas el trabajo concurrente como parte de una tarea padre en lugar de enviar trabajo a un pool global y retornar la respuesta. Una respuesta sólida establece que los hijos no sobreviven a su alcance, que la cancelación del padre se propaga, que las fallas pueden ser de interrupción inmediata (fail-fast) o parciales según el valor del negocio, y que la espera conjunta (joining) y la limpieza ocurren dentro de un límite visible.
Una respuesta débil se limita a decir "usa async/await en paralelo". Una sólida distingue la concurrencia estructurada de los futuros desacoplados (detached futures) y explica cuándo el trabajo debe trasladarse a una cola duradera separada con un nuevo propietario.
Preguntas para aclarar primero
- ¿Deben tener éxito los tres hijos, o el perfil y las órdenes son obligatorios mientras que las recomendaciones son opcionales?
- ¿Es el límite de tiempo una restricción estricta de la solicitud (hard deadline), o el servidor puede finalizar bajo un presupuesto flexible separado?
- ¿Los hijos realizan únicamente operaciones de I/O cancelables, o pueden desencadenar efectos secundarios externos irreversibles?
- Tras la desconexión de un cliente, ¿el trabajo puede pasar a una notificación asíncrona o a un trabajo en segundo plano?
- ¿Una falla debe devolver un único error, resultados parciales o una respuesta explícitamente degradada?
Estas respuestas modifican el límite del alcance: el trabajo corto de la solicitud permanece dentro del alcance padre; el trabajo que continúa después de la solicitud necesita una transferencia explícita de propiedad.
Una respuesta de 30 segundos
Trato la solicitud como la tarea padre y las tres lecturas paralelas como tareas hijas. El padre sale únicamente después de que los hijos terminan, fallan o son cancelados y limpiados. Una falla en el perfil o en las órdenes cancela a los hermanos y devuelve un error reintentable; una falla en las recomendaciones devuelve una respuesta degradada explícita. Las desconexiones de clientes y los límites de tiempo propagan la cancelación hacia abajo, y cada hijo cierra conexiones y descriptores en su ruta de limpieza. Si el trabajo debe continuar después de la solicitud, lo escribo en una cola duradera e inicio un nuevo ciclo de vida en lugar de desacoplar un futuro en segundo plano. Verifico la cancelación, los tiempos de espera, las fallas parciales y las métricas de fugas de recursos mediante inyección de fallas.
Respuesta paso a paso
Paso 1: Dibujar el árbol de tareas
Establece el manejador de solicitudes como la raíz. El perfil, las órdenes y las recomendaciones son hijos directos. Los hijos heredan del padre el límite de tiempo, el contexto de trazabilidad (tracing) y la señal de cancelación. El padre es responsable de la espera, la composición de errores y el cierre del alcance; un hijo no puede delegar trabajo a un ejecutor global y dejar que el padre retorne sin una transferencia de propiedad.
La invariante clave es verificable: cuando el padre sale, cada hijo ha finalizado, ha sido cancelado o ha sido transferido explícitamente a otro propietario registrado. Sin esa invariante, las fugas de hilos, las escrituras duplicadas y la latencia de cola inexplicable permanecen ocultas.
Paso 2: Elegir la política de fallas y resultados parciales
Clasifica según el valor de negocio. El perfil y las órdenes son necesarios para la vista de pago (checkout), por lo que la falla de cualquiera de ellos cancela a los hermanos y devuelve un error reintentable. Las recomendaciones son una mejora visual/funcional, por lo que su falla devuelve una respuesta sin recomendaciones y registra la degradación. No permitas que una falla de bajo valor derribe toda la solicitud, ni disfraces datos críticos faltantes como un objeto vacío.
El pseudocódigo puede expresar la política sin atarse a un lenguaje específico:
within request_scope(deadline):
profile = child(fetch_profile)
orders = child(fetch_orders)
recommendations = child(fetch_recommendations)
wait(profile, orders)
if profile.failed or orders.failed:
cancel_all_children()
return retryable_error
return compose(profile, orders, recommendations.or_empty)Paso 3: Hacer que la cancelación alcance los límites de los recursos
La cancelación no es solo un booleano en un objeto de tarea. Los clientes de red, los controladores de bases de datos y las lecturas de archivos deben observar la señal; las esperas deben ser interrumpibles; los bucles de reintento deben volver a verificar el tiempo restante antes de otro intento. Para un efecto secundario irreversible, como un pago aceptado o un correo electrónico enviado, la cancelación puede detener los pasos posteriores, pero no puede pretender que el efecto secundario ya completado fue revertido.
Cuando el cliente se desconecta, el punto de entrada cancela la raíz. La raíz propaga la cancelación, y los hijos cierran cuerpos de respuesta, conexiones, suscripciones y archivos temporales en su ruta de limpieza. La limpieza en sí misma necesita un límite de tiempo para que "esperar por la limpieza" no bloquee indefinidamente el cierre de la solicitud.
Paso 4: Separar tiempo de espera agotado, cancelación y falla
Un tiempo de espera agotado (timeout) significa que el presupuesto de tiempo se consumió. Una cancelación significa que un servicio aguas arriba ya no necesita el resultado. Una falla significa que la tarea no pudo completarse. Pueden coincidir, pero los registros y el estado del usuario no deben colapsar en un simple error 500. Registra la causa original, el desencadenante y la ruta de la tarea. Por lo general, no reintentes tras la desconexión de un cliente; permite un único reintento corto tras un timeout de dependencia solo si el presupuesto restante lo permite; envía los errores de negocio a través de una política de degradación explícita.
No coloques reintentos ciegos o sleep fuera del alcance. Consumen un presupuesto ya expirado y siguen generando carga después de que el padre ha retornado.
Paso 5: Demostrar que la estructura se preserva
Prueba la finalización normal, la falla de un hijo crítico, la falla de un hijo opcional, la cancelación del padre, la expiración del límite de tiempo y la cancelación antes de que un hijo comience. Cada prueba verifica que los hijos activos vuelvan a cero, que las conexiones aguas abajo se cierren, que las trazas muestren las relaciones padre-hijo, que los efectos secundarios no se dupliquen y que la clasificación de errores coincida con el estado visible para el usuario.
Monitorea tareas activas, latencia de propagación de cancelaciones, tasa de timeouts de límites de tiempo, excepciones en hijos, duración de la limpieza, tareas que siguen en ejecución tras la cancelación y el despliegue máximo en abanico (fan-out) por solicitud. Una respuesta exitosa por sí sola no demuestra la existencia de concurrencia estructurada.
Respuesta modelo de alta calidad
"Trataría una solicitud como una tarea padre y colocaría las tres lecturas dentro de un mismo alcance delimitado por un límite de tiempo. El padre crea, espera y cierra a los hijos; ningún hijo puede sobrevivir a ese alcance. El perfil y las órdenes son dependencias críticas, por lo que la falla de cualquiera de ellos cancela a los hermanos y devuelve un error reintentable. Las recomendaciones son opcionales; en caso de falla devuelvo un estado explícito de recomendaciones vacías y registro la degradación.
La desconexión del cliente, el timeout del padre o la cancelación aguas arriba se propagan hacia abajo por el árbol de tareas. Cada llamada de I/O utiliza una interfaz cancelable, los reintentos verifican el presupuesto restante y la limpieza cierra conexiones y suscripciones. No asumo que un efecto secundario externo pueda revertirse después de haber ocurrido. Si el trabajo debe continuar después de la solicitud, primero lo escribo en una cola duradera y permito que un consumidor cree un nuevo árbol de tareas.
Inyectaría fallas críticas, fallas opcionales, carreras de cancelación y timeouts de limpieza, observando luego las tareas activas, la latencia de cancelación, las fugas de recursos y las trazas padre-hijo. La parte fundamental es el ciclo de vida y la propiedad, no una API particular de Java, Kotlin o Swift."
Modos comunes de falla
- Equiparar concurrencia estructurada con ejecución paralela → Solo describes iniciar tareas juntas y omites los tiempos de vida → Muestra los límites del alcance, la espera conjunta (join), la cancelación y la limpieza.
- Enviar trabajo a un ejecutor global y retornar → El trabajo puede acceder a los recursos de la solicitud después de que el padre ya no existe → Mantenlo dentro del alcance de la solicitud o transfiérelo explícitamente a una cola duradera.
- Tratar la cancelación como una terminación forzada (kill) → Los efectos externos pueden haberse completado y los recursos podrían no cerrarse automáticamente → Especifica la cancelación cooperativa, el trabajo irreversible y la propiedad de la limpieza.
- Devolver un resultado vacío para cualquier falla → Los datos críticos faltantes quedan ocultos para los clientes que llaman → Define reglas de interrupción inmediata (fail-fast) y de resultados parciales según la criticidad del negocio.
- Probar únicamente el caso de éxito → Las condiciones de carrera en cancelaciones y el trabajo huérfano permanecen invisibles → Inyecta desconexiones, límites de tiempo, fallas de hermanos y cancelaciones repetidas; luego verifica que el trabajo activo llegue a cero.
Preguntas de seguimiento
Pregunta de seguimiento 1: La generación de recomendaciones toma varios segundos, pero la solicitud ya finalizó. ¿Qué haces?
Primero pregunto si el usuario todavía necesita el resultado. Si solo es para la página actual, la cancelo junto con el padre. Si el negocio requiere una generación asíncrona, persisto la entrada y una clave de idempotencia, y luego dejo que un consumidor cree un nuevo alcance. Dicho consumidor es dueño de un nuevo límite de tiempo, política de reintentos y alertas; no puede tomar prestado el contexto de la solicitud ya finalizada.
Pregunta de seguimiento 2: ¿Por qué no dejar que los otros hijos terminen después de que un hijo falla?
Puedes hacerlo, si el trabajo restante tiene valor y encaja dentro del presupuesto de tiempo. Continuar la búsqueda de órdenes después de que fallaron los datos críticos del perfil suele desperdiciar conexiones y capacidad aguas abajo; finalizar una actualización independiente de caché o un registro de auditoría puede ser razonable. Establece la condición para la cancelación en lugar de tratar la interrupción inmediata (fail-fast) como una regla universal.
Pregunta de seguimiento 3: Se envió la señal de cancelación, pero la consulta a la base de datos sigue ejecutándose. ¿Cómo lo manejas?
Verifica si el controlador (driver) soporta cancelación y libera la conexión. Si no es así, aplica un timeout de consulta independiente, aísla el pool y protégete contra el uso de un resultado tardío. Mide el tiempo transcurrido desde la cancelación hasta la liberación del recurso; superar un umbral debería activar degradación, aislamiento o una alerta. Una consulta no cancelable no debe retener capacidad a nivel de solicitud de manera indefinida.
Pregunta de seguimiento 4: ¿La concurrencia estructurada reemplaza a todos los thread pools y colas de mensajes?
No. Se adapta a trabajos de corta duración con una clara relación padre-hijo y cancelación y limpieza compartidas. El trabajo entre solicitudes, las tareas programadas y los reintentos duraderos aún necesitan una cola o un flujo de trabajo (workflow). Un pool compartido puede proporcionar recursos de ejecución, pero el alcance emisor debe definir quién espera, quién cancela y a quién le pertenece el resultado.