- Une startup qui venait d’activer la monétisation a subi une panne de paiement des abonnements, mais comme elle n’était pas reproductible en interne, l’identification de la cause a pris 5 jours
- Le problème est parti de la copie d’un format de conversion Prisma/TypeScript→Python/SQLAlchemy généré par ChatGPT, où une chaîne d’ID codée en dur s’est retrouvée utilisée comme valeur par défaut au lieu d’une fonction de génération d’UUID
- En raison d’une architecture composée de 8 tâches AWS ECS avec 5 instances chacune, les utilisateurs tombaient sur l’un d’un maximum de 40 pools d’ID uniques, et le problème était masqué en journée par des déploiements fréquents
- La nuit, lorsque les déploiements s’arrêtaient, l’ID unique de chaque serveur s’épuisait, puis les nouvelles tentatives d’abonnement échouaient à cause d’une collision d’ID unique
- Sur la base de 50 plaintes par jour, pendant 5 jours, et d’un abonnement à 40 $/mois, la perte est estimée à 10 000 $ de revenus mensuels ; l’absence de tests, de logs et d’alertes, ainsi que la copie de code, ont amplifié la gestion de l’incident
Une panne d’abonnement révélée juste après la monétisation
- La startup a activé pour la première fois la monétisation en mai et a obtenu son premier client moins d’une heure après le lancement
- Le lendemain matin, plus de 40 plaintes d’utilisateurs s’étaient accumulées dans Gmail
- Les utilisateurs ne pouvaient pas finaliser leur abonnement
- Ils signalaient qu’un spinner de chargement infini apparaissait après avoir cliqué sur le bouton d’abonnement
- L’équipe a créé un nouveau compte pour vérifier elle-même, mais l’abonnement fonctionnait normalement en interne et la cause n’a pas pu être reproduite
- Pendant les heures de bureau, il y avait très peu de plaintes, et l’incident s’accumulait surtout pendant la nuit
Mise en place de la monétisation sous pression
- Le mois de mai correspondait au début du batch YC S23, et l’équipe n’était pas sûre de la meilleure direction à prendre après le lancement
- Dalton, group partner chez YC, a conseillé de prendre les abonnés payants comme indicateur pour décider de la direction, et de doubler le prix mensuel envisagé
- Le prix final a été fixé à 40 $ par mois
- Le projet était à l’origine un full-stack NextJS, mais une migration vers Python/FastAPI a été menée autour du travail de monétisation
- ChatGPT a été utilisé pendant la migration
- L’intégration de Stripe a également été finalisée
- Pendant les 5 jours qui ont suivi, le sommeil a fortement diminué et il a fallu traiter chaque jour 30 à 50 e-mails de plainte
Le format de conversion de modèles généré par ChatGPT
- Lors de la migration du backend, les modèles de base de données ont été déplacés de Prisma/TypeScript vers Python/SQLAlchemy
- La conversion des modèles était fastidieuse et, ChatGPT semblant bien s’en charger, il a été utilisé pour presque toute la migration
- Le code généré a été copié-collé puis vérifié en fonctionnement, et comme il semblait normal en production, l’équipe a continué ainsi
- À ce moment-là, les insertions en base de données étaient encore gérées par l’API Next, et le backend Python ne faisait que lire la base
- En implémentant la fonctionnalité d’abonnement, l’équipe a commencé pour la première fois à insérer des enregistrements en base depuis Python
- Le nouveau modèle SQLAlchemy a été créé manuellement, mais le format généré par ChatGPT dans les modèles existants a été copié tel quel
- Le même problème s’est retrouvé dans la méthode de génération d’ID de tous les modèles
La vraie cause et pourquoi elle ne se voyait pas en journée
- L’erreur centrale était de ne pas passer une fonction ou une lambda générant un UUID, mais une unique chaîne d’ID codée en dur
- Une fois qu’un utilisateur finalisait un abonnement avec cet ID sur une instance backend donnée, les tentatives d’abonnement suivantes sur la même instance provoquaient une collision d’ID unique
- La configuration du backend a masqué le problème plus longtemps
- 8 tâches ECS tournaient sur AWS
- Chaque tâche exécutait 5 instances backend
- Les utilisateurs pouvaient potentiellement arriver sur l’un de 40 ID différents
- En journée, 10 à 20 commits étaient faits directement chaque jour sur la branche main, déclenchant à chaque fois un nouveau déploiement backend
- À chaque déploiement, 40 nouveaux ID devenaient disponibles pour les clients
- La nuit, lorsque les commits et les déploiements s’arrêtaient, l’ID unique de chaque serveur s’épuisait rapidement
- Au début, près de 40 serveurs permettaient encore de s’abonner, mais au fil du temps ce nombre tombait presque à 0
Montant de la perte et mesures prises ensuite
- La perte a été calculée comme
50 emails/day x 5 days x $40/month, soit une estimation de 10 000 $ de chiffre d’affaires mensuel perdu- Ce calcul ne prend en compte que les utilisateurs ayant envoyé une plainte
- Il a fallu 5 jours, de très nombreux e-mails, des centaines de logs Sentry, une longue conversation Discord avec un ingénieur Stripe et la revue de cinq fichiers clés pour trouver la cause
- Une fois la cause découverte, Adam a rapidement publié un correctif
- Ensuite, de solides tests unitaires/d’intégration, des alertes et du logging ont été ajoutés
- Cet incident montre que, lorsque se combinent erreur humaine, manque de tests, copie de code et push direct sur main, même une petite ligne peut entraîner une importante perte de chiffre d’affaires
2 commentaires
Hein, le code généré automatiquement par une IA doit évidemment être relu, pourquoi l’utiliser tel quel ?
Avis de Hacker News
C’est l’absence de monitoring qui a fait perdre 10 000 dollars. L’application générait en continu, en grand nombre, des exceptions de base de données, mais personne n’a reçu d’alerte
S’il y avait eu ce type d’alerte, l’enquête aurait pris 5 minutes au lieu de 5 jours. Si le système d’alerte n’a pas été corrigé, alors en réalité rien n’a été corrigé
Programmer est facile quand tout fonctionne bien ; ce qui est difficile, c’est de gérer les problèmes
Dès qu’il y a des clients payants, il faut quelqu’un qui ait les connaissances et l’expérience pour gérer les logs, le monitoring, les alertes, la sécurité, etc. Il ne faut pas traiter le DevOps en amateur
Mais le vrai point délirant, c’est qu’il n’y avait ni journalisation des erreurs ni alertes sur la base de données. Ce n’est pas du vieux code legacy vieux de 20 ans, c’est un nouveau produit, et ce n’est pas non plus du code d’une époque où l’on utilisait les erreurs DB comme validation de données
Le billet de blog renvoie un 404, donc voici le lien Web Archive
https://web.archive.org/web/20240610032818/https://asim.bear...
L’auteur a ajouté une correction importante : les pratiques décrites ici étaient très mauvaises et franchement embarrassantes, et depuis, ils ont ajouté des tests unitaires/d’intégration solides ainsi que des alertes/logs. En fin de compte, c’était une erreur humaine, et avec le recul, quelque chose d’évidemment évitable
Il a aussi ajouté que cela s’était produit pendant les toutes premières semaines de l’entreprise, sous une forte pression temporelle, et qu’il fallait voir cela comme une histoire amusante sur une reproductibilité étrange d’un bug en production
C’est une erreur stupide, mais les humains, individuellement comme collectivement, font forcément des erreurs stupides
https://0912i390129ionkjan.bearblog.dev/how-a-single-chatgpt...
https://webcache.googleusercontent.com/search?q=cache%3Ahttp...
L’erreur sautait aux yeux. J’ai du respect pour l’équipe, mais cela n’a pas grand-chose à voir avec ChatGPT ; c’est surtout lié au fait qu’ils ont utilisé un modèle de programmation qu’ils ne maîtrisaient pas suffisamment
Même si le code avait passé la code review, il y a de fortes chances que cela aurait été détecté avec de simples outils de monitoring configurables en cinq minutes
Un titre comme « Une erreur de programmation commise en utilisant un LLM, faute d’assurance qualité, a coûté 10 000 dollars » ne provoquerait pas chez les dirigeants une réaction du type « Si ChatGPT sabote quelque chose, quel est notre risque financier ? ». Il va y avoir énormément de managers intermédiaires et supérieurs qui vont partager cet article sur LinkedIn
Un LLM ne peut pas « faire d’erreur ». Ce n’est pas déterministe, cela ne peut ni raisonner, ni penser, ni exécuter une logique. C’est un générateur de salade de mots statistique extrêmement sophistiqué, et comme il n’y a aucune garantie que la sortie soit correcte ou exacte, parler d’« erreur » n’est même pas approprié par définition
Édition : après avoir été massivement downvoté pour des raisons évidentes, l’article a soudainement grimpé dans le classement, ce qui laisse penser qu’un modérateur l’a boosté : https://hnrankings.info/40627558/
C’est assez drôle qu’un article suffisamment putaclic pour justifier un changement de titre selon les règles ait été boosté par un modérateur. Et le fait que l’auteur semble appartenir à une société Y Combinator est sûrement une pure coïncidence : https://news.ycombinator.com/item?id=40629998
Il a expliqué que, pour la génération d’UUID de clé primaire, il fallait utiliser directement le callable
uuid.uuid4plutôt questr(uuid.uuid4()), parce que SQLAlchemy appelle la fonction lorsqu’il génère la valeur. Il a aussi dit que la valeur par défaut de dateserver_default=text("(now())")pouvait ne pas se comporter comme prévu et a recommandéfunc.now(), a suggéré de vérifier les imports deuuidet detextde SQLAlchemy, et a aussi proposé d’envisagerDateTime(timezone=True)pour la gestion du fuseau horaireEnsuite, il a proposé comme correctif
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()), unique=True, nullable=False), et ici l’ajout delambda:a corrigé le problèmeuuid.uuid4()comme une définition de schéma dans Prisma ou quelque chose du genre. Donc le bug en lui-même ne me surprend pas, et j’aurais moi aussi pu faire la même erreurMalgré tout, un simple
kubectl logsaurait suffi à le corriger immédiatement. Et puis passer de Next.js et Prisma à Python ? Pourquoi ?L’erreur elle-même se comprend. Même sans ChatGPT, cela semble assez facile à laisser passer en écrivant le code
En revanche, je ne comprends pas pourquoi elle n’a pas été détectée après le premier échec. Cette entreprise n’avait-elle aucun logging ? Le fait que le backend tentait de réutiliser un UUID aurait dû apparaître immédiatement dans l’erreur
Se tromper en passant une chaîne à une fonction qui attend un callable au lieu d’un
String, c’est courant. Si l’on n’avait pas utilisé d’ORM, ce problème précis aurait probablement été évité, mais c’est peut-être juste mon biais personnel contre les ORM. Des bugs comparables peuvent très bien survenir dans d’autres contextes que les bases de donnéesCeux qui affirment avec aplomb qu’ils auraient forcément trouvé ce bug sont soit de bien meilleurs ingénieurs que moi, soit, plus probablement, légèrement dans l’illusion quant à leurs propres capacités
En revanche, l’absence de logs, ou le fait de ne pas les avoir consultés, est vraiment difficile à comprendre. Avec ECS, j’aurais pensé qu’une exception Duplicate Key serait remontée vers CloudWatch sans configuration particulière ; je me demande donc si cela n’a pas été le cas, ou si cela l’a été mais que personne n’a vérifié pendant la nuit quelles exceptions s’étaient produites
Dans une telle situation, il est utile de se demander pourquoi la détection a été tardive et pourquoi le diagnostic a pris si longtemps
J’ai vu plusieurs fois la même erreur dans du code écrit par des humains aussi. Surtout en React / TypeScript / JavaScript, il arrive souvent que quelqu’un oublie une lambda
J’ai l’impression que l’article de blog n’explique pas correctement la cause profonde du problème et passe directement à la faute de ChatGPT. Quand on travaille dans l’urgence et qu’on envoie sur la branche principale un commit avec de gros changements ou sans revue par des collègues, ce genre de chose arrive
Le vrai problème, c’est que si on se précipite, qu’on prend des raccourcis et qu’on ne fait pas assez de tests ni de revue de code par les pairs, des erreurs se produisent. Il semble qu’un simple test essayant plusieurs options d’inscription aurait permis de le repérer immédiatement
Si on place ce genre de personne près d’un code financièrement critique, on obtiendra des problèmes similaires, et je remettrais en question le jugement de la personne qui a décidé de déployer ce code avec presque aucun test
Je suis surpris qu’il n’y ait pas eu de règle de lint pour ce cas
J’espère que ce n’était pas le cas
Le passage « le projet était au départ un NextJS full-stack, mais je voulais d’abord tout migrer vers Python/FastAPI » ouvre les yeux
Je ne vois pas comment une startup sans clients peut justifier une réécriture
Qu’il y ait des clients ou non, je ne vois pas pourquoi faire, si tôt, ce qui est essentiellement un déplacement horizontal de Node vers Python. S’il y avait des centaines de clients et qu’on envisageait de passer à quelque chose comme Go, je pourrais à la rigueur le comprendre, mais même là j’aurais encore des doutes
Par exemple, il faut créer une quantité énorme d’objets DTO, mais AutoMapper ne fonctionne pas avec la combinaison de versions et la configuration de projet que j’utilise, et Entity Framework comme la sérialisation/désérialisation JSON apportent plus de douleur que de bénéfices
Bien sûr, on peut résoudre cela progressivement. En creusant profondément la documentation, en bricolant un peu, en mettant à niveau des paquets et en réécrivant la configuration. Mais en tant qu’humain, on a envie de prendre un bidon d’essence métaphorique, de tout incendier, puis de construire un deuxième système meilleur. Évidemment, en pratique, il n’est pas réellement meilleur, il crée juste d’autres points de douleur, et il se peut même qu’il ne fasse pas tout ce que faisait le premier système, ou pas correctement
J’ai la même impulsion chaque fois que je vois un système legacy ou pénible au travail. Il faut un effort actif et constant pour résister à la partie du cerveau qui crie de tout réécrire. Parfois, une réécriture ou un changement d’architecture comme l’introduction de conteneurs fonctionne bien, mais le plus souvent cela mène aux flammes ou à un travail sans fin
Sauf quand on a un haut niveau de confiance que cela améliorera l’exploitation du système ou l’expérience développeur des autres développeurs, il vaut mieux ne pas céder à cette impulsion
ChatGPT a plutôt été ce qui a permis à l’app de générer de l’argent. Sans ChatGPT, ils n’auraient pas eu la capacité de l’implémenter
Ce sont l’incapacité à coder, déboguer, journaliser et superviser qui ont fait perdre 10 000 dollars, et dans cette histoire ChatGPT a un effet net positif
Tous les messages de commit contiennent des émojis. Singes, bananes, fusées, feux d’artifice, il y a de tout
https://grook.ai/share?id=e269e88a7b1a71eff4f176c864b30161&x...
C’était à l’origine un NextJS full-stack, et ils migraient le backend vers Python/FastAPI en traduisant le modèle de base de données Prisma/Typescript en Python/SQLAlchemy. D’après eux, comme ce travail était ennuyeux et qu’ils avaient constaté que ChatGPT s’en sortait plutôt bien, ils l’ont utilisé pour presque toute la migration
S’il n’y avait pas eu ChatGPT au départ, ils n’auraient probablement pas tenté cette migration préalable, donc il est difficile de dire que l’effet net est positif. L’ancienne stack avait peut-être une meilleure journalisation des erreurs, ou peut-être pas, et comme le code aurait été écrit directement par eux, ils en auraient peut-être mieux connu la structure et en auraient eu moins besoin
La décision elle-même de « réécrire tout le code une seconde fois » avant même d’activer la monétisation est aussi intéressante
Il y a cette phrase : « Je veux d’abord dire que les pratiques ici étaient mauvaises et auraient pu être évitées. Cela s’est produit à une autre période où la pression temporelle était forte. Merci de lire cela en gardant ce point à l’esprit. »
C’est ce genre de contraintes qui rend les abonnements logiciels inquiétants
Il m’est même arrivé de facturer un utilisateur deux fois à cause d’une condition de concurrence. Du coup, quand je vois un timeout ou une erreur liés à de l’argent, je deviens assez paranoïaque pour supposer d’abord que le paiement est passé, puis vérifier ensuite
Du code en TypeScript et en Python, des frameworks comme Next.js, 5 instances pour chacune de 8 tâches AWS, et au final 40 dollars de chiffre d’affaires pour seulement quelques semaines de développement ?
Je me suis demandé ce qui avait bien pu se passer. Ils ont corrigé en disant que le code était en désordre à cause de contraintes de temps, mais c’est encore pire qu’ils aient passé du temps à refactorer entre langages et à construire un système distribué sans aucune raison
C’était une complexité autodestructrice à jongler en même temps avec les fonctionnalités et une complexité technique absurde. Je ne comprends pas ce qu’ils avaient en tête
Édition : c’est une boîte du batch été 2023 de YC, et il semble qu’à l’été 2024 le produit soit encore derrière une liste d’attente. Sans doute parce qu’ils sont en train de tout réécrire en Rust
On dirait qu’il n’a même pas écrit 1 000 lignes de Python au total, et pourtant il a bien identifié le problème
Python a un défaut : il n’a pas correctement repris la stratégie d’évaluation de Common Lisp. Si, dans l’expression de valeur par défaut d’un argument optionnel, on a quelque chose comme
foo=obj.whatever(), alorsobj.whatever()est évalué non pas au moment de l’appel de la fonction, mais au moment où la définition de la fonction est traitéeJe soupçonne que c’est intentionnel pour des raisons d’efficacité. Python a aussi un autre défaut : il n’existe pas de vraie syntaxe littérale pour des objets courants comme les listes.
[1, 2, 3]n’est pas un littéral, mais quelque chose qui ressemble davantage à un constructeur, et à chaque évaluation il faut créer une nouvelle liste et y insérer les valeursLe concepteur n’a probablement pas voulu qu’un paramètre comme
list=[]crée une nouvelle liste vide à chaque fois qu’un argument est omis. En Lisp,'(1 2 3)et'()sont de vrais littéraux et pointent vers le même objet à chaque référence. Le programmeur peut choisir d’écrire(list 1 2 3)ou'(1 2 3)comme expression de valeur par défautLe premier crée, comme
[1, 2, 3], un nouvel objet mutable à chaque fois, tandis que le second renvoie presque certainement le même objet, qui ne peut pas être modifié de façon fiable et portable. Les langages populaires modernes ont récupéré la plupart des fonctionnalités de Lisp, si bien que ça ressemble à une blague disant qu’il n’y avait rien à perdrefoo=obj.whatever(),obj.whatever()est évalué non pas au moment de l’appel de la fonction mais au moment du traitement de la définition de la fonction, ne semble pas pouvoir être correcteJe ne vois pas ce qu’il se passerait si
.whatever()dépendait d’un état interne qui change après l’initialisation de l’objet