Comecemos pela parte que mais facilmente se perde ao ler o resto: não foi retirado dinheiro nenhum. Nenhuma carteira foi debitada duas vezes, nenhum cartão foi cobrado, nenhuma dotação de subscrição foi gasta em trabalho que nunca aconteceu.
O que estava avariado era um número num ecrã. Em algumas contas, a repartição do consumo na tua própria página de conta indicava a mais — na pior que medimos, 2,4 vezes o livro-razão que ela deve resumir. O livro-razão esteve certo o tempo todo. Também os tectos mensais, que são lidos de um sítio completamente diferente. Foi um defeito de apresentação, está corrigido, e eis a causa.
Uma chave que procurava algo que ninguém escreveu
O trabalho que surge por causa de algo que fizeste — actualizar um resumo diário de consumo, creditar uma recomendação, difundir uma notificação — não acontece dentro do teu pedido. O pedido escreve uma mensagem e um consumidor em segundo plano apanha-a um instante depois; é isso que impede um efeito secundário lento de tornar lenta uma página rápida.
O perigo dessa disposição é fazer o mesmo trabalho duas vezes, porque um broker volta a entregar de bom grado a mesma mensagem depois de um percalço. A defesa é uma caixa de entrada: antes de processar, um consumidor reclama a mensagem escrevendo uma linha que transporta a chave dessa mensagem, e um índice único torna impossível uma segunda reclamação.
A nossa reclamava sobre um par — o inquilino a que a mensagem pertencia, mais a chave da mensagem — e procurava esse par contra o identificador de inquilino transportado pelo envelope da mensagem, que vem preenchido.
As linhas de reclamação não transportavam identificador de inquilino nenhum. Todas estavam vazias, e estavam-no desde o início. A própria documentação da entidade afirmava, com estas palavras, que a framework o carimbava na inserção. Nunca o fez. Medido contra produção no dia em que o encontrámos: as 3.344 linhas da caixa de entrada vazias, ao passo que 1.174 de 1.451 linhas do lado produtor transportavam um inquilino real — que é precisamente por isso que ninguém suspeitou do lado que lê.
Portanto a procura pedia (inquilino, chave), a linha guardada era (nada, chave), e nunca poderiam corresponder.
Nada falhou, e é por isso que andou semanas
Uma procura que nunca pode corresponder soa a algo que deveria dar erro à primeira tentativa. Fez o contrário.
A reclamação decidiu que a mensagem era nova. A inserção embateu então no índice único e foi rejeitada como duplicado — correctamente, porque a linha já lá estava. Um ramo de recurso apanhou essa rejeição e leu-a com benevolência: outra entrega deve ter reclamado esta, aqui não há nada a fazer. O envelope ficou no estado «ainda por processar», e como ainda estava por processar, a entrega seguinte apanhou-o e fez o trabalho outra vez.
Cada consumidor reprocessava a sua própria pendência, mais ou menos a cada quarenta e cinco segundos, indefinidamente. Vinte e seis faziam-no. Uma linha de produção terminou 333 horas depois de ter chegado — e tinha estado a terminar uma e outra vez durante a maior parte desse tempo.
O que estragou de facto
Desses vinte e seis consumidores, quatro acumulam em vez de sobrescrever. Esses quatro são as projecções de facturação que a página de consumo lê, e foi aí que a coisa veio ao de cima.
Tinham uma protecção explícita contra exactamente isto, escrita depois de um incidente em Agosto em que um único evento foi aplicado nove vezes a um indicador. Essa protecção marca uma linha como bem-sucedida antes de a processar, para que uma reentrega encontre a marca e pare. A marca era escrita correctamente de todas as vezes. Depois a reclamação seguinte procurava-a com a mesma chave avariada e não a encontrava. A garantia esteve vazia desde o dia em que foi lançada.
O que isto não tocou merece ser enumerado com precisão, porque «projecção de facturação» soa alarmante e os factos não são:
- O livro-razão estava correcto. Cada chamada de IA escreve exactamente um registo de facturação com o seu próprio instantâneo de preço, e nada neste caminho os escreve.
- Nenhuma carteira se mexeu. Os saldos vêm do livro-razão, nunca de um resumo.
- Nenhum tecto se mexeu. O guarda do orçamento lê um contador separado, e é esse a única fonte de verdade sobre o que gastaste este mês. Ninguém foi cortado cedo demais por causa de um número inflacionado, e ninguém passou sem dever passar.
A única coisa que lê o resumo afectado é o histórico de consumo na tua própria página de conta — o teu total e a repartição do que foi gasto em quê. É este todo o raio de impacto, e o que fez foi indicar a mais.
A metade para que ninguém olhava
A mesma procura avariada estava num segundo sítio: o caminho que corre quando uma mensagem falha. Aí nunca encontrava a linha que devia actualizar, e mesmo por baixo estava um ramo com o comentário «inalcançável na prática» — que devolvia «esta falha é terminal» em todas as falhas.
Assim o contador de novas tentativas nunca avançou e nenhuma mensagem envenenada chegou alguma vez à fila de mensagens mortas. Uma camada inteira de tratamento de erros estava desligada, por trás de um comentário que dizia a quem o lesse que ali não havia nada a verificar.
A correcção
Ambas as procuras passam agora pela chave da mensagem dentro do grupo de consumidores, que é o que o índice único impõe de qualquer forma. As chaves de mensagem derivam do identificador daquilo que as produziu, pelo que não podem colidir entre inquilinos por construção.
A documentação que afirmava o carimbo foi corrigida em vez de apagada. Aquela frase era a armadilha, e apagá-la não deixaria nada que diga que alguém finalmente verificou.
Nove testes fixam o comportamento. Seis deles falham contra o código antigo, o que confirmámos repondo o código antigo.
A limpeza a seguir
As linhas inflacionadas reparam-se sozinhas. Um worker reconstrói os resumos diários a partir do livro-razão a cada hora, numa janela de dois dias. Andava a correr o tempo todo e a perder: uma vez por hora contra um consumidor que volta a aplicar a cada quarenta e cinco segundos não é uma corrida justa.
Com o consumidor já sem se repetir, ganha. Os dois dias ainda dentro dessa janela foram reconstruídos a partir do livro-razão assim que a correcção entrou em produção. Dois dias anteriores já tinham caído fora dela, e o que resta neles é uma conta nossa mais uma diferença insignificante noutro sítio — por isso deixámo-los, em vez de alargar uma janela em produção para ir atrás disso. Pareceu-nos melhor do que insinuar que a varredura foi total.
O que levámos disto
Uma chave de desduplicação tem de espelhar o que o índice impõe de verdade, não o que o código acredita ter sido escrito. As duas metades desta eram alimentadas por campos diferentes, e nenhuma tinha alguma vez sido medida contra a outra. Agora ambas estão, e o teste que o prova fica vermelho contra a versão que estava errada.