Ir al contenido

Blog ·

La migración que nunca se ejecutó

Un migrador que informa de éxito mientras omite un fichero es peor que uno que revienta. Cómo el orden por marca de tiempo descarta migraciones en silencio.

Por Provibr

Un migrador de esquema que revienta es una buena tarde. Se obtiene una traza, un número de línea y una base de datos que sigue en un estado comprensible.

La mala tarde es aquella en la que el migrador imprime que el esquema está al día, sale con código cero y se equivoca. Esa tarde se ha repetido más de una vez, por más de una causa, y la peor de ellas acabó con código de producción leyendo una columna que no existía.

Esto es lo que aprendimos, escrito para cualquiera que ejecute migraciones de Postgres desde un directorio de ficheros, sea cual sea la herramienta que los generó.

La decisión se toma sobre un solo número

La mayoría de los migradores basados en ficheros mantienen una tabla de lo aplicado. El nuestro también: un esquema, una tabla, una fila por migración con un hash y una marca de tiempo de creación.

El error es suponer que esa tabla es un conjunto que el migrador consulta. No lo es. Al leer el código aparece una consulta que selecciona exactamente una fila, la más reciente por marca de tiempo, y después una única comparación por fichero de migración: ejecutarlo si la última marca aplicada es menor que la marca de este fichero.

De ahí se siguen tres cosas de inmediato, y ninguna está en el modelo mental de nadie.

El hash no participa en la decisión. Se escribe cuando una migración se ejecuta y nunca se vuelve a leer para decidir nada. Un hash de la tabla que ya no coincide con ningún fichero del disco suele significar que ese fichero se editó después de aplicarse, lo cual es poco pulcro pero inofensivo. No es la herramienta dándose cuenta de nada.

Solo importa la fila más reciente. No el conjunto de ficheros aplicados, ni el recuento. Una fila.

El orden es por marca de tiempo, no por nombre de fichero. Los números del principio de los nombres son para las personas. Si la herramienta registra un valor de orden en un manifiesto, ese valor es la verdad y el número es decoración.

Al juntarlo todo aparece la propiedad que cuesta tardes: una migración cuya marca de tiempo no sea mayor que la marca aplicada más reciente no se ejecutará jamás en esa base de datos. Ni en este despliegue, ni en el siguiente, ni tras diez reintentos. No hay error, porque desde el punto de vista de la herramienta no pasa nada.

Cómo se llega a tener una

Suena a algo que no le podría ocurrir a un equipo cuidadoso. Así nos ocurrió a nosotros, dos veces, de formas que no tenían nada que ver con el descuido.

Dos ramas, el mismo número. Dos personas añaden cada una su migración en paralelo. Las dos herramientas reparten el siguiente número libre, las dos obtienen el mismo, las dos escriben una marca de tiempo. La rama que se fusiona en segundo lugar se renombra al número siguiente, que es el arreglo evidente y no es el arreglo completo: si su marca sigue siendo menor que la ya aplicada, el renombrado cambió el nombre del fichero y no la decisión. La migración se salta. El síntoma, en nuestro caso, fue un tipo enumerado al que simplemente le faltaba un valor, en una base de datos donde todos los demás objetos estaban presentes y correctos.

Marcas de tiempo escritas a mano que no estaban en orden. Las nuestras son números redondos puestos a mano en lugar de un reloj real, lo que las hace legibles y las hace fáciles de equivocar. Una migración recibió una marca anterior a dos que ya estaban aplicadas. El migrador informó de éxito. En producción eso habría sido un error de tipo en cada una de las filas de integración, que no es la clase de cosa que conviene descubrir por un cliente.

Los dos casos comparten una forma que merece nombre. La contabilidad se lleva sobre un valor de orden, no sobre una identidad. Un libro llevado por identidad («¿he aplicado esta migración?») no puede saltarse nada, llegue lo que llegue en el orden que llegue. Un libro llevado por orden («¿hasta dónde he llegado?») sí puede, y es un diseño que comparten la mayoría de los migradores porque convierte el camino habitual en la lectura de una sola fila.

El contador no es la comprobación

Tras el primer incidente añadimos la protección evidente: después de migrar, mirar el número de filas de la tabla de migraciones y confirmar que ha subido.

Esa protección es necesaria y no es suficiente, cosa que descubrimos viéndola pasar mientras faltaba justo lo que necesitábamos. Una ejecución de migraciones llevó el recuento de filas de noventa y ocho a ciento cuatro. Entraron seis migraciones. La que nos importaba no estaba entre ellas. El recuento subió, todo informó de éxito, y todas las páginas del producto que requieren sesión devolvieron un 500 por una columna que no existía.

Nada en un recuento dice cuáles migraciones se ejecutaron. Si se quiere saber si un objeto existe, la única comprobación honesta es preguntar a la base de datos si ese objeto existe: listar las tablas, describir la tabla, seleccionar los valores del enumerado. Nosotros comparamos la lista de tablas del entorno que acabamos de migrar contra la del entorno en el que confiamos.

Hay una variante de este fallo que no tiene nada que ver con las marcas de tiempo y produce la misma salida en verde. Nuestro migrador viaja dentro de una de nuestras imágenes de servicio. Alguien reconstruyó solo el otro servicio, ejecutó la migración desde la imagen vieja y obtuvo un alegre «el esquema está al día» de un contenedor que jamás había oído hablar de las dos migraciones en cuestión. La herramienta no mentía. Respondía a una pregunta sobre los ficheros que tenía, y tenía ficheros viejos.

Todo o nada, en una transacción

Un detalle de la implementación de nuestro migrador merece conocerse porque cambia la forma de recuperarse: cada migración pendiente se ejecuta dentro de una única transacción, con la inserción contable de cada una dentro de ella.

Eso es una buena noticia y una restricción. La buena noticia es que un fallo a mitad de un lote de siete deja ninguna de las siete aplicada, en lugar de tres y media. No hay un estado a medio migrar sobre el que razonar, que es exactamente lo que se quiere en el momento en que menos capacidad de razonar hay.

La restricción es que una sentencia que Postgres no puede ejecutar dentro de un bloque de transacción se lleva por delante el lote entero, y que una migración muy larga mantiene bloqueos durante todo el lote en vez de durante su propia duración. Ninguna de las dos nos ha mordido todavía. Las dos son de esas cosas que conviene haber leído antes de que muerdan.

Reparar una sin empeorar la siguiente

Supongamos que aparece: una migración del directorio que esta base de datos nunca ejecutó. El instinto es arreglar el manifiesto, bajar o subir la marca de tiempo hasta que la herramienta la recoja, y volver a ejecutar.

No conviene bajarla, y conviene subirla con cuidado.

El motivo es la misma regla que causó el problema. Si se baja la marca de una migración que ya se aplicó en otro sitio, no se ha conseguido que se ejecute nada; se ha creado un fichero cuya marca queda ahora por debajo de la línea de máxima en las bases de datos donde sí se ejecutó, y la siguiente migración que se escriba con una marca intermedia se saltará allí. Se arregló un entorno y se armó el siguiente fallo en otro.

Lo que funciona es aplicar el SQL a mano, en una única transacción, y después asegurarse de que la contabilidad refleja la realidad. El migrador no puede hacerlo por nadie, porque desde su lado no hay nada que hacer.

Lo que hacemos ahora

Cinco reglas, todas baratas, todas escritas porque no las seguimos una vez cada una.

  1. Desplegar y migrar son una operación, no dos. El hueco entre ambas es una ventana en la que código nuevo lee un esquema viejo. El nuestro nos costó una página de administración para cada administrador y un trabajo de facturación que falló en silencio toda una tarde sobre columnas que todavía no estaban.
  2. Construir todo antes de migrar, no solo el servicio que se cambió. El migrador viaja en una imagen, y una imagen que no se reconstruyó contiene las migraciones que no se añadieron.
  3. Revisar los valores de orden, no los nombres de fichero, siempre que dos personas hayan trabajado en paralelo. Renombrar un fichero no cambia lo que la herramienta compara.
  4. Verificar el objeto, no el contador. Después de una migración que añade una columna, hay que pedir la columna. Cuesta un comando y es la única comprobación que no puede pasar en vacío.
  5. Nunca bajar un valor de orden que ya se aplicó en algún sitio. Parece un arreglo local y es una mina remota.

El hilo incómodo que atraviesa todo esto es que la herramienta se comportó correctamente cada vez. Nunca reventó, nunca corrompió nada, nunca mintió sobre lo que hizo. Respondió a una pregunta más estrecha que la que creíamos estar haciendo, e imprimió una frase que era verdad sobre esa pregunta más estrecha.

Esa es la clase de fallo contra la que merece la pena construir hábitos, en migraciones y en todo lo demás. Una herramienta que falla a gritos enseña lo que hace. Una herramienta que acierta en estrecho no enseña nada hasta el día en que importa.

Empiece con un host y un agente

Cree una cuenta, instale el agente con un solo comando y conecte su primer host de Proxmox. El periodo de prueba dura 14 días y no pide ningún dato de pago.