Alertas que se disparan una vez, no una vez por servidor
Una de nuestras alertas operativas —“créditos bajos” para la app publicada de un creador— está pensada para llegarnos como máximo una vez al día. Una tarde llegó cinco veces. La causa: cada copia de nuestra API recordaba en su propia memoria qué alertas ya había enviado, así que dos servidores la enviaban dos veces, y cada despliegue borraba esa memoria y permitía que volvieran a enviarla. El mismo patrón hacía que un trabajo en segundo plano hiciera su tarea dos veces. Ahora ambos se coordinan a través de un almacén compartido, y los dos fallan del lado seguro si ese almacén no está disponible.
El síntoma
Mythex envía alertas al equipo cuando algo necesita atención: un servicio caído, un trabajo atascado, o el saldo de créditos de un creador bajo mientras su app está publicada. Cada alerta tiene una ventana de deduplicación: un periodo durante el cual la misma alerta solo se envía una vez. Para la alerta de créditos bajos, esa ventana es de 24 horas.
Cinco copias de la misma alerta en una tarde significaban que la ventana no estaba funcionando. Peor aún, llegaban en parejas, separadas unos 11 minutos.
Por qué pasó
Nuestro paquete de alertas guardaba la deduplicación en memoria: una tabla de “clave de alerta → cuándo se envió por última vez”, dentro del proceso en ejecución. Eso funciona cuando hay un solo proceso que nunca se reinicia. Producción no cumple ninguna de las dos cosas:
- Dos réplicas de la API. Producción ejecuta dos copias de la API. Cada una tenía su propia tabla, así que cada una enviaba la alerta una vez. Ahí están las parejas.
- Despliegues. Cada despliegue reemplaza ambas réplicas por procesos nuevos con tablas vacías. Tras un despliegue, la alerta salía de nuevo, como si nunca se hubiera enviado.
Los ~11 minutos entre las dos alertas de cada pareja venían del trabajo que genera esta alerta. Se ejecuta en ambas réplicas y toma un bloqueo, así que las réplicas se turnan, y cada una generaba la alerta cuando le tocaba.
Había un segundo error, más silencioso, en el mismo código. Para evitar que la tabla creciera sin límite, se podaba a sí misma al llegar a 500 claves, borrando cada entrada con más de 15 minutos, sin importar su ventana. Bajo carga, una deduplicación de 24 horas podía durar un cuarto de hora.
La solución
Un registro compartido por alerta
Antes de que la API envíe una alerta, ahora reclama la clave de deduplicación de esa alerta en Redis, un almacén en memoria compartido al que hablan ambas réplicas y que no se reinicia cuando la API lo hace. La reclamación es una única operación de “establece esta clave solo si no existe, y caduca tras la ventana”. La primera réplica que la reclama envía la alerta; la otra encuentra la clave ocupada y se calla. Un proceso nuevo tras un despliegue encuentra la clave todavía ahí.
La clave y la ventana vienen de la misma regla que usa el paquete de alertas, ahora compartida entre ambas capas, de modo que no pueden discrepar sobre qué cuenta como “la misma alerta”.
Fallar abierto, no cerrado
La decisión más importante fue qué hacer cuando no se puede contactar con Redis. Elegimos enviar la alerta de todos modos. Si Redis está caído o no responde en 1,5 segundos, la alerta sale. Una alerta duplicada es ruido. Una alerta descartada es alguien que no se entera.
Las alertas más graves, P0, nunca se retienen por la deduplicación.
Cada entrada conserva su propia ventana
En la deduplicación en memoria del paquete de alertas, cada entrada ahora guarda cuándo termina su ventana, y la poda elimina solo las entradas cuya ventana ya terminó. Una clave de 24 horas se mantiene 24 horas, sin importar cuántas otras alertas lleguen.
El mismo error, en otro lugar
El mismo patrón había duplicado también un trabajo de segundo plano: un proceso de conciliación que mantiene nuestro registro del estado de cada espacio de trabajo alineado con el de nuestro proveedor de sandboxes. Cada pasada lista todos los sandboxes de la cuenta —miles en producción, unos cinco segundos— más cada proyecto en ejecución o pausado. Ambas réplicas lo ejecutaban cada dos minutos.
Sus escrituras son idempotentes —ejecutarlas dos veces da el mismo resultado que una—, así que nada se rompió. Simplemente era el doble de trabajo. Ahora cada ventana de dos minutos se reclama en Redis de la misma forma en que nuestros medidores de uso ya reclamaban las suyas, y solo la réplica que reclama la ventana ejecuta la pasada. Si no se puede contactar con Redis, la pasada se ejecuta igualmente: hacer el trabajo dos veces es inofensivo; no hacerlo, no.
Cómo lo probamos
Las pruebas modelan la configuración real. Cargan el módulo de alertas dos veces, como dos “réplicas” separadas, las apuntan a un mismo almacén compartido, disparan la misma alerta desde cada una y luego cargan una tercera copia nueva para simular “después de un despliegue” y la disparan de nuevo. Se envía exactamente una alerta. Otras pruebas comprueban que la alerta de otro creador sigue saliendo, que un P0 se envía desde ambas réplicas, y que la alerta sigue enviándose cuando el almacén está caído o tarda más que el tiempo de espera. La corrección de la poda tiene su propia prueba: una clave de 24 horas sobrevive a 600 otras alertas que llegan 20 minutos después.
Para el proceso de conciliación, dos réplicas ejecutándose a la vez, más un tercer tick en la misma ventana, producen un único listado.
Lo que aprendimos
- El estado en memoria es por proceso. Todo lo que debe ser cierto entre servidores —“enviada una vez”, “ejecutado una vez”— tiene que vivir en un lugar que compartan.
- Los despliegues son reinicios. El estado que solo sobrevive hasta el siguiente reinicio no sobrevive a una jornada laboral normal.
- Decide de qué forma fallar. En alertas, que un almacén compartido esté caído debe significar más alertas, no menos.
- Busca el patrón, no solo la instancia. La alerta fue el caso ruidoso; el proceso de conciliación duplicado era el mismo error, costando trabajo en silencio.
Nada de esto se ve desde dentro de Mythex. Para saber más sobre cómo mantenemos la plataforma estable, consulta cómo logramos bajar los despliegues a cero errores 502 y el archivo de swap que faltaba. Si es la primera vez que oyes hablar de ejecutar más de una copia de un servicio, nuestra guía sobre qué es un servidor cubre lo básico.