Anual Pague anualmente e ganhe 2 meses grátis em qualquer plano pago. Ver planos
Entrar Comece grátis

A página de consumo mostrava a mais, e nenhum centavo se moveu

Vamos começar pela parte mais fácil de se perder ao ler o resto: nenhum dinheiro foi retirado. Nenhuma carteira foi debitada duas vezes, nenhum cartão foi cobrado, nenhuma cota de assinatura foi gasta em trabalho que nun…

Vamos começar pela parte mais fácil de se perder ao ler o resto: nenhum dinheiro foi retirado. Nenhuma carteira foi debitada duas vezes, nenhum cartão foi cobrado, nenhuma cota de assinatura foi gasta em trabalho que nunca aconteceu.

O que quebrou foi um número numa tela. Em algumas contas, o detalhamento de consumo na sua própria página de conta mostrava a mais — na pior que medimos, 2,4 vezes o livro-razão que ela deveria resumir. O livro-razão esteve certo o tempo todo. Os tetos mensais também, e eles são lidos de um lugar completamente diferente. Foi um defeito de exibição, já está corrigido, e esta foi a causa.

Uma chave que procurava algo que ninguém escreveu

O trabalho que nasce de algo que você fez — atualizar um resumo diário de consumo, creditar uma indicação, distribuir uma notificação — não acontece dentro da sua requisição. A requisição escreve uma mensagem e um consumidor em segundo plano a pega um instante depois; é isso que impede um efeito colateral lento de deixar lenta uma página rápida.

O perigo desse arranjo é fazer o mesmo trabalho duas vezes, porque um broker reentrega alegremente a mesma mensagem depois de um soluço. A defesa é uma caixa de entrada: antes de processar, um consumidor reivindica a mensagem escrevendo uma linha que carrega a chave dessa mensagem, e um índice único torna impossível uma segunda reivindicação.

A nossa reivindicava sobre um par — o inquilino a que a mensagem pertencia, mais a chave da mensagem — e procurava esse par contra o identificador de inquilino carregado pelo envelope da mensagem, que vem preenchido.

As linhas de reivindicação não carregavam identificador de inquilino algum. Todas estavam vazias, e estavam assim desde o começo. A própria documentação da entidade afirmava, com essas mesmas palavras, que o framework o carimbava na inserção. Nunca carimbou. Medido em produção no dia em que encontramos: as 3.344 linhas da caixa de entrada vazias, enquanto 1.174 de 1.451 linhas do lado produtor carregavam um inquilino real — exatamente por isso ninguém desconfiou do lado que lê.

Então a busca pedia (inquilino, chave), a linha salva era (nada, chave), e elas nunca poderiam bater.

Nada falhou, e é por isso que rodou por semanas

Uma busca que nunca pode bater soa como algo que deveria dar erro na primeira tentativa. Fez o oposto.

A reivindicação decidiu que a mensagem era nova. A inserção então bateu no índice único e foi rejeitada como duplicata — corretamente, porque a linha já estava lá. Um ramo alternativo capturou essa rejeição e a leu com benevolência: outra entrega deve ter reivindicado esta, aqui não há nada a fazer. O envelope ficou no estado "ainda a processar", e como ainda estava a processar, a entrega seguinte o pegou e refez o trabalho.

Cada consumidor reprocessava a própria fila acumulada mais ou menos a cada quarenta e cinco segundos, indefinidamente. Vinte e seis faziam isso. Uma linha de produção terminou 333 horas depois de chegar — e vinha terminando repetidamente durante a maior parte desse tempo.

O que de fato quebrou

Desses vinte e seis consumidores, quatro acumulam em vez de sobrescrever. Esses quatro são as projeções de cobrança que a página de consumo lê, e foi ali que a coisa apareceu.

Eles tinham uma proteção explícita exatamente contra isso, escrita depois de um incidente em agosto em que um único evento foi aplicado nove vezes a um indicador. Essa proteção marca uma linha como bem-sucedida antes de processá-la, para que uma reentrega encontre a marca e pare. A marca era escrita corretamente todas as vezes. Aí a reivindicação seguinte a procurava com a mesma chave quebrada e não a encontrava. A garantia esteve vazia desde o dia em que foi lançada.

O que isso não tocou vale ser listado com precisão, porque "projeção de cobrança" soa alarmante e os fatos não são:

  • O livro-razão estava correto. Cada chamada de IA escreve exatamente um registro de cobrança com seu próprio instantâneo de preço, e nada neste caminho os escreve.
  • Nenhuma carteira se moveu. Os saldos vêm do livro-razão, nunca de um resumo.
  • Nenhum teto se moveu. O guardião do orçamento lê um contador separado, e é ele a única fonte de verdade sobre o quanto você gastou neste mês. Ninguém foi cortado cedo demais por causa de um número inflado, e ninguém passou sem dever passar.

A única coisa que lê o resumo afetado é o histórico de consumo na sua própria página de conta — o seu total e o detalhamento de onde ele foi parar. Esse é todo o raio de impacto, e o que ele fez foi mostrar a mais.

A metade para a qual ninguém olhava

A mesma busca quebrada estava num segundo lugar: o caminho que roda quando uma mensagem falha. Ali ela nunca encontrava a linha que deveria atualizar, e logo abaixo havia um ramo com o comentário "inalcançável na prática" — que devolvia "esta falha é terminal" em toda falha.

Assim o contador de novas tentativas nunca avançou e nenhuma mensagem envenenada chegou à fila de mensagens mortas. Uma camada inteira de tratamento de erros estava desligada, atrás de um comentário que dizia a todo mundo que o lia que ali não havia nada a verificar.

A correção

As duas buscas agora se chaveiam pela chave da mensagem dentro do grupo de consumidores, que é o que o índice único impõe de qualquer jeito. As chaves de mensagem derivam do identificador daquilo que as produziu, então 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 própria armadilha, e apagá-la não deixaria nada dizendo que alguém finalmente conferiu.

Nove testes fixam o comportamento. Seis deles falham contra o código antigo, o que confirmamos colocando o código antigo de volta.

A limpeza depois

As linhas infladas se consertam sozinhas. Um worker reconstrói os resumos diários a partir do livro-razão a cada hora, numa janela de dois dias. Ele vinha rodando o tempo todo e perdendo: uma vez por hora contra um consumidor que reaplica a cada quarenta e cinco segundos não é uma corrida justa.

Com o consumidor não rodando mais de novo, ele ganha. Os dois dias ainda dentro dessa janela foram reconstruídos a partir do livro-razão assim que a correção entrou em produção. Dois dias anteriores já tinham saído dela, e o que resta neles é uma conta nossa mais uma diferença desprezível em outro lugar — então deixamos como está, em vez de alargar uma janela em produção para correr atrás disso. Pareceu melhor do que insinuar que a varredura foi total.

O que levamos disso

Uma chave de deduplicação precisa espelhar o que o índice realmente impõe, não o que o código acredita ter sido escrito. As duas metades desta eram alimentadas por campos diferentes, e nenhuma jamais foi medida contra a outra. Agora as duas são, e o teste que prova isso fica vermelho contra a versão que estava errada.

Compartilhar LinkedIn X E-mail
Experimente no seu próprio trabalhoCada resposta mostra quanto custou. Comece no plano gratuito. Comece grátis
Receba novidades do produto, não ruído Novos modelos, funcionalidades e destaques do changelog — um email curto quando algo é mesmo lançado.
Ao se inscrever, você concorda em receber e-mails da ToRun com novidades do produto. Você pode cancelar a inscrição a qualquer momento. Política de Privacidade