Tema representativo de entrevista

Entrevista de Backend: ¿Cómo diseñarías un pool de workers acotado (Bounded Worker Pool)?

BackendIntermedio
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Diseña un pool de workers acotado para un servicio de backend. Explica la capacidad de la cola, la admisión o rechazo, la cancelación, la propiedad de los reintentos, la correlación de resultados, las métricas y el apagado ordenado.

Consigna y alcance

Diseña un pool de workers para un servicio de backend que acepte tareas, ejecute como máximo un número fijo de forma concurrente y se apague sin descartar silenciosamente el trabajo aceptado. Explica la capacidad de la cola, la admisión o rechazo, la cancelación, la propiedad de los reintentos, la correlación de resultados, las métricas y el apagado ordenado.

Asume que las tareas son independientes y pueden llamar a una API downstream. El pool es un componente en proceso (in-process); las colas durables pertenecen a un diseño independiente cuando las tareas deben sobrevivir a la pérdida del proceso.

Qué evalúa el entrevistador

Quieren ver un límite de concurrencia claro, un modelo de memoria acotado y una política para la sobrecarga. También evalúan si la cancelación llega a los workers, si los reintentos pueden multiplicar la carga y si el apagado distingue entre tareas en cola, en ejecución, completadas y rechazadas.

Preguntas para aclarar antes de responder

  • ¿Es aceptable perder trabajo si el proceso falla (crash)? Si no, coloca la tarea en un broker durable primero.
  • ¿Son las tareas idempotentes y se pueden reintentar de forma segura?
  • ¿Cuáles son los límites de tasa (rate limits) del downstream, el tiempo promedio de ejecución y los objetivos de latencia de cola (tail latency)?
  • ¿El envío (submit) debería bloquearse brevemente, retornar 429 o descartar trabajo de baja prioridad cuando la cola esté llena?
  • ¿La cancelación significa únicamente "detener antes de iniciar", o la tarea puede interrumpir cooperativamente la E/S?

Estructura de respuesta en 30 segundos

"Expondría una API de submit acotada respaldada por un recuento fijo de workers y una cola finita. La admisión retorna un resultado de sobrecarga claro cuando se agota la capacidad, mientras que un contexto o token de cancelación permite que las tareas en cola salgan antes de la ejecución y que las tareas en ejecución se detengan de forma cooperativa. Cada tarea aceptada tiene un ID y un estado terminal; los reintentos están limitados, tienen jitter y son propiedad de una sola capa. Las métricas cubren la profundidad de la cola, la antigüedad, los workers activos, el rechazo, el tiempo de ejecución y la cancelación. El apagado detiene la admisión, cancela el trabajo en cola según la política, drena el trabajo aceptado hasta una fecha límite (deadline) y reporta todo lo que no pudo finalizar."

Análisis detallado paso a paso

Paso 1: Definir la máquina de estados

Utiliza submitted → queued → running → succeeded|failed|cancelled. Una cola llena produce rejected en lugar de una espera invisible. Persistir estos estados fuera del proceso es necesario cuando los llamadores requieren recuperación después de una caída.

Paso 2: Acotar tanto los workers como la cola

Elige W workers y una capacidad de cola Q; la memoria está acotada por Q cargas útiles de tareas más las pilas de los workers. No crees un hilo o goroutine por solicitud. ThreadPoolExecutor de Python expone max_workers, pero aún se necesita una política de admisión acotada a nivel de aplicación cuando el volumen de envíos es ilimitado.

Paso 3: Elegir la admisión y la contrapresión (backpressure)

Ofrece un submit no bloqueante, espera acotada o rechazo según la prioridad. Retorna un código de sobrecarga estable y Retry-After solo cuando reintentar sea seguro. Un productor que espera indefinidamente en una cola llena puede bloquear una ruta de solicitud (deadlock), mientras que una cola no acotada convierte la sobrecarga en latencia y crecimiento de memoria.

text
submit(job, deadline):
    if stopping or deadline expired: return REJECTED
    if queue.try_push(job): return ACCEPTED(job.id)
    if policy == WAIT and wait_until(deadline) and queue.try_push(job):
        return ACCEPTED(job.id)
    return OVERLOADED

Paso 4: Hacer que la cancelación sea cooperativa

Pasa un contexto de cancelación a cada tarea. Elimina las tareas canceladas que no se hayan iniciado; las tareas en ejecución deben verificar la cancelación en puntos seguros y pasarla a los clientes downstream. Un pool no puede matar hilos arbitrarios de forma segura, así que documenta qué significa "cancelado" para la E/S no interrumpible.

Paso 5: Mantener la propiedad de los reintentos en una sola capa

Elige entre el pool, el manejador de la tarea o la cola durable para programar los reintentos. Limita los intentos, añade retroceso exponencial (exponential backoff) con jitter y clasifica los errores en reintentables y permanentes. De lo contrario, un tiempo de espera agotado (timeout) en tres capas puede generar una tormenta de reintentos contra la misma dependencia.

Paso 6: Correlacionar resultados y errores

Retorna un ID de tarea o future, nunca un espacio de resultado mutable compartido. Registra el error original, el recuento de intentos y el estado terminal. Si los llamadores hacen sondeo (polling), haz explícita la retención y la autorización del almacén de resultados; si los llamadores esperan de forma asíncrona (await), define cómo afectan las desconexiones al trabajo.

Paso 7: Diseñar un apagado ordenado (graceful shutdown)

Durante el apagado, detén la admisión primero, luego marca las tareas en cola según la política y permite que las tareas en ejecución se drenen hasta una fecha límite. La guía de pipelines de Go propaga la cancelación a través de una señal done; el mismo principio se aplica aquí. Después de la fecha límite, reporta las tareas no finalizadas para su reejecución o márcalas como abandonadas solo con un contrato explícito.

Paso 8: Instrumentar el cuello de botella

Monitorea la profundidad y antigüedad de la cola, los workers activos, la utilización, los recuentos de aceptados/rechazados/cancelados, la duración de la ejecución, el recuento de reintentos y los errores de downstream. Emite alertas ante una antigüedad de cola y rechazos sostenidos, no solo por CPU. Dimensiona W a partir del cuello de botella de downstream: pool de conexiones, límite de tasa externo o capacidad de CPU.

Compensaciones y límites

Compensación 1: Workers fijos o escalado dinámico

Los workers fijos hacen que la concurrencia sea predecible y protegen las dependencias. El escalado dinámico puede mejorar el rendimiento (throughput), pero debe imponer un límite global estricto y contabilizar cada réplica; de lo contrario, cada instancia escala de forma independiente y satura la dependencia.

Compensación 2: Rechazar o esperar cuando esté lleno

Rechazar brinda a los llamadores una respuesta rápida y preserva la latencia. Esperar puede suavizar ráfagas cortas, pero la espera debe tener un tiempo límite y no debe retener hilos de solicitud escasos indefinidamente.

Compensación 3: Cola en proceso o durable

Un pool en proceso ofrece baja latencia y simplicidad. Un broker durable agrega recuperación, reejecución y costo operativo. Utiliza la opción durable cuando el trabajo aceptado deba sobrevivir a despliegues, caídas o enrutamiento entre múltiples instancias.

Simulacros de fallas y plan de evolución

Simulacro 1: Caída del servicio downstream

Haz que cada tarea falle lentamente y verifica la antigüedad de la cola, los rechazos, los timeouts y los reintentos limitados. Confirma que el pool no cree más concurrencia mientras la dependencia no esté en buen estado.

Simulacro 2: Tormenta de cancelaciones

Envía 10,000 tareas, cancela la mitad antes de la ejecución y detén el servicio durante el drenado. Verifica que las cancelaciones en cola no se ejecuten y que las tareas aceptadas en ejecución alcancen un estado terminal reportado.

Simulacro 3: Despliegue de réplicas

Ejecuta dos versiones durante un despliegue continuo (rolling deploy). Comprueba que cada instancia aplique su límite local y que una cola durable o un limitador global aplique el límite de todo el sistema cuando sea necesario.

Errores comunes y seguimiento

Error 1: Búfer no acotado

Una cola no acotada oculta la sobrecarga hasta que la memoria o la latencia colapsan. Haz que la capacidad y la política de rechazo sean observables.

Error 2: Reintentos duplicados

Si tanto el cliente HTTP como el manejador de la tarea reintentan, los intentos se multiplican. Asigna un único propietario del reintento y propaga los metadatos de los intentos.

Error 3: Tratar la cancelación como la terminación forzada de hilos

La mayoría de los entornos de ejecución no pueden matar trabajo arbitrario de forma segura. Utiliza comprobaciones cooperativas, E/S cancelable y una política clara para el trabajo abandonado.

Error 4: Drenar sin detener la admisión

Nuevas tareas pueden mantener la cola no vacía indefinidamente. Cierra la admisión antes de esperar la fecha límite de drenado.

Error 5: Omitir los límites por réplica

Un pool de 20 workers en 10 réplicas representa 200 llamadas concurrentes. Especifica si el límite es local, particionado (sharded) o coordinado globalmente.

Error 6: Sin política de retención de resultados

Los futures y los registros de sondeo necesitan expiración, autorización y una ruta de fallo. De lo contrario, las tareas aceptadas se convierten en un almacenamiento ilimitado.

Fuentes públicas

Preguntas relacionadas