4dim / Notas

Un temporizador en el servidor no sobrevive a un despliegue

Cada despliegue reemplaza el proceso, y con él mueren todos los setTimeout que tenía en memoria. La noche en que una campaña de 300 contactos esperó hasta la mañana, y las dos alternativas: avanzar al leer, o un timer fuera del proceso con cerrojo en Postgres.

El temporizador que muere con el proceso

Si mandas campañas o mensajes programados desde un sistema propio, esta nota explica por qué a veces no salen. El culpable suele ser una línea de código que parece inofensiva.

Es la forma más natural de programar algo que pasa después: «dentro de treinta segundos, marca este mensaje como entregado». Un setTimeout en el servidor, que es la orden de hacer algo más tarde.

Funciona en la máquina de quien lo escribe, y funciona en producción hasta el primer despliegue. Un despliegue reemplaza el proceso, y con el proceso se van todos los temporizadores que tenía en memoria. Lo que iba a pasar en treinta segundos no pasa nunca, y no hay error que lo diga.

Con azul/verde, que es desplegar levantando una copia nueva y cambiando el tráfico a ella, eso ocurre varias veces al día. Cualquier estado que dependa de un temporizador vivo queda a medias en cada despliegue, sin nadie que lo rescate. La regla que sacamos de ahí es simple: en el servidor no se programa nada «para dentro de un rato».

Avanzar al leer

La primera alternativa es no tener temporizador. El estado avanza en el momento en que alguien lo consulta.

La cuenta de demostración imita los tiempos de WhatsApp: enviado a los 2 segundos, recibido a los 6, leído a los 25. Esa escalera no la mueve ningún reloj. Se calcula al pintar la bandeja, mirando cuánto tiempo ha pasado. Si nadie mira, no avanza, y da igual: nadie lo iba a ver.

El despacho de los mensajes que salen empezó igual. Al pintar la bandeja, el navegador le pedía al servidor que sacara la cola. Como contestar y que el mensaje salga ocurren casi seguidos, funcionaba. Y el sondeo de la bandeja, que se repite solo, hacía de red de seguridad.

La noche de los trescientos contactos

Hasta que apareció el caso que «avanzar al leer» no cubre. La tienda cierra, se apaga el computador, y una campaña de trescientos contactos se queda encolada hasta la mañana siguiente. No hay ninguna pestaña abierta que dispare el despacho.

Un sistema que solo trabaja mientras alguien lo mira es rápido cuando lo estás mirando, y nada más.

Un CRM de WhatsApp que se ve rápido con la pestaña abierta no garantiza nada con la pestaña cerrada. Y las campañas se programan para cuando nadie está.

El temporizador que sí sobrevive

La segunda alternativa es un temporizador que vive fuera del proceso: un timer de systemd, el reloj del propio sistema operativo. Cada 60 segundos ejecuta el mismo script de despacho, haya o no una sesión abierta.

Sesenta y no menos, porque el cerrojo haría que la mitad de los pases no hicieran nada. Sesenta y no más, porque un mensaje encolado justo después de un pase esperaría demasiado para algo que el negocio vive como inmediato.

Tres decisiones que costaron un error cada una:

  • El script corre desde un enlace que siempre apunta a la copia viva del azul/verde, no desde el clon del repositorio. En el clon no se reinstalan dependencias, y un proceso con node_modules de hace tres semanas falla de formas que no se parecen a nada.
  • Tampoco desde una copia fija: acertaría la mitad de las veces.
  • El servicio tiene un tope de 110 segundos. Si un pase se cuelga hablando con Meta, systemd lo corta antes de que el siguiente lo encuentre vivo.

Dos cerrojos y un presupuesto

Con un timer y, además, el despacho al leer, el mismo mensaje puede caer en dos procesos a la vez. El cerrojo está en Postgres, la base de datos, y no en un archivo. La consulta que toma mensajes pendientes usa FOR UPDATE SKIP LOCKED, que quiere decir: sáltate las filas que otro ya tomó. Y en la misma sentencia marca la fila como reservada. Dos procesos que llegan juntos toman filas distintas.

La reserva dura dos minutos, a propósito más que los diez segundos de plazo de una petición a Meta. Así un segundo pase no reintenta algo que todavía está en camino.

Y cada pase tiene un presupuesto de 50 segundos y una tanda pequeña por bandeja. Queda por debajo del minuto del timer, para que una carrera nunca choque consigo misma. Un negocio que falla no arrastra a los demás: las credenciales y los fallos son por bandeja.

Qué hacer entonces

  • Buscar los setTimeout y setInterval del servidor. Cada uno es un estado que se pierde en el próximo despliegue.
  • Lo que puede avanzar al leer, que avance al leer.
  • Lo que tiene que pasar aunque nadie mire, a un temporizador externo: systemd o cron, con un tope de tiempo menor que su intervalo.
  • Correr desde la copia viva, nunca desde el clon.
  • El cerrojo en la base, con SKIP LOCKED y una reserva más larga que el plazo de red.
  • Preguntarle a cualquier CRM: «si cierro la pestaña a las 6 de la tarde, ¿a qué hora sale la campaña programada para las 8?».

Esta nota sale de nuestro trabajo en Software a medida.

← todas las notas