El worker murió. La sincronización, no.

Mata el worker a mitad de un lote y sigue a un contacto: qué cursor se guardó, por qué la escritura repetida no llega dos veces, quién es dueño de la sincronización mientras se reinicia. Después, los dos incidentes que demostraron que la recuperación era solo la mitad del trabajo.

recuperación de fallos de sincronización

¿Qué le pasa a una sincronización en curso cuando el worker muere a mitad de un lote?

El cursor solo avanza al final de un lote sin errores, así que un worker que muere a mitad de un lote lo deja al inicio de ese lote y la siguiente ejecución lee la misma página. La clave de entrega de cada registro se deriva del cursor de la página y de su posición, y un registro de siete días con las escrituras completadas impide que la repetición llegue dos veces. Una concesión temporal por sincronización impide que dos ejecuciones trabajen a la vez. Esa era la mitad fácil. Una sincronización siguió fallando, ejecución tras ejecución, antes de que algo la pausara, y un espacio de trabajo pasó un tiempo en silencio, con errores y sin ningún éxito, antes de que algo lo notara. Por eso ahora hay un disyuntor que pausa tras cinco fallos consecutivos y una alerta horaria tras veinticuatro horas sin un éxito.

Saber más
En esta página

En una charla del equipo de relaciones con desarrolladores de Crusoe, publicada el 3 de octubre de 2026 por AI Engineer, se lanzó un trabajo de entrenamiento de PyTorch en dos nodos A100. Después se le comunicó al propio sistema de monitorización de la demo que una GPU había lanzado un error XID 79. El nodo se marcó como caído y el trabajo se canceló. El proceso recibió un SIGTERM y “hasta dos minutos para gestionar ese SIGTERM, ya sea para guardar tus checkpoints o para volcar tus logs”. Entró un nodo de reserva y “el trabajo de slurm se reiniciará. Cargará el checkpoint y continuará el entrenamiento donde lo dejó”. Algo menos de quince minutos de principio a fin. No se avisó a nadie de guardia.

Una sincronización es un animal mucho más pequeño que un entrenamiento. La pregunta es la misma. ¿Qué necesita una ejecución para que matar el worker cueste minutos y ningún duplicado?

Esta es la respuesta para el ejecutor de sincronizaciones de Tajo, leída en el código y no en un folleto. Reverse ETL describe la forma del trabajo. Esto es lo que pasa cuando el trabajo muere.

Sigue a un contacto

Una ejecución lee la fuente página a página, desde un cursor guardado. Llamemos c3 al cursor de la página, y a nuestro contacto, el registro número 212 de esa página. Antes de que se escriba nada, hay tres cosas ciertas sobre ese contacto.

Primero, tiene una clave de entrega. La clave es un hash del id de la sincronización, la fuente, el cursor de la página, la posición en la página y el propio registro. El cursor y el registro entran en el hash y nunca se incrustan en la clave, así que la clave no contiene ninguna dirección de email.

Segundo, la escritura pasa por un registro de escrituras, no va directamente al destino. Antes de llamar al destino, la clave se reserva como pendiente. Cuando la escritura tiene éxito, la fila se marca como completada y se conserva durante siete días.

Tercero, el cursor no se ha movido. Solo se mueve al final de un lote, solo si cada registro de ese lote llegó, y solo después de que la ejecución haya demostrado que sigue siendo dueña de la sincronización.

Ahora mata el worker entre la escritura y el punto de control.

Ante un SIGTERM, cada worker de cola se vacía bajo un único plazo compartido de quince segundos, y el proceso sale a los veinticinco segundos, siga lo que siga en marcha; systemd le da cuarenta y cinco antes de matarlo. Una ejecución que no terminó dentro de esa ventana sigue marcada como en curso, y el siguiente worker la recoge mediante la recuperación de trabajos atascados de la cola. Ante una terminación forzada no hay vaciado alguno. En ambos casos se cumplen los mismos dos hechos. El destino tiene nuestro contacto. Nuestro cursor sigue diciendo c3.

La repetición

La siguiente ejecución empieza en c3, lee la misma página, deriva la misma clave para el registro 212 y consulta el registro de escrituras. El registro dice: completada. No se llama al destino; se reproduce el resultado almacenado y la ejecución sigue adelante. En la ruta masiva, donde el endpoint de importación de Brevo recibe una página entera en una sola llamada, la misma comprobación se ejecuta por clave antes de construir la solicitud. Una página en la que todas las claves ya están completadas no envía nada.

Ese es todo el truco, y vale la pena explicar por qué funciona. La clave se deriva del cursor de la página al inicio del lote, no de la ejecución que estuviera en marcha en ese momento. Un fallo, un reintento sesenta segundos después y una ejecución programada mañana derivan todos la misma clave para el mismo registro. Al registro de escrituras le da igual cuál de ellos pregunte.

Ahora, el coste. El registro de escrituras olvida a los siete días. El propio comentario del código dice que siete días cubren un reintento retrasado y una caída breve, y no afirma nada más. Una repetición desde un cursor más antiguo vuelve a escribir. Y una ejecución interrumpida de forma forzada puede tener que esperar. La concesión temporal de la ejecución muerta dura hasta quince minutos, y en una sincronización programada, una ejecución cuya concesión temporal está libre solo se barre y se reprograma una vez que empezó hace más de una hora. La demo de Crusoe vuelve a funcionar en menos de quince minutos. La nuestra, no siempre.

Quién es dueño de la sincronización mientras se reinicia

Una sola ejecución es dueña de una sincronización a la vez. La ejecución toma una concesión temporal en Redis con su propio token, una vida de quince minutos y una renovación cada cinco minutos. Antes de cada punto de control, verifica que sigue teniendo esa concesión. Si no la tiene, se detiene. La ejecución se registra con la frase sencilla que leerá la persona: “The run lost its slot part-way through and stopped to avoid writing twice.” (La ejecución perdió su turno a mitad de camino y se detuvo para no escribir dos veces). Una segunda ejecución que choca con una dueña activa no se reintenta. Termina con “Another run of this sync was already in progress” (Ya había otra ejecución de esta sincronización en curso), y la siguiente ejecución programada empieza con normalidad.

La responsabilidad de la solución también está escrita. Cada código de fallo se asocia a una frase sobre lo que pasó, otra sobre qué hacer a continuación, y un responsable: tú o Tajo. Un destino que rechazó todos los registros porque el mapeo está mal es cosa tuya. Una concesión temporal perdida es cosa nuestra. La página de la ejecución muestra el siguiente paso; la guía de recuperación de sincronizaciones indica cuál.

Hasta aquí, la mitad fácil. Un fallo cuesta una repetición y nada llega dos veces. Dos incidentes nos enseñaron la otra mitad.

Fallo tras fallo

Una sincronización falló una y otra vez. No en un bucle cerrado: una vez por intervalo del planificador, ejecución tras ejecución, con el mismo mensaje cada vez, que un id personalizado no puede contener dos puntos. La ejecución nunca llegó al destino. Moría mientras se ponía en cola, antes de que ningún ejecutor la viera, así que una alerta basada en el ejecutor nunca tuvo nada en lo que basarse.

Nada vigilaba la racha. Ese es todo el hallazgo.

La solución es un disyuntor. Después de cada fallo registrado, lee las últimas cinco ejecuciones terminadas de la sincronización desde su último cambio. Si las cinco son errores, pausa la sincronización, escribe una fila de auditoría con el motivo circuit_breaker_tripped y lanza una alerta crítica. La racha se deriva de las ejecuciones almacenadas, no de un contador en el worker. Cuenta un fallo registrado por cualquier vía, incluido el fallo al poner en cola que causó el incidente. El comentario del código dice que habría detenido ese incidente en la quinta ejecución. Un solo éxito reinicia la racha. Quitar la pausa rearma el disyuntor en lugar de volver a dispararlo con las filas antiguas. La guía del conector de Shopify dice lo mismo en una línea: tras cinco ejecuciones fallidas consecutivas, la sincronización se pausa sola y su página muestra el último error.

Lo que el disyuntor no hace es arreglar nada. Crusoe sustituye el nodo y reinicia el trabajo sin preguntar. Tajo se detiene y pregunta: una sincronización pausada sigue pausada hasta que una persona corrige la causa y la vuelve a publicar. Este valor prudente por defecto es deliberado: un reintento automático de una sincronización que escribe en registros de clientes es, en sí mismo, una escritura.

El silencio que nadie vio

La objeción es justa y suena así: “Un disyuntor que pausa tras cinco fallos también habría detectado el segundo incidente. ¿Para qué construir un segundo vigilante?”. Porque el segundo incidente no parecía un fallo. Durante un largo periodo, un espacio de trabajo tuvo una sincronización activa, una pila creciente de filas de error y ni un solo éxito, nunca, y nada alertó. Cada señal existente se basaba en actividad: una ejecución falló, llegó un webhook, se superó un presupuesto. Cero éxitos no es un evento. Es la ausencia de uno, y un disyuntor solo se ejecuta cuando se registra un fallo. Un planificador muerto, una cola atascada, un worker que nunca arranca: nada de eso registra nada, y un disyuntor nunca despierta.

El dispositivo de hombre muerto de una locomotora funciona al revés. El maquinista tiene que seguir pulsándolo, y el silencio detiene el tren. La versión para sincronizaciones se ejecuta una vez por hora, a y trece. Para cada espacio de trabajo con una sincronización activa hace una sola pregunta: ¿ha tenido éxito alguna ejecución en las últimas veinticuatro horas? Un espacio de trabajo cuya sincronización activa más antigua se activó hace menos de un día se deja en paz; todavía no ha tenido un día para tener éxito. Uno más antiguo y en silencio recibe una alerta crítica. La alerta dice de qué silencio se trata: ejecuciones registradas pero ninguna con éxito, o ninguna ejecución registrada. Salta una vez por incidente. Un marcador en Redis la mantiene cerrada hasta la primera ejecución con éxito y después se borra, de modo que el siguiente incidente vuelve a alertar.

Los dos vigilantes llegaron en un mismo commit, a posteriori. El comentario del disyuntor dice que habría detenido el primer incidente en la quinta ejecución. El del hombre muerto dice que una sincronización activada hace mucho y con cero ejecuciones debe alertar. Ninguno existía cuando hizo falta. Los escribimos después, con los dos incidentes anotados en la cabecera del archivo para que nadie olvide por qué.

Qué está construido y qué no

Construido, en el momento de escribir esto:

  • El cursor se guarda al final de cada lote sin errores, y otra vez ante un fallo.
  • La clave de entrega de cada registro se deriva del cursor de la página y de su posición, y se comprueba en un registro de siete días antes de cada llamada al destino.
  • Una concesión temporal por sincronización, con una vida de quince minutos.
  • Cinco intentos por ejecución, con una espera creciente a partir de sesenta segundos.
  • Un disyuntor que pausa la sincronización tras cinco fallos consecutivos y registra el motivo.
  • Una alerta horaria tras veinticuatro horas sin un éxito.

No construido, y conviene saberlo antes de confiar en ello:

  • El hombre muerto vigila el espacio de trabajo, no la sincronización. Una sincronización sana oculta a una hermana silenciosa en el mismo espacio de trabajo.
  • Su alerta llega a los operadores de Tajo: siempre una línea de log, y Sentry, un webhook o un email donde estén configurados. No aparece en la página de la sincronización. La pausa del disyuntor, sí.
  • Nada reinicia una sincronización pausada. Lo hace una persona.
  • La recuperación tras una terminación forzada no es rápida. La concesión temporal puede durar quince minutos y el barrido espera una hora.
  • Los dos umbrales, cinco fallos y veinticuatro horas, son ajustes del despliegue. No puedes configurarlos por sincronización.

Una larga cadena de pasos casi perfectos sigue fallando; la salida es definir el estado final y comprobarlo. Este artículo es la mitad operativa de eso. Define lo que deja tras de sí una ejecución muerta, y comprueba la nada que deja el silencio.

La frase de cierre de Crusoe fue que “los fallos son inevitables”. La nuestra va un paso más allá. El fallo fue la mitad fácil, porque dejó filas. El silencio fue la mitad que tuvimos que salir a buscar.

Preguntas frecuentes

¿Qué le pasa a una sincronización en curso cuando el worker muere a mitad de un lote?

El cursor sigue apuntando al inicio del lote interrumpido, porque Tajo solo lo mueve cuando un lote llega sin errores. La siguiente ejecución vuelve a leer la misma página. Cada registro lleva una clave de entrega derivada del cursor de la página y de su posición, y un registro de escrituras conserva cada escritura completada durante siete días, así que los registros ya escritos se reproducen desde ese registro en lugar de enviarse por segunda vez al destino.

¿Por qué una sincronización repetida no escribe dos veces el mismo contacto?

Antes de cada llamada al destino, la ejecución comprueba la clave de entrega del registro en un registro de escrituras. La clave es un hash de la sincronización, la fuente, el cursor de la página, la posición en la página y el propio registro, así que un fallo, un reintento un minuto después y una ejecución programada al día siguiente derivan la misma clave para el mismo registro. Una clave ya marcada como completada dentro de la ventana de siete días no se vuelve a enviar.

¿Qué es una alerta de hombre muerto para una sincronización?

Una comprobación que salta ante una ausencia y no ante un fallo. Una vez por hora, Tajo revisa cada espacio de trabajo con una sincronización activa y alerta cuando ninguna ejecución ha tenido éxito en las últimas veinticuatro horas, incluido el caso en que no se registró ninguna ejecución. Salta una vez por incidente y se rearma tras la primera ejecución con éxito. Existe porque una alerta de fallo solo puede ver los fallos que quedaron registrados.

Solicita acceso anticipado

Escribe tu nombre y tu dirección de correo electrónico. Te enviaremos un enlace para confirmar tu solicitud de acceso a Tajo.

Obtener Brevo

Esta página en Markdown: worker-died-sync-didnt.md

Cargando Markdown…
Abrir en Claude Abrir en ChatGPT