Cois To Avoid, Episode 1: INSERT IGNORE
Código de legado ataca de volta
Confiar em funcionalidades de linguagem não documentada é bastante arriscado. E quando está misturado com INSERIR IGNORE as coisas vão muito mal.
Durante uma das atualizações do sistema operacional no início de 2016, Perl foi atualizado de 5.14 para 5.18. Infelizmente, o manuseio de hashes mudou, a ordem das chaves em um cenário bem específico não foi mais mantida e o código quebrou. Você está certo, Perl nunca prometeu a ordem, mas nosso código estava confiando em algumas especificações de implementação.
Um trecho do Perl changelog para a v5.18:
Por padrão, duas variáveis de hash distintas com chaves e valores idênticos podem agora fornecer seu conteúdo em uma ordem diferente onde antes era idêntico.
Por que foi um problema? O código estava preparando uma lista de hashes a serem inseridos na base de dados. Depois usou o primeiro item para preparar a lista de nomes de colunas a serem passadas para INSERT queries que se seguirão. No entanto, cada consulta INSERT enviada estava usando hashes diferentes para preparar a lista de valores a serem inseridos. Isto levou ao seguinte:
INSERT IGNORE INTO foo (id, date) VALUES (1, ‘2006-10-11’), (‘2007-12-09’, 2), …
Basicamente, a ordem dos nomes das colunas e respectivos valores não foi mantida. Claro, o MySQL normalmente levanta um erro quando se tenta inserir uma data na coluna inteira. Mas você se lembra da citação da documentação do MySQL, certo? Estes erros não são levantados quando INSERT IGNORE é utilizado e a conversão de dados silenciosa ocorre:
Data consistentemente inconsistente
O script estava a correr bem após a actualização do Perl (bem, pelo menos a sua stderr estava silenciosa), mas após alguns dias recebemos um relatório de bug que algo está errado com as nossas ferramentas internas que utilizam tabelas alimentadas pelo Perl. A julgar pelo relatório, alguma coisa era bastante suspeito. E nossas preocupações foram comprovadas por uma rápida consulta SELECT:
Então o que temos aqui. Carimbo da hora e valores inteiros convertidos em string e inseridos como nome de usuário, anos armazenados em colunas inteiras (é a contagem das edições de 2016 ou um ano da última edição). Inconsistência de dados no seu melhor.
Você não precisa concordar, mas para mim nenhum dado é melhor que “dados” como linhas acima.
A tabela afetada pesa mais de 75 GiB e armazena linhas de 460 mm. Podemos regenerá-los, mas é um processo longo. O script está rodando enquanto você lê esta história. Bem, já está a correr há cinco semanas… Tudo graças ao IGNORE numa única consulta.
Lessons learned
- INSERT IGNORE é uma besta silenciosa apenas à espera que o seu erro transforme os dados num conjunto de valores sem sentido.
- Nunca ignore os erros. Faça-os barulhentos. Falhe cedo.
- Confiar em funcionalidades de linguagem não documentada (Perl e hash key é apenas um dos numerosos exemplos) é um jogo arriscado.
Você foi avisado. Agora verifique seu código ou ngrep seu tráfego para consultas INSERT IGNORE.