Blogue ·
Um null que passou pelo crivo
Todos os contentores reais diziam que a cobertura da cópia de segurança não estava apurada. A configuração foi lida. O fio dizia null, o esquema dizia ausente.
Por Provibr
Estávamos a construir o separador de hardware dos contentores quando uma verificação de navegador produziu uma frase que nunca deveria ter produzido: não foi possível ler a configuração deste contentor, por isso não está apurado o que uma cópia de segurança contém.
A configuração tinha sido lida. Estava na resposta, completa, com todos os pontos de montagem lá dentro. O que falhara era a nossa própria análise dela, uma camada mais à frente, e a mensagem culpava o lado errado do fio.
Recuar a partir dali demorou algum tempo, porque não havia nada a partir do qual recuar. Nenhuma exceção, nenhuma linha de registo, nenhum teste a falhar. Apenas uma frase serena num ecrã a dizer que uma coisa não tinha sido medida.
Uma chave ausente e um null não são a mesma coisa
O agente constrói as linhas dos pontos de montagem em Rust com serde_json::json!, e um dos campos é um Option<String>: o caminho dentro do contentor, que vem do mp=/data na configuração.
Um Option que é None serializa ali como null. Não como uma chave em falta. Esse é o comportamento correto e documentado e é o que qualquer leitor daquela macro esperaria se pensasse nisso por um segundo, coisa que ninguém tinha feito.
Do outro lado o esquema dizia .optional(), e em zod um optional simples significa que a chave pode estar ausente. Não significa que o valor pode ser null. São duas afirmações diferentes sobre JSON e nós tínhamos escrito uma enquanto enviávamos a outra.
O campo que fez isto disparar em todos os contentores sem exceção é aquele que está estruturalmente sempre vazio. Toda a configuração de contentor tem uma linha rootfs, e um rootfs por definição não tem mp=, porque está montado na raiz. Portanto a primeira linha de cada observação de cada contentor real transportava "mountPath": null.
Um campo mau significa nenhuns dados
Se a falha se tivesse confinado àquele campo teria sido um defeito cosmético. Não foi, e a razão está na forma da análise e não na forma do erro.
Validamos a observação inteira como um só objeto. Um safeParse sobre um objeto falha como um todo; não há resultado parcial. Portanto um campo rejeitado significava nenhuma lista de montagens, nenhuma classificação de cobertura e nenhuma pré-visualização daquilo que uma reposição substituiria. Tudo a jusante viu nada, e tudo a jusante está escrito para ser honesto acerca de nada.
Essa honestidade é deliberada e é a razão pela qual o produto não diz que uma cópia de segurança está completa quando não foi ver. Uma configuração de contentor sem um rootfs legível é comunicada como desconhecida e não como completa, precisamente para que uma medição vazia nunca possa ser vestida de boa.
Aqui a guarda funcionou exatamente como foi desenhada e produziu uma frase que era, nos seus próprios termos, verdadeira: nada tinha sido apurado. A mentira estava um nível acima, na afirmação sobre o porquê.
Nada estava vermelho, e a razão é incómoda
Isto foi entregue com a ronda que introduziu a cobertura das cópias de contentores, e os dados semeados de três rondas passaram pelo mesmo código sem que nada ficasse vermelho.
A razão é que as sementes foram escritas à mão. Para exercitar a tabela de cobertura sem um Proxmox a sério, escreve-se uma linha parecida com aquilo que o agente envia, e o que se escreve quando se quer dizer este não tem caminho é deixar a chave de fora. O que passa na análise. Três rondas de dados de teste codificaram portanto a suposição do esquema e não o comportamento do agente, e todas elas estavam verdes.
Não há hipervisor a sério no circuito para corrigir isso. O Proxmox a que temos acesso é a máquina de produção de alguém, e nenhum agente nosso lá vai. Portanto a única descrição do fio contra a qual os dois lados podiam ter sido verificados era a fonte Rust, e nada a estava a ler.
A guarda lê o produtor em vez de o copiar
A reparação é um campo. A parte interessante é o teste ao lado dela.
Abre o lxc.rs, encontra a função que constrói as linhas, extrai os nomes das chaves do bloco json!, e depois afirma duas coisas. Primeiro, que o conjunto de chaves que o agente envia é exatamente o conjunto de chaves que o esquema conhece, para que nenhum dos lados possa ganhar um campo sozinho. Segundo, e é esta que importa, que uma linha em que todos os campos opcionais são null passa pelo crivo.
Esse segundo teste constrói a sua linha a partir das chaves que acabou de ler, e não de uma lista escrita no ficheiro de teste. Um campo opcional novo acrescentado com um .optional() simples fica vermelho no dia em que é acrescentado, sem que ninguém tenha de se lembrar deste artigo.
Há um terceiro teste para a direção contrária: uma linha de um agente mais antigo, que não transporta de todo os campos mais recentes, continua a passar. Ambas as formas são reais, ambas estão na base de dados, e uma correção que só aceitasse a nova teria partido as linhas antigas parecendo correta.
A correção em si é um preprocess que mapeia null para undefined em vez de um .nullable(). É uma escolha deliberada sobre o tipo derivado: com nullable o tipo passa a ser string | null | undefined e cada leitor ganha um terceiro estado que não lhe diz nada. Com preprocess o tipo fica como estava, os quatro leitores mantêm o código que já tinham, e a tolerância do fio fica expressa exatamente num sítio.
Dois erros na medição, não no produto
Estabelecer que a reparação funcionava produziu dois falsos verdes seus, e são a metade mais transferível desta ronda.
O primeiro: um esquema escrito com .strict() rejeitou em silêncio a nossa própria semente, e o ecrã ficou vazio em vez de errado. Um ecrã vazio durante uma reparação é exatamente igual a um ecrã que ainda não carregou.
O segundo é pior e é uma armadilha conhecida aqui em que caímos na mesma. A tabela de cópias de segurança vive num painel que está sempre montado no documento, porque é assim que se faz funcionar o retorno do foco de uma caixa de diálogo. Portanto uma verificação que lê a tabela com um seletor de consulta lê-a esteja ela aberta ou não. A nossa afirmação de que dois separadores diziam a mesma coisa estava a passar sobre duas tabelas que estavam ambas vazias e ambas invisíveis.
Ambos foram fechados da mesma maneira: exigir que a caixa de diálogo esteja genuinamente aberta, e pôr um limite inferior ao número de linhas antes de as comparar. Uma comparação de nada com nada não é uma comparação.
O que registaríamos
- Uma chave ausente e um
nullsão afirmações diferentes, e os serializadores de JSON escolhem por si. - Um esquema que só aceita uma chave em falta nunca leu o seu produtor.
- A validação do objeto inteiro transforma um campo mau em nenhuns dados, o que é o desenho certo e uma falha má de depurar.
- A mensagem de erro mais perigosa é a serena. A nossa era honesta quanto ao resultado e errada quanto à causa.
- Os dados de teste escritos à mão codificam as suas suposições, incluindo aquela em que está prestes a errar.
- Deixe a guarda ler o produtor. Uma lista de chaves copiada para um teste deriva; uma lista de chaves extraída da fonte não pode.
- Teste explicitamente a linha toda a null, porque é essa a linha que o seu produtor real envia primeiro.
- Aceite também a forma antiga. As linhas escritas por agentes mais antigos continuam na base de dados.
- Tolere no fio e não no tipo. Alargue na fronteira e deixe os leitores em paz.
- Um painel sempre montado torna uma verificação do DOM verde por vacuidade. Afirme que está aberto e que tem linhas.
Para que fique claro o que isto é: o defeito estava na cobertura de todos os contentores reais desde a ronda que entregou as cópias de contentores, encontrado por uma verificação de navegador e não por qualquer bateria de testes, reparado com um só preprocess e guardado por um teste que lê a fonte Rust; a ronda em que foi encontrado levou o módulo de testes dos contentores de trinta e cinco testes para trinta e nove, com musl e formatação limpos, e terminou com os pacotes partilhados a 5347 verdes, o gateway a 1467, as ferramentas do simulador a 489 e a bateria de testes da web a 4593, sem falhas em lado nenhum, e cinquenta verificações de navegador verdes em duas línguas sem achados de acessibilidade. Por provar: nada disto correu contra um Proxmox a sério, portanto a forma do fio está estabelecida a partir da fonte e não de uma resposta em direto, e a cadeia em funcionamento foi exercitada contra o nosso próprio simulador.
Do trabalho de plataforma na Provibr. Para que serve a tabela de cobertura está em O que uma cópia de segurança não contém, a ronda que leu pela primeira vez a configuração de um contentor está em Reinstalar um contentor é uma substituição, e outra verificação que estava verde porque nunca lá chegou está em Verde porque não conseguia lá chegar.