Pulsar Publicar dos veces ya publica una sola vez
Tres peticiones de publicación de la misma app llegaron a nuestra API con 84 milisegundos de diferencia, y cada una inició un despliegue completo de esa app. Compitieron entre sí por el mismo alojamiento, varias de las versiones resultantes fallaron porque otra había llegado antes, y la única señal visible fue un correo de «la publicación ha fallado»: la app acabó bien. Ahora, una publicación que llega mientras otra se está ejecutando para la misma app no inicia una segunda compilación. La base de datos decide qué petición gana, y a las demás se les dice la verdad: ya hay una publicación en curso.
Qué pasó
Cuando publicas una app en Mythex, la API registra un despliegue para ella y pide a nuestro orquestador —el servicio que ejecuta las compilaciones— que la compile y la publique. La publicación puede iniciarse desde el botón Publicar, desde otra pestaña del navegador, o desde el agente cuando le pides que publique. Un doble clic, dos pestañas o una petición del agente que llega dos veces pueden generar más de una publicación para la misma app casi al mismo tiempo.
En este caso llegaron tres en 84 ms. Cada una ejecutó un despliegue completo. En la plataforma de alojamiento, se crearon versiones de la misma app en el mismo segundo, y tres fallaron porque las otras ya estaban en curso. Una versión posterior se quedó atascada, y la app funcionó después.
Eso es lo que hizo fácil pasarlo por alto. El usuario vio una app que funcionaba. Los fallos solo aparecieron como correos de error de publicación.
Por qué nada lo detenía
El código de publicación sí tenía un paso que miraba los despliegues en curso del proyecto. Pero lo que hacía era marcar como fallido cada despliegue en curso antes de empezar —«reemplazado por una nueva publicación»—. Estaba pensado para limpiar compilaciones que quedaron colgadas tras un proceso caído.
En la práctica, eso significaba que una segunda publicación que llegaba a mitad de compilación declaraba muerta a la primera y empezaba la suya. Ambas se ejecutaban hasta el final contra la misma app. No había ningún cerrojo; había un paso que despejaba activamente el camino para una carrera.
Había además un síntoma más extraño en la primera publicación de una app. Sin un registro de despliegue que reutilizar todavía, dos primeras publicaciones concurrentes intentaban insertar uno. La base de datos tiene un índice único sobre el nombre del enlace de la app (su slug, como tu-app en tu-app.mythex.ai), así que la segunda inserción fallaba. La API traducía eso como «ese nombre de enlace ya está en uso», y aconsejaba al usuario renombrar una app que, en ese momento, se estaba publicando correctamente con ese nombre.
La solución: la reclamación es el cerrojo
Reclamar el registro de despliegue ahora es una única actualización condicional en la base de datos: toma esta fila y márcala como compilando solo si nadie la está publicando. Como es una sola sentencia, la base de datos resuelve las peticiones que llegan en el mismo milisegundo. Exactamente una de ellas cambia la fila. Las demás ven que no se ha cambiado ninguna fila, se detienen, y nunca llegan al orquestador.
Un cerrojo también necesita una salida cuando quien lo tiene muere. Si la API se reiniciaba a mitad de una publicación, la fila quedaría marcada como «compilando» para siempre y la app no podría volver a publicarse. Por eso, una fila cuyo dueño ha dejado de enviar latidos —la publicación en curso actualiza la fila cada 30 segundos, y cinco minutos de silencio cuentan como muerte— puede volver a reclamarse. Ese umbral sale del latido, no de una suposición sobre cuánto dura una compilación. Compilar un Dockerfile propio del usuario puede tardar mucho de forma legítima, y una compilación lenta que sigue enviando latidos conserva su reclamación.
El paso que marcaba como fallidos todos los despliegues en curso ya no se ejecuta antes de publicar. Solo se limpian las filas que dejó un proceso muerto.
Decirle la verdad al que pierde
La petición que pierde ahora recibe una respuesta clara: publish_in_progress, junto con el ID del despliegue que se está ejecutando. Cada lugar que puede iniciar una publicación lo gestiona:
- El espacio de trabajo sigue la compilación que ya está en marcha en lugar de mostrar un error encima.
- El agente recibe el aviso de que ya hay una publicación de esta app en curso y debe esperar, en lugar de que se le diga que algo falló —lo que antes podía llevarle a «arreglar» un Dockerfile y reintentar—.
- Una colisión en la primera publicación por el nombre del enlace se comprueba: si la fila que tiene el nombre es la publicación en curso del propio proyecto, la respuesta es «ya se está publicando», no «ese nombre ya está en uso». Una colisión real con otro proyecto sigue mostrando el mensaje para renombrar.
Cómo lo comprobamos
Escribimos un script de reproducción que ejecuta tres publicaciones de una app a la vez contra una base de datos real y un orquestador simulado, y cuenta cuántos despliegues se piden al orquestador. Cubre una primera publicación y una republicación de una app en línea. Antes del arreglo, contaba dos despliegues. Después, uno.
El script también cubre la salida de emergencia: marca la fila de la app como compilando sin latidos durante diez minutos, vuelve a publicar y comprueba que la nueva publicación reclama la fila y se ejecuta.
Las pruebas unitarias mantienen el cerrojo a las dos cosas que debe hacer: de tres republicaciones simultáneas, exactamente una pasa y las otras dos reciben el aviso de que hay una publicación en curso; y una compilación cuyo dueño murió puede reclamarse. Otra prueba comprueba que un nuevo intento no arrastra el mensaje de error del intento anterior.
Lo que aprendimos
- El código de limpieza puede ser una carrera disfrazada. «Falla todo lo que esté en curso y luego empieza» parece higiene. Con dos llamadores, es cada uno cancelando al otro.
- Deja que la base de datos arbitre. Una lectura seguida de una escritura deja un hueco para otra petición. Una única actualización condicional no lo deja.
- Todo cerrojo necesita un vencimiento ligado a la actividad. Sin él, un fallo se convierte en una app atascada.
- Los mensajes de error son instrucciones. «Ese nombre ya está en uso» llevaba a la gente a hacer lo incorrecto. El mensaje correcto era más sencillo y cierto.
Más sobre publicar en la documentación: cómo publicar. Y un arreglo relacionado: tu app publicada sigue activa mientras la republicas.