FiabilidadObservabilidadSQLiteSistemas distribuidosRustTimeouts

29 tareas zombis: el trabajo que nunca termina no da ningún error

Publicado el 2026-08-18 · Xiliux

Un orquestador de escaneos llevaba 29 tareas en estado «ejecutándose» desde hacía días. Ningún error en el registro, ningún proceso vivo detrás, ninguna alarma.

Nadie se enteró, y la razón es la que hace que esta clase de defecto sobreviva tanto tiempo: una tarea colgada y una tarea lenta se ven exactamente igual.

El estado que miente

El diseño es el habitual. Al empezar una tarea se escribe una fila con estado running. Al terminar, se actualiza a completed o a failed.

El fallo está en lo que no se escribió: qué pasa si el proceso muere entre las dos escrituras. Si el trabajador cae, si lo mata el sistema por memoria, si el servicio se reinicia en medio, nadie vuelve a tocar esa fila. Se queda en running para siempre.

Y running para siempre no dispara nada. No es un error: es el estado normal de una tarea que está trabajando. El sistema no puede distinguir «esto lleva ocho horas porque el objetivo es enorme» de «esto lleva ocho horas porque murió hace ocho horas».

Por qué el registro no servía

Lo primero que hice fue mirar el registro del servicio. En diez horas tenía cinco líneas, y las cinco eran del reinicio.

Ese es el segundo problema, y merece decirse aparte: un registro que no habla no es un registro sano, es un instrumento que no discrimina. Un servicio callado y un servicio muerto producen el mismo silencio. Antes de concluir nada del registro conviene comprobar que el registro habría dicho algo si hubiera pasado algo — y ahí es donde este falló.

Así que la comprobación se hizo contra el artefacto: la propia base de datos.

SELECT status, COUNT(*), MAX(started_at)
FROM scan_logs GROUP BY status;

Cuatro filas y el diagnóstico completo: 158 completadas, 113 con plazo agotado, 29 en running con marcas de días atrás. Nada de esto se veía desde fuera.

La causa raíz: no había plazo

Las 29 zombis tenían una causa común, y no era el estado mal escrito: faltaba la variable de configuración que fija el plazo máximo de un escaneo.

Sin plazo declarado no existe el instante en que el sistema pueda decir «esto fracasó». Un trabajo sin límite superior no falla nunca; se queda esperando. El estado colgado es el síntoma; la ausencia de un techo es la enfermedad.

Es la misma lección que enseña un timeout en integración continua: el techo no está para el caso normal, está para convertir una degradación silenciosa en un fallo visible.

Las dos correcciones, que son distintas

1. Reconciliación al arrancar. Una rutina que corre al iniciar el servicio y marca como interrupted toda tarea que figure como running. El razonamiento es sólido: al arrancar, ninguna tarea del proceso anterior sigue viva, luego toda tarea «en ejecución» es por definición huérfana. Limpia el estado heredado y deja escrito el motivo, en vez de borrar la fila.

2. Plazo máximo por tarea. Impide que se generen zombis nuevos mientras el servicio corre. La reconciliación limpia el pasado; el plazo protege el futuro. Hacen falta las dos, y confundirlas es fácil.

La comprobación, contra el artefacto

Al día siguiente, la misma consulta:

Una sola tarea en ejecución y reciente. Eso es lo que se ve en un sistema sano, y no hay forma de deducirlo del registro.

Lo que me llevo

  1. Un estado que solo se escribe en el camino feliz es un estado que miente. Toda transición necesita quién la cierra cuando el proceso no llega.
  2. Reconcilia al arrancar. Es diez líneas y es el único momento en que puedes afirmar con certeza que nada del proceso anterior sigue vivo.
  3. Ninguna tarea sin techo. Sin plazo declarado, el fracaso no tiene forma de manifestarse.
  4. Verifica contra el artefacto, no contra el registro. Y antes de creerte un silencio, comprueba que ese registro habría hablado.

Preguntas frecuentes

¿Por qué no lo detecta la monitorización normal?

Porque no hay nada anómalo que observar. El proceso no falló, no lanzó excepción y no dejó traza de error: simplemente desapareció mientras su fila en la base seguía diciendo «ejecutándose». Las alarmas se disparan con errores, y aquí no hubo ninguno.

¿Qué es exactamente una reconciliación al arrancar?

Una rutina que corre al iniciar el servicio y compara el estado persistido con lo que puede estar vivo. Como al arrancar no hay ninguna tarea del proceso anterior en ejecución, toda tarea que figure como «ejecutándose» es por definición huérfana, y se marca como interrumpida con su motivo.

¿Basta con la reconciliación?

No. La reconciliación limpia el estado heredado, pero no impide que se vuelvan a crear zombis mientras el servicio corre. Hace falta además un plazo máximo por tarea: sin un límite declarado, no existe el instante en que el sistema pueda decir que algo fracasó.

¿Cómo compruebo que la corrección funcionó?

Mirando el artefacto, no el registro. Se consulta la propia base y se cuenta cuántas tareas hay en cada estado: si queda una sola «ejecutándose» y su marca de tiempo es reciente, el sistema está sano. Si hay veintinueve con marcas de hace días, no lo está, diga lo que diga el registro.

← Ver más artículosCotizar un proyecto