← Escrevo

Diagnóstico em Edge Workers: Por que o Cron Rodava sem Limpar o Catálogo

Investigação de gargalo de subrequests em Cloudflare Workers: como o teto de chamadas em loop interrompia silenciosamente a drenagem de 100 variantes em um e-commerce real.

Cloudflare WorkersDebuggingSubrequestsE-commerce
Diagnóstico em Edge Workers: Por que o Cron Rodava sem Limpar o Catálogo

Uma loja de persianas sob medida cria uma variante de produto a cada configuração que o cliente monta. Largura, altura, tecido, cor, acionamento: cada combinação vira um item de verdade no catálogo, senão não entra no carrinho. Isso significa que o catálogo enche de lixo por construção, e que existe um Worker agendado cuja única função é apagar as variantes que ninguém comprou.

A suspeita registrada há duas semanas era direta: os IDs das variantes estavam espalhados numa faixa larga demais para uma janela de 24 horas, então o cron provavelmente não estava rodando.

A suspeita acertou o sintoma e errou a causa inteira. O cron rodava. Rodava havia meses, sem uma falha de execução. E mesmo assim havia 100 variantes vencidas, a mais antiga criada 101 dias antes.

O padrão que entregou o caso

Antes de mexer em qualquer linha, rodei um script somente leitura contra a loja para contar o que existia de fato. O resultado não parecia aleatório, e é isso que interessa:

  • 109 variantes do configurador vivas, das quais 100 já vencidas.
  • Todas as 100 vencidas estavam em produtos da posição 79 em diante da lista.
  • O único produto do configurador dentro das primeiras 47 posições tinha zero vencidas.

Onde o cron alcançava, ele funcionava perfeitamente. O problema não era execução, era alcance. E “alcance” não é uma categoria de bug que ocorre a você quando a hipótese de partida é “não está rodando”.

Três causas somadas

Nenhuma das três sozinha explicava o acúmulo.

A janela nunca foi 24 horas. A constante no código dizia 72, com um comentário explicando o motivo: dar três dias para o cliente voltar ao carrinho abandonado. O README, a documentação e minhas próprias notas diziam 24. A suspeita original tinha sido medida contra a janela errada, e com 72 horas o espalhamento esperado de IDs é três vezes maior. Ou seja, o dado que levantou a suspeita não era nem evidência de problema.

O filtro por nome pegava a loja inteira. A lista de produtos do configurador era identificada por nome, procurando por termos como “blackout” e “tela solar”. Só que a loja vende persianas: esses termos casavam com 111 dos 113 produtos. Listar as variantes de 111 produtos custa 111 subrequests, e o plano grátis do Cloudflare Workers dá um teto de 50. O loop estourava o orçamento por volta do produto 47 e dava break. Sempre nos mesmos 47 primeiros. Os produtos que realmente acumulavam lixo viviam nas posições 79 a 111.

O agendamento era semanal. O painel mostrava 0 3 * * 0, domingo às 3h. O comentário no código, o README e a documentação diziam 0 3 * * *, diário. Nunca foi diário.

O corolário que explica por que ninguém tinha resolvido na mão

Existia uma rota manual de limpeza, para forçar a faxina sem esperar o agendamento. Ela chamava exatamente a mesma função, que começa sempre do início da lista.

Rodar a limpeza manual, quantas vezes fosse, gastava o orçamento inteiro percorrendo produtos que já estavam limpos e parava antes de chegar nos sujos. A válvula de escape tinha o mesmo defeito do sistema que ela deveria socorrer, o que a tornava perfeitamente inútil de um jeito silencioso.

A correção não foi aumentar o orçamento

A saída óbvia seria paginar, guardar um cursor entre execuções e ir rodando por partes. Cheguei a considerar. Foi a leitura da documentação da API que tornou isso desnecessário: o endpoint de produtos já devolve as variantes embutidas em cada produto, com data de criação e tudo.

Os 113 produtos vêm em uma subrequest. Não 111.

Com isso a varredura passou a montar uma fila global de vencidas, ordenada da mais antiga para a mais nova, e a gastar as ~47 chamadas restantes só em exclusões. Como cada execução enxerga a loja inteira, qualquer acúmulo se drena sozinho execução após execução, sem precisar guardar estado em lugar nenhum.

O rodízio entre execuções que eu tinha cogitado só existia como ideia porque listar variantes custava caro. Quando o custo caiu de 111 para 1, o problema que o rodízio resolvia deixou de existir. Vale como regra geral: boa parte da complexidade que a gente projeta existe para contornar uma restrição que nunca foi verificada.

O achado colateral que quase virou incidente

Duas coisas apareceram por acidente no meio do diagnóstico, e as duas eram mais perigosas que o bug original.

A primeira: a rota manual de limpeza chamava a função com o parâmetro que ignora a janela de tempo. O nome e o uso pretendido eram “adiantar o cron”, mas o comportamento real era apagar todas as variantes, inclusive as de carrinhos abertos naquele instante. Quem clicasse achando que estava só antecipando a faxina destruiria compras em andamento. O padrão passou a respeitar a janela, igual ao agendamento, e o modo destrutivo passou a exigir um parâmetro explícito na URL.

A segunda: depois de drenar as 100 variantes, um dos produtos ficou com uma única variante, aquela que eu vinha tratando havia semanas como lixo cosmético. Ela é o que mantém viva a propriedade de variação do produto. Apagá-la deixaria o produto sem variante nenhuma, e a plataforma remove o atributo nesse caso, o que quebraria a criação de variantes futuras. O configurador daquela página simplesmente pararia de funcionar.

Uma decisão anterior de não mexer nela tinha sido tomada por outro motivo, e por sorte. Ela não era cosmética, era estrutural.

O que ficou

A drenagem apagou 100 variantes, com 0 erros e nenhum produto zerado. Sobraram 9, todas dentro da janela de 72 horas, a mais antiga com 47 horas: carrinhos legítimos, que é exatamente o que deveria sobrar.

Antes de subir, validei a lógica nova simulada contra a loja real, sem apagar nada: 1 chamada de varredura contra as 112 de antes, 47 sobrando para exclusões, e no pior caso simulado a fila saiu ordenada corretamente, com a variante estrutural fora dela.

O que eu tiro daqui, e que não é sobre cron:

Um agendamento que executa sem erro não é um agendamento que funciona. O log dizia “sucesso” todas as vezes, porque terminar o orçamento e sair é um caminho de sucesso. Só medir o efeito no mundo, e não o status da execução, revelou o problema.

Três documentos diziam a mesma coisa errada. README, documentação e minhas notas concordavam entre si sobre a janela e sobre a frequência. Concordância entre documentos não é verificação: eles tinham sido copiados uns dos outros. O código e o painel discordavam dos três, e estavam certos.

A hipótese inicial custou duas semanas. “O cron não está rodando” era plausível, era compatível com o sintoma, e mandou olhar para o lugar errado. O que destravou foi parar de testar a hipótese e ir contar o que existia na loja.