Blogue ·
A migração que nunca correu
Um migrador que anuncia sucesso enquanto salta um ficheiro é pior do que um que rebenta. Como a ordenação por data omite migrações, e o que verificar.
Por Provibr
Um migrador de esquema que rebenta dá uma boa tarde. Recebe-se um rasto de pilha, um número de linha e uma base de dados que continua num estado compreensível.
A tarde má é aquela em que o migrador escreve que o esquema está atualizado, sai com zero, e está errado. Já tivemos essa tarde mais do que uma vez, por mais do que uma causa, e a pior delas acabou com código de produção a ler uma coluna que não existia.
É isto que aprendemos, escrito para quem corre migrações de Postgres a partir de um diretório de ficheiros, seja qual for a ferramenta que gerou esses ficheiros.
A decisão é tomada com base num único número
A maioria dos migradores baseados em ficheiros mantém uma tabela do que já foi aplicado. O nosso também: um esquema, uma tabela, uma linha por migração com um resumo criptográfico e uma data de criação.
O erro é presumir que essa tabela é um conjunto que o migrador consulta. Não é. Ao ler o código encontra-se uma consulta que seleciona exatamente uma linha, a mais recente por data, e depois uma única comparação por ficheiro de migração: correr o ficheiro se a última data aplicada for inferior à data desse ficheiro.
Daí decorrem imediatamente três coisas, e nenhuma delas está no modelo mental de ninguém.
O resumo criptográfico não tem papel nenhum na decisão. É escrito quando uma migração corre e nunca mais é lido para decidir seja o que for. Um resumo na tabela que já não corresponde a nenhum ficheiro em disco significa normalmente que esse ficheiro foi editado depois de ter sido aplicado, o que é desarrumado mas inofensivo. Não é a ferramenta a reparar em coisa nenhuma.
Só interessa a linha mais recente. Não o conjunto de ficheiros aplicados, não a contagem. Uma linha.
A ordenação é por data, não por nome de ficheiro. Os números à frente dos nomes dos ficheiros são para pessoas. Se a sua ferramenta registar um valor de ordenação num manifesto, esse valor é a verdade e o número é decoração.
Junte-se tudo isto e obtém-se a propriedade que custa tardes: uma migração cuja data não seja maior do que a data aplicada mais recente nunca vai correr, nessa base de dados, nunca. Nem nesta implantação, nem na seguinte, nem ao fim de dez tentativas. Não há erro nenhum, porque do ponto de vista da ferramenta não há nada de errado.
Como é que se chega a ter uma
Parece uma coisa que não podia acontecer a uma equipa cuidadosa. Eis como nos aconteceu, duas vezes, de maneiras que nada tinham a ver com descuido.
Dois ramos, o mesmo número. Duas pessoas acrescentam cada uma a sua migração em paralelo. As duas ferramentas entregam o número livre seguinte, as duas recebem o mesmo, as duas escrevem uma data. O ramo que integrar em segundo lugar é renomeado para o número seguinte, o que é a correção óbvia e não é a correção completa: se a sua data continuar inferior à que já foi aplicada, a mudança de nome alterou o nome do ficheiro e não a decisão. A migração é saltada. O sintoma, no nosso caso, foi um tipo enumerado a que faltava simplesmente um valor, numa base de dados onde todos os outros objetos estavam presentes e corretos.
Datas escritas à mão que não estavam por ordem. As nossas são números redondos definidos à mão em vez de um relógio verdadeiro, o que as torna legíveis e as torna fáceis de errar. Uma migração recebeu um carimbo anterior ao de duas que já tinham sido aplicadas. O migrador comunicou sucesso. Em produção isso teria sido um erro de tipo em cada uma das linhas de integração, que não é o género de coisa que se queira descobrir através de um cliente.
Os dois casos partilham uma forma que vale a pena nomear. A escrituração é indexada por um valor de ordenação, não pela identidade. Um livro-razão indexado pela identidade («já apliquei esta migração?») não consegue saltar nada, chegue o que chegar e pela ordem que chegar. Um livro-razão indexado pela ordenação («até onde é que já cheguei?») consegue, e é um desenho que a maioria dos migradores partilha porque torna o caminho comum uma leitura de uma só linha.
O contador não é a verificação
Depois do primeiro incidente acrescentámos a guarda óbvia: depois de migrar, olhar para o número de linhas na tabela de migrações e confirmar que subiu.
Essa guarda é necessária e não é suficiente, o que descobrimos ao vê-la passar enquanto faltava aquilo de que precisávamos. Uma execução de migração levou a contagem de linhas de noventa e oito para cento e quatro. Entraram seis migrações. Aquela que nos interessava não estava entre elas. A contagem subiu, tudo comunicou sucesso, e todas as páginas com sessão iniciada no produto devolviam um 500 numa coluna que não existia.
Não há nada numa contagem que diga quais migrações correram. Para saber se um objeto existe, a única verificação honesta é perguntar à base de dados se esse objeto existe: listar as tabelas, descrever a tabela, selecionar os valores do enumerado. Comparamos a lista de tabelas do ambiente que acabámos de migrar com a do ambiente em que confiamos.
Existe uma variante desta falha que nada tem a ver com datas e produz a mesma saída verde. O nosso migrador viaja dentro de uma das nossas imagens de serviço. Alguém reconstruiu apenas o outro serviço, correu a migração a partir da imagem envelhecida, e recebeu um alegre «o esquema está atualizado» de um contentor que nunca tinha ouvido falar das duas migrações em questão. A ferramenta não estava a mentir. Estava a responder a uma pergunta sobre os ficheiros que tinha, e tinha ficheiros velhos.
Tudo ou nada, numa transação
Vale a pena conhecer um pormenor da implementação do nosso migrador, porque muda a forma de recuperar: todas as migrações pendentes correm dentro de uma única transação, com a inserção de escrituração de cada uma lá dentro.
Isso é uma boa notícia e uma restrição. A boa notícia é que uma falha a meio de um lote de sete não deixa aplicada nenhuma das sete, em vez de deixar três e meia. Não há um estado meio migrado sobre o qual raciocinar, que é exatamente o que se quer no momento em que se está menos capaz de raciocinar.
A restrição é que uma instrução que o Postgres não consegue correr dentro de um bloco de transação leva o lote inteiro atrás de si, e que uma migração muito longa mantém bloqueios durante o lote inteiro em vez de durante a sua própria duração. Nenhuma das duas nos mordeu ainda. As duas são do género de coisa que se quer ter lido antes de morder.
Reparar uma sem piorar a seguinte
Suponha-se que a encontra: uma migração no seu diretório que esta base de dados nunca correu. O instinto é corrigir o manifesto, baixar ou subir a data até a ferramenta a apanhar, e correr de novo.
Não a baixe, e suba-a com cuidado.
A razão é a mesma regra que causou o problema. Se baixar a data de uma migração que já foi aplicada noutro sítio, não fez nada correr; fez um ficheiro cujo carimbo está agora abaixo da marca máxima nas bases de dados onde ele correu de facto, e a próxima migração que escrever com um carimbo entre os dois será saltada nessas. Corrigiu um ambiente e armou a falha seguinte noutro.
O que funciona é aplicar o SQL à mão, numa única transação, e depois garantir que a escrituração reflete a realidade. O migrador não o consegue fazer por si, porque do lado dele não há nada a fazer.
O que fazemos agora
Cinco regras, todas baratas, todas escritas porque não as seguimos uma vez cada.
- Implantar e migrar são uma operação, não duas. O intervalo entre as duas é uma janela em que código novo lê um esquema velho. O nosso custou-nos uma página de administração para todos os administradores e um trabalho de faturação que falhou em silêncio uma tarde inteira em colunas que ainda não estavam lá.
- Construa tudo antes de migrar, não apenas o serviço que alterou. O migrador viaja numa imagem, e uma imagem que não reconstruiu contém as migrações que não acrescentou.
- Verifique os valores de ordenação, não os nomes dos ficheiros, sempre que duas pessoas tenham trabalhado em paralelo. Mudar o nome de um ficheiro não muda aquilo que a ferramenta compara.
- Verifique o objeto, não o contador. Depois de uma migração que acrescenta uma coluna, peça a coluna. Isto leva um comando e é a única verificação que não pode passar em vazio.
- Nunca baixe um valor de ordenação que já foi aplicado nalgum sítio. Parece uma correção local e é uma mina remota.
O fio incómodo que atravessa tudo isto é que a ferramenta se comportou corretamente de todas as vezes. Nunca rebentou, nunca corrompeu nada, nunca mentiu sobre o que fez. Respondeu a uma pergunta mais estreita do que aquela que julgávamos estar a fazer, e escreveu uma frase que era verdadeira sobre essa pergunta mais estreita.
É essa a classe de falha contra a qual vale a pena construir hábitos, nas migrações e em todo o resto. Uma ferramenta que falha alto ensina o que faz. Uma ferramenta que tem sucesso de forma estreita não ensina nada até ao dia em que interessa.