1 pontos por GN⁺ 2024-06-10 | 2 comentários | Compartilhar no WhatsApp
  • Uma startup que tinha acabado de ativar a monetização enfrentou uma falha no pagamento de assinaturas, mas como o problema não era reproduzido internamente, a identificação da causa foi atrasada em 5 dias
  • A falha começou ao copiar o formato de conversão Prisma/TypeScript→Python/SQLAlchemy gerado pelo ChatGPT, em que, no lugar de uma função de geração de UUID, entrou uma string de ID hardcoded como se fosse o valor padrão
  • Por causa da estrutura com 8 tarefas no AWS ECS e 5 instâncias em cada uma, os usuários podiam acabar encontrando um entre 40 pools de IDs únicos diferentes, e durante o dia o problema ficava mascarado por implantações frequentes
  • À noite, quando as implantações paravam, o único ID de cada servidor se esgotava, e depois disso novas tentativas de assinatura falhavam por conflito de ID único
  • Com base em 50 reclamações por dia, ao longo de 5 dias, e em uma assinatura mensal de $40, a perda foi estimada em $10.000 por mês, e a ausência de testes, logs e alertas, além da cópia de código, ampliou o impacto do incidente

Falha na assinatura exposta logo após a monetização

  • A startup ativou a monetização pela primeira vez em maio e conseguiu o primeiro cliente em menos de 1 hora após o lançamento
  • Na manhã do dia seguinte, havia mais de 40 reclamações de usuários acumuladas no Gmail
    • Os usuários não conseguiam concluir a assinatura
    • Eles relatavam que, ao clicar no botão de assinatura, aparecia um spinner de carregamento infinito
  • Foi criado um novo usuário para testar diretamente, mas internamente a assinatura funcionava normalmente, então não foi possível reproduzir a causa
  • Durante o horário de trabalho quase não havia reclamações, e a falha se acumulava principalmente durante a noite

Implementação da monetização sob pressão de tempo

  • Maio foi o momento em que a turma YC S23 começou, e a equipe ainda não tinha certeza de qual direção seria a melhor após o lançamento
  • O group partner da YC, Dalton, aconselhou usar assinantes pagantes como métrica para definir a direção e dobrar o preço mensal inicialmente imaginado
  • O preço final foi definido em $40 por mês
  • O projeto era originalmente full-stack em NextJS, mas durante o trabalho de monetização estava sendo migrado para Python/FastAPI
    • O ChatGPT foi usado no processo de migração
    • A integração com Stripe também foi concluída
  • Nos 5 dias seguintes, o sono foi drasticamente reduzido, e foi preciso lidar com 30 a 50 e-mails de reclamação por dia

O formato de conversão de modelos criado pelo ChatGPT

  • Durante a migração do backend, os modelos de banco de dados foram movidos de Prisma/TypeScript para Python/SQLAlchemy
  • A conversão dos modelos era tediosa, e como pareceu que o ChatGPT fazia isso bem, ele foi usado em quase toda a migração
  • O código gerado foi copiado e colado, depois testado para ver se funcionava, e como parecia normal em produção, seguiram assim
  • Na época, as inserções no banco de dados ainda eram feitas pela API em Next, e o backend em Python apenas lia o banco
  • Ao implementar a funcionalidade de assinatura, começou-se pela primeira vez a inserir registros no banco a partir do Python
    • Os novos modelos SQLAlchemy foram criados manualmente, mas o formato gerado pelo ChatGPT nos modelos existentes foi copiado diretamente
    • O mesmo problema foi introduzido na forma de geração de ID de todos os modelos

A causa real e por que não aparecia durante o dia

  • O erro central foi passar uma única string de ID hardcoded, em vez de passar uma função ou lambda que gerasse UUIDs
  • Quando um usuário concluía a assinatura com aquele ID em uma instância específica do backend, as tentativas seguintes de assinatura na mesma instância geravam conflito de ID único
  • A configuração do backend escondeu esse problema por mais tempo
    • Eram operadas 8 tarefas no ECS da AWS
    • Cada tarefa executava 5 instâncias do backend
    • Os usuários podiam potencialmente chegar a um entre 40 IDs diferentes
  • Durante o dia, eram feitos de 10 a 20 commits diretos na branch main por dia, e cada um deles disparava uma nova implantação do backend
    • A cada implantação, surgiam 40 novos IDs que os clientes podiam usar
  • À noite, os commits e as implantações paravam, e o único ID de cada servidor era rapidamente consumido
    • No início, havia quase 40 servidores capazes de aceitar assinaturas, mas com o tempo isso se aproximava de 0

Escala da perda e medidas posteriores

  • A perda foi estimada em $10.000 de receita mensal com o cálculo 50 emails/day x 5 days x $40/month
    • Esse cálculo considera apenas os usuários que enviaram reclamações
  • Foram necessários 5 dias para encontrar a causa, inúmeros e-mails, centenas de logs no Sentry, uma longa conversa no Discord com engenheiros da Stripe e a revisão de cinco arquivos principais
  • Depois de encontrar a causa, Adam publicou rapidamente a correção
  • Depois disso, foram adicionados testes unitários/de integração robustos, alertas e logs
  • O caso mostra que, quando erro humano, falta de testes, cópia de código e push direto na main se combinam, até uma pequena linha pode levar a uma grande perda de receita

2 comentários

 
znjadong 2024-06-11

Ué, código gerado automaticamente por IA tem que ser revisado, né? Por que usar isso do jeito que saiu?

 
GN⁺ 2024-06-10
Opiniões no Hacker News
  • A falta de monitoramento foi o que fez US$ 10 mil irem pelo ralo. O app estava gerando exceções de banco de dados continuamente, em grande volume, e ninguém recebeu alerta
    Se houvesse um alerta desses, teria sido uma investigação de 5 minutos, não de 5 dias. Se eles não corrigiram o sistema de alertas, na prática não corrigiram nada

    • Exato. A mensagem de log teria dito que o ID não era único, e isso teria reduzido muito o tempo de depuração
      Programar é fácil quando tudo está funcionando bem; a parte difícil é lidar com os problemas
    • Esse tipo de coisa está ficando cada vez mais comum. Empresas e fundadores não pensam em infraestrutura, acreditando que o provedor de nuvem escolhido vai resolver tudo magicamente
      A partir do momento em que entram clientes pagantes, é preciso ter alguém com conhecimento e experiência para lidar com logging, monitoramento, alertas, segurança etc. Não dá para tratar DevOps de forma amadora
    • Também é ruim não ter testes, e é perigoso usar algo gerado por IA sem conferir três vezes
      Mas a parte realmente absurda é não haver registro de erros e alertas no banco de dados. Isso não é um código legado de 20 anos, é um produto novo, e também não é código da época em que erros de DB eram usados como validação de dados
    • Fazer o deploy e ir dormir logo em seguida parece um sinal de alerta aqui. Deveriam ter feito o deploy por volta das 9h da manhã e monitorado os problemas durante o horário comercial
    • Parece que o ChatGPT não avisou que monitoramento era necessário
  • O post do blog está dando 404, então deixo o link do Web Archive
    https://web.archive.org/web/20240610032818/https://asim.bear...
    O autor acrescentou uma correção importante: as práticas ali eram muito ruins e constrangedoras, e depois disso eles adicionaram testes unitários/de integração robustos e alertas/logging. No fim, foi erro humano e, em retrospecto, algo claramente evitável
    Ele também acrescentou que isso aconteceu nas primeiras semanas da empresa, sob grande pressão de tempo, e pediu que fosse visto como uma história engraçada sobre a reprodutibilidade peculiar de um bug em produção

  • O erro ficou visível de imediato. Com todo respeito pela equipe, isso não tem muito a ver com o ChatGPT, e sim com o uso de um modelo de programação com o qual a equipe não estava suficientemente familiarizada
    Mesmo que tivesse passado por code review, é bem provável que tivesse sido pego com ferramentas de monitoramento que levam 5 minutos para configurar

    • Para ser justo, acho que eu não teria visto se não estivesse procurando por esse bug. Ainda assim, é verdade dizer que qualquer monitoramento ou até o teste manual mais básico teria pego isso imediatamente
    • Parece que a equipe nem conseguiu fazer uma solução de problemas básica nos logs do banco de dados ou da aplicação. Esse era um erro simples; fico preocupado com o que fariam diante de um erro transitório, como um bloqueio implícito de tabela
    • Não foi um erro inocente; o título foi intencionalmente feito como clickbait e para otimização de busca. Ele sugere que o ChatGPT cometeu o erro para induzir cliques movidos por ansiedade
      Um título como “Cometemos um erro de programação ao usar um LLM e, por falta de garantia de qualidade, isso nos custou 10 mil dólares” não geraria a reação de executivos do tipo “quanto é nossa exposição se o ChatGPT estragar tudo?”. Vai ter um monte de gerentes de médio e alto escalão postando esse texto no LinkedIn
      Um LLM não pode “cometer erros”. Ele não é determinístico, não infere, não pensa nem executa lógica. É um gerador extremamente sofisticado de salada de palavras que usa probabilidades estatísticas, e não há garantia de que o que ele gera esteja certo ou seja preciso; portanto, por definição, chamar isso de erro também não é adequado
      Edit: depois de o post ser muito downvotado por motivos óbvios, a posição dele subiu de repente, o que parece significar que um moderador o impulsionou: https://hnrankings.info/40627558/
      É engraçado que um post clickbait a ponto de, pelas regras, o título ter que ser alterado tenha sido impulsionado por um moderador. E, além disso, o fato de o autor aparentemente trabalhar em uma empresa da Y Combinator deve ser uma coincidência total: https://news.ycombinator.com/item?id=40629998
    • Curiosamente, quem encontrou esse erro também foi o ChatGPT-4o. Não dá para compartilhar o chat com imagens, mas colei a imagem do código errado e perguntei “qual é o problema neste código?”, e ele explicou o seguinte
      Disse que, para gerar o UUID da chave primária, não se deve usar str(uuid.uuid4()), mas sim passar diretamente o chamável uuid.uuid4, e que o SQLAlchemy chama a função ao criar o valor. Também disse que o valor padrão de data server_default=text("(now())") talvez não funcione como esperado e recomendou usar func.now(); pediu para verificar também os imports de uuid e de text do SQLAlchemy, e sugeriu considerar DateTime(timezone=True) para lidar com fuso horário
      Depois, como código corrigido, sugeriu id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()), unique=True, nullable=False), e a adição de lambda: ali corrigiu o problema
    • Se você quase não tem experiência com Python, imagino que pensaria em uuid.uuid4() como uma definição de schema em algo como Prisma. Então o bug em si não surpreende, e eu poderia ter cometido o mesmo erro
      Ainda assim, um único kubectl logs teria pego isso na hora. E, além disso: sair de Next.js e Prisma para Python? Por quê?
  • O erro em si é compreensível. Parece algo relativamente fácil de deixar passar mesmo escrevendo código sem o ChatGPT
    Mas não entendo por que não foi identificado depois da primeira falha. Essa empresa não tinha logging? O fato de o backend estar tentando reutilizar um UUID deveria ter ficado evidente assim que se olhasse o erro

    • Eles nem sabiam que havia ocorrido um erro até o cliente reclamar. Você precisa saber quais erros aconteceram antes do cliente; logging, alertas ou qualquer forma de monitoramento teria ajudado
    • Esse é o problema real. Em um projeto que se move rápido, é bem provável que algum outro bug em produção desse tipo aconteça em algum momento. Só espero que, da próxima vez, não levem 5 dias para identificá-lo
    • Levar 5 dias para entender esse bug é bem absurdo
    • É perfeitamente plausível que um caso especial como múltiplas assinaturas do Stripe fique de fora de testes unitários comuns. Como o texto diz, provavelmente também não seria fácil reproduzir com testes de aceitação, e tanto o autor quanto outras pessoas exageram um pouco ao focar no ChatGPT
      Passar por engano uma string para uma função que espera um objeto chamável em vez de String é algo comum. Se não estivessem usando ORM, acho que esse problema específico teria sido evitado, mas talvez isso seja só meu preconceito pessoal contra ORMs. Bugs parecidos podem muito bem acontecer também fora do contexto de bancos de dados
      As pessoas que afirmam com tanta certeza que teriam pego esse bug ou são engenheiros muito melhores do que eu, ou, mais provavelmente, têm uma percepção um pouco equivocada das próprias habilidades
      Dito isso, não ter logs ou não olhar os logs é realmente difícil de entender. Se era ECS, eu esperaria que a exceção de Duplicate Key fosse propagada para o CloudWatch sem configuração extra; fico curioso se isso não aconteceu, ou se aconteceu e mesmo assim ninguém verificou durante a noite quais exceções tinham ocorrido
    • Por causa desse tipo de falha de processo, a Amazon tem correção de erros, ou seja, um processo de análise pós-incidente: https://aws.amazon.com/blogs/mt/why-you-should-develop-a-cor...
      Em situações assim, é útil perguntar por que a detecção demorou e por que o diagnóstico levou tanto tempo
  • Já vi o mesmo erro várias vezes em código feito por pessoas. Especialmente em React / TypeScript / JavaScript, é comum alguém esquecer uma lambda
    O post do blog parece não explicar direito a causa raiz do problema e pula direto para culpar o ChatGPT. Quando se trabalha com pressa e se coloca na main um commit com uma mudança grande ou sem revisão de colegas, esse tipo de coisa acontece
    O problema real é que, quando se tem pressa, se pega atalhos e não há testes suficientes nem code review por pares, erros aparecem. Acho que bastaria ter um teste tentando várias opções de assinatura para encontrar isso imediatamente

    • Meu modelo mental para o ChatGPT é o de um engenheiro júnior que nunca será promovido. Alguém que um dia seria demitido, mas que, em compensação, consegue digitar infinitamente rápido, então pode ser útil se usado com muito cuidado
      Se você coloca alguém assim perto de código financeiramente importante, problemas parecidos vão surgir, e eu passaria a questionar o julgamento de quem decidiu colocar esse código em produção com quase nenhum teste
    • Esse tipo de problema é comum em vários lugares. Por exemplo, propriedades de componentes no Vue podem ter valores padrão, mas se você usar um objeto ou array literal como valor padrão em vez de uma função que retorna um objeto ou array, dá muito ruim
      É surpreendente que não houvesse uma regra de lint para esse caso
    • O ChatGPT é apenas um elemento de distração. O que importa não é o que gerou o código, mas o que você faz com esse código
    • A estrutura era: o ChatGPT escrevia o código, fazia push e depois o próprio ChatGPT revisava. /s
      Espero que não tenha sido assim
  • A parte “o projeto original era full-stack NextJS, mas eu queria primeiro migrar tudo para Python/FastAPI” me fez arregalar os olhos
    Não sei como uma startup sem clientes justifica uma reescrita

    • Pensei a mesma coisa e achei que eu estivesse ficando louco. Eu achava que uma reescrita tão cedo assim não fazia sentido, mas parecia que ninguém nos comentários estava apontando isso
      Com ou sem clientes, não entendo por que fazer, num estágio tão inicial, o que é essencialmente um deslocamento horizontal de Node para Python. Se tivessem centenas de clientes e estivessem mudando para algo como Go, eu até poderia entender um pouco, mas ainda assim seria questionável
    • Além disso, estavam reescrevendo em uma linguagem na qual tinham pouca experiência a ponto de não perceber um bug que parece óbvio
    • No meu caso, se fosse um projeto real em que estou trabalhando agora, poderia ser porque C# é uma boa linguagem e o runtime e o framework web também são ok, mas eles reduzem a velocidade de desenvolvimento e os pontos de dor continuam se acumulando
      Por exemplo, é preciso criar um monte de objetos DTO, mas o AutoMapper não funciona com a combinação de versões e a configuração do projeto que eu uso, e o Entity Framework mais a serialização/desserialização JSON trazem mais dor do que benefício
      Claro que dá para resolver isso gradualmente. Dá para mergulhar na documentação, misturar alguns hacks, atualizar pacotes e reescrever configurações. Mas, sendo humano, dá vontade de pegar um galão de gasolina metafórico, queimar tudo e fazer o segundo sistema melhor. Claro que, na prática, ele não fica melhor; só surgem outros pontos de dor, e talvez ele nem faça tudo que o primeiro sistema fazia, ou não faça direito
      No trabalho também sinto o mesmo impulso sempre que vejo um sistema legado ou trabalhoso. É preciso um esforço ativo e contínuo para vencer a parte do cérebro que grita para reescrever tudo. Às vezes uma mudança arquitetural como uma reescrita ou adoção de contêineres dá certo, mas em geral ela leva você para dentro das chamas ou para um trabalho interminável
      Exceto quando há alta confiança de que isso vai melhorar a operação do sistema ou a experiência de desenvolvimento de outros desenvolvedores, é melhor não ceder a esse impulso
    • A única falha do ChatGPT nesse caso foi que sua capacidade fez parecer razoável e fácil gastar runway em uma reescrita antes do lançamento
  • O ChatGPT, na verdade, foi o lado que gerou o dinheiro que o app ganhou. Sem o ChatGPT, eles não teriam capacidade de implementá-lo
    A falta de capacidade em codar, depurar, fazer logging e monitoramento foi o que queimou US$ 10 mil; nessa história, o efeito líquido do ChatGPT é positivo

    • Olhando os projetos dessa equipe em github.com/reworkd, a maturidade do produto e do time fica evidente na hora. É desenvolvimento guiado por emojis
      Todas as mensagens de commit têm emojis. Tem macaco, banana, foguete, fogos de artifício, de tudo
    • Ainda mais considerando o custo de US$ 20 por mês
    • US$ 10 mil é troco. O Elon talvez perca US$ 500 bilhões com o erro da xAI que saiu hoje
      https://grook.ai/share?id=e269e88a7b1a71eff4f176c864b30161&x...
    • Parece que eles tinham capacidade, já que a implementação já existia
      Dizem que originalmente era full-stack NextJS e que, ao migrar o backend para Python/FastAPI, estavam traduzindo o modelo de banco de dados de Prisma/Typescript para Python/SQLAlchemy. O texto diz que esse trabalho era tedioso e, vendo que o ChatGPT fazia isso razoavelmente bem, usaram-no em quase toda a migração
      Se o ChatGPT não existisse, talvez nem tivessem tentado essa migração preliminar, então é difícil dizer que o efeito líquido foi positivo. A stack existente talvez tivesse logging de erros melhor, talvez não; e, por ser código escrito por eles mesmos, talvez entendessem melhor a estrutura e precisassem menos disso
      A decisão de “reescrever todo o código uma segunda vez” antes de ativar a monetização já é interessante por si só
  • Há uma frase: “Quero começar dizendo que as práticas aqui foram ruins e poderiam ter sido evitadas. Isso aconteceu em outro período, sob grande pressão de tempo. Leiam levando isso em conta”
    É por causa desse tipo de restrição que assinaturas de software me dão medo

    • Respeito muito o fato de o autor ter tornado essa história pública, especialmente com um prefácio assim. Saber que erros outras pessoas cometem é realmente útil, mas expor um erro próprio pode ser bem constrangedor
    • Mesmo que você não goste de assinaturas, o mundo antigo, em que se compravam licenças de centenas de dólares por assento de usuário, também não era maravilhoso
    • Já lidei com código legado de assinaturas, e ele pode ser bem bagunçado
      Já cobramos um usuário duas vezes por causa de uma condição de corrida. Por isso, quando vejo timeouts ou erros relacionados a dinheiro, fico paranoico a ponto de presumir primeiro que o pagamento foi feito e conferir de novo depois
    • A alternativa é escrever você mesmo, mas aí isso impõe restrições a todo o resto
    • Fazer uma reescrita sob “grande” pressão de tempo também é incomum
  • Código em TypeScript e Python, frameworks como Next.js, 8 tarefas na AWS, cada uma rodando 5 instâncias, e mesmo assim a receita foi de US$ 40, com apenas algumas semanas de desenvolvimento?
    Fico pensando que diabos aconteceu. Dizem que corrigiram porque o código estava uma bagunça por causa da restrição de tempo, mas é ainda pior terem gastado tempo com refatoração entre linguagens e com a criação de um sistema distribuído sem motivo nenhum.
    É uma complexidade autoinfligida, tentando fazer malabarismo ao mesmo tempo com funcionalidades e uma complexidade técnica absurda. Não faço ideia do que estavam pensando.
    Correção: é uma empresa da turma de verão de 2023 da YC, mas no verão de 2024 o produto ainda parece estar atrás de uma lista de espera. Talvez porque estejam reescrevendo em Rust.

    • Porque têm US$ 500 mil de capital inicial, mais US$ 1,2 milhão em cima disso, além de créditos gratuitos da AWS para queimar.
  • Parece que ele nem deve ter escrito 1.000 linhas de Python na vida, mas identificou o problema corretamente.
    O Python tem uma falha por não ter copiado direito a estratégia de avaliação do Common Lisp. Se, nas expressões de valor padrão de argumentos opcionais de uma função, houver um argumento como foo=obj.whatever(), obj.whatever() é avaliado no momento em que a definição da função é processada, não quando a função é chamada.
    Suspeito que tenham feito isso de propósito por eficiência. O Python tem outra falha: não há uma sintaxe literal de verdade para objetos comuns como listas. [1, 2, 3] não é um literal, é mais parecido com um construtor; cada vez que é avaliado, precisa criar uma nova lista e preenchê-la com valores.
    O projetista provavelmente não queria que um parâmetro como list=[] criasse uma nova lista vazia toda vez que o argumento fosse omitido. Em Lisp, '(1 2 3) e '() são literais de verdade e apontam para o mesmo objeto sempre que são referenciados. O programador pode escolher se quer usar (list 1 2 3) ou '(1 2 3) como expressão de valor padrão.
    O primeiro cria um novo objeto mutável a cada vez, como [1, 2, 3]; o segundo quase certamente retorna o mesmo objeto e, de forma confiável e portável, não pode ser modificado. Como as linguagens populares modernas já têm a maior parte dos recursos do Lisp, isso parece uma piada do tipo “não há nada a perder”.

    • O que foi dito está correto, mas não foi isso que aconteceu no post do blog. O problema não estava em uma definição de função, e sim dentro da definição de uma classe.
    • A afirmação de que, em foo=obj.whatever(), obj.whatever() é avaliado no momento em que a definição da função é processada, e não quando a função é chamada, não parece poder estar correta.
      Não sei o que aconteceria se .whatever() dependesse de um estado interno que muda depois da inicialização do objeto.