1 puntos por GN⁺ 2024-06-10 | 2 comentarios | Compartir por WhatsApp
  • Una startup que acababa de activar la monetización sufrió una falla en los pagos de suscripción, pero como el problema no se reproducía internamente, la identificación de la causa se retrasó 5 días
  • La falla comenzó al copiar un patrón de conversión de Prisma/TypeScript→Python/SQLAlchemy generado por ChatGPT, donde en lugar de una función para generar UUID se había puesto una cadena de ID hardcodeada como si fuera el valor por defecto
  • Debido a una estructura de 8 tareas de AWS ECS con 5 instancias cada una, los usuarios podían encontrarse con uno de hasta 40 pools de IDs únicos distintos, y durante el día los despliegues frecuentes ocultaban el problema
  • Por la noche, al detenerse los despliegues, el único ID de cada servidor se agotaba, y después los nuevos intentos de suscripción fallaban por colisiones de ID únicos
  • Con 50 quejas al día, durante 5 días y una suscripción mensual de $40, la pérdida estimada fue de $10,000 al mes; la falta de pruebas, logging y alertas, además de copiar código, agravó la respuesta al incidente

Falla de suscripción revelada justo después de monetizar

  • La startup activó por primera vez la monetización en mayo y consiguió a su primer cliente dentro de la primera hora del lanzamiento
  • A la mañana siguiente, en Gmail se habían acumulado más de 40 quejas de usuarios
    • Los usuarios no podían completar la suscripción
    • Reportaban que al presionar el botón de suscripción aparecía un spinner de carga infinito
  • Crearon una cuenta nueva para comprobarlo directamente, pero internamente la suscripción funcionaba bien y no lograban reproducir la causa
  • Durante el horario laboral casi no había quejas, y la falla se acumulaba sobre todo por la noche

Implementación de monetización bajo presión de tiempo

  • Mayo coincidía con el inicio de la batch YC S23, y el equipo aún no tenía claro cuál era la mejor dirección tras el lanzamiento
  • Dalton, group partner de YC, aconsejó usar a los suscriptores de pago como métrica para definir el rumbo y subir al doble el precio mensual que tenían en mente
  • El precio final quedó en $40 al mes
  • El proyecto originalmente era full-stack en NextJS, pero antes y después del trabajo de monetización avanzaban en una migración a Python/FastAPI
    • Usaron ChatGPT durante la migración
    • También completaron la integración con Stripe
  • Durante los 5 días siguientes durmieron mucho menos y tuvieron que atender entre 30 y 50 correos de queja cada día

El patrón de conversión de modelos generado por ChatGPT

  • Durante la migración del backend trasladaron los modelos de base de datos de Prisma/TypeScript a Python/SQLAlchemy
  • Como convertir los modelos era tedioso y consideraron que ChatGPT lo hacía bien, lo usaron en casi toda la migración
  • El código generado se copiaba y pegaba, luego se verificaba que funcionara, y como en producción parecía operar con normalidad, siguieron adelante
  • En ese momento las inserciones en la base de datos seguían a cargo de la API de Next, y el backend en Python solo leía la base de datos
  • Al implementar la suscripción empezaron por primera vez a insertar registros en la BD desde Python
    • El nuevo modelo de SQLAlchemy lo hicieron manualmente, pero copiaron tal cual el patrón generado por ChatGPT de los modelos existentes
    • El mismo problema quedó en la forma de generar IDs de todos los modelos

La causa real y por qué no se veía de día

  • El error clave fue pasar una cadena de ID hardcodeada en lugar de pasar una función o lambda que generara el UUID
  • Cuando un usuario completaba la suscripción con ese ID en una instancia concreta del backend, los intentos posteriores de suscripción en esa misma instancia provocaban una colisión de ID único
  • La configuración del backend ocultó el problema por más tiempo
    • Operaban 8 tareas de ECS en AWS
    • Cada tarea ejecutaba 5 instancias del backend
    • Los usuarios podían terminar potencialmente en uno de 40 IDs distintos
  • Durante el día hacían entre 10 y 20 commits directos al branch main, y cada uno disparaba un nuevo despliegue del backend
    • Cada vez que ocurría un despliegue, aparecían 40 IDs nuevos disponibles para los clientes
  • Por la noche se detenían los commits y despliegues, así que el único ID de cada servidor se agotaba rápidamente
    • Al principio había cerca de 40 servidores capaces de aceptar suscripciones, pero con el tiempo ese número se acercaba a 0

Magnitud de la pérdida y medidas posteriores

  • La pérdida se estimó como 50 emails/day x 5 days x $40/month, es decir, $10,000 de ingresos mensuales
    • Ese cálculo toma en cuenta solo a los usuarios que enviaron una queja
  • Para encontrar la causa hicieron falta 5 días, muchísimos correos, cientos de logs de Sentry, una larga conversación en Discord con ingenieros de Stripe y revisar cinco archivos clave
  • Después de descubrir la causa, Adam subió rápidamente la corrección
  • Luego añadieron pruebas unitarias y de integración sólidas, alertas y logging
  • Este incidente muestra que cuando se combinan error humano, pruebas insuficientes, copia de código y pushes directos a main, incluso una sola línea pequeña puede terminar causando una gran pérdida de ingresos

2 comentarios

 
znjadong 2024-06-11

Eh, el código generado automáticamente por IA siempre hay que revisarlo; ¿por qué lo usan tal cual?

 
GN⁺ 2024-06-10
Opiniones de Hacker News
  • La falta de monitoreo fue lo que les hizo perder 10 mil dólares. La app estaba generando excepciones de base de datos de forma continua y masiva, pero nadie recibió una alerta.
    Si hubieran tenido esa alerta, habría sido una investigación de 5 minutos, no de 5 días. Si no arreglaron el sistema de alertas, en realidad no arreglaron nada.

    • Exacto. El mensaje de log habría dicho que el ID no era único, y eso habría reducido muchísimo el tiempo de depuración.
      Programar es fácil cuando todo funciona bien; lo difícil es la parte que maneja los problemas.
    • Este tipo de cosas se está volviendo cada vez más común. Las empresas y los fundadores no piensan en la infraestructura porque creen que el proveedor de nube que eligieron lo va a hacer todo mágicamente.
      Desde el momento en que entran clientes de pago, se necesita a alguien con el conocimiento y la experiencia para encargarse de logging, monitoreo, alertas, seguridad, etc. No se puede tratar DevOps como amateur.
    • También es malo no tener pruebas, y es riesgoso usar algo hecho por IA sin revisarlo tres veces.
      Pero lo realmente absurdo es que no haya logging de errores y alertas en la base de datos. No es código legacy de hace 20 años, sino un producto nuevo, y tampoco es código de la época en que se usaban errores de DB como validación de datos.
    • Haber desplegado y luego irse a dormir de inmediato parece una señal de alerta aquí. Deberían haber desplegado alrededor de las 9 a. m. y monitoreado los problemas durante el horario laboral.
    • Parece que ChatGPT no les avisó que necesitaban monitoreo.
  • La publicación del blog da 404, así que dejo el enlace de Web Archive.
    https://web.archive.org/web/20240610032818/https://asim.bear...
    El autor agregó una corrección importante: dice que las prácticas aquí eran muy malas y vergonzosas, y que después añadieron pruebas unitarias/de integración sólidas y alertas/logging. En resumen, fue un error humano y, visto en retrospectiva, algo claramente evitable.
    También agregó que ocurrió durante las primeras semanas de la empresa, bajo una gran presión de tiempo, y que debería tomarse como una historia curiosa sobre lo particular que era reproducir el bug en producción.

  • El error se veía de inmediato. Respeto al equipo, pero esto no tiene mucho que ver con ChatGPT, sino más bien con que usaron un modelo de programación con el que el equipo no estaba lo suficientemente familiarizado.
    Incluso si hubiera pasado una revisión de código, probablemente se habría detectado con herramientas de monitoreo que se configuran en 5 minutos.

    • Para ser justos, si no hubiera estado mirando específicamente para encontrar este bug, creo que no lo habría visto. Aun así, es cierto que se habría detectado de inmediato con monitoreo o incluso con la prueba manual más básica.
    • Parece que el equipo ni siquiera pudo hacer una resolución de problemas básica con los logs de la base de datos o de la aplicación. Este era un error simple, y me preocupa qué harían si apareciera un error transitorio como un bloqueo implícito de una tabla.
    • No fue un error ingenuo: el título es deliberadamente clickbait y está optimizado para búsquedas. Sugiere que ChatGPT cometió el error para provocar clics por ansiedad.
      Un título como “Cometimos un error de programación al usar un LLM y nos costó 10 mil dólares por no hacer aseguramiento de calidad” no generaría en los ejecutivos una reacción del tipo “¿cuál sería nuestra exposición si ChatGPT arruina algo?”. Habrá montones de gerentes medios y altos compartiendo este artículo en LinkedIn.
      Un LLM no puede “cometer errores”. No es determinista, no razona, no piensa ni ejecuta lógica. Es un generador muy vistoso de ensaladas de palabras que usa probabilidades estadísticas, y como no hay garantía de que lo que genera sea correcto o preciso, por definición tampoco corresponde llamarlo error.
      Edición: después de que el artículo fuera fuertemente votado en contra por razones obvias, su posición subió de repente, lo que parece indicar que un moderador lo impulsó: https://hnrankings.info/40627558/
      Es gracioso que los moderadores hayan impulsado un artículo tan clickbait que, según las reglas, debería haber tenido el título cambiado. Además, que el autor parezca pertenecer a una empresa de Y Combinator seguramente es una completa coincidencia: https://news.ycombinator.com/item?id=40629998
    • Curiosamente, otra entidad que detectó este error fue ChatGPT-4o. No puedo compartir un chat con imágenes, pero pegué la imagen del código incorrecto y pregunté “qué está mal en el código”, y me respondió lo siguiente.
      Dijo que para generar el UUID de la clave primaria no había que usar str(uuid.uuid4()), sino pasar directamente el callable uuid.uuid4, y que SQLAlchemy llamaría a la función al crear el valor. Para el valor por defecto de la fecha, dijo que server_default=text("(now())") podría no comportarse como se esperaba y recomendó usar func.now(); también indicó revisar las importaciones de uuid y text de SQLAlchemy, y considerar DateTime(timezone=True) para manejar zonas horarias.
      Después propuso como código corregido id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()), unique=True, nullable=False), y ahí agregar lambda: solucionó el problema.
    • Si tienes muy poca experiencia con Python, me imagino que habrías pensado en uuid.uuid4() como una definición de esquema en algo como Prisma. Por eso este bug en sí no me sorprende, y yo podría haber cometido el mismo error.
      Aun así, con un solo kubectl logs se habría detectado de inmediato. Además, ¿pasar de Next.js y Prisma a Python? ¿Por qué?
  • El error en sí se entiende. Incluso escribiendo el código sin ChatGPT, parece algo que se podría pasar por alto con relativa facilidad.
    Pero no entiendo por qué no lo detectaron después del primer fallo. ¿Esta empresa no tenía logging? El hecho de que el backend intentara reutilizar un UUID tendría que haber sido evidente al ver el error.

    • Ni siquiera supieron que había un error hasta que el cliente se quejó. Tienes que enterarte de qué errores ocurrieron antes que tus clientes; logging, alertas o cualquier forma de monitoreo habría ayudado.
    • Este es el verdadero problema. En un proyecto que se mueve rápido, es muy probable que tarde o temprano aparezca otro bug en producción como este. Solo espero que la próxima vez no tarden 5 días en identificarlo.
    • Que hayan tardado 5 días en entender este bug es bastante demencial.
    • Es totalmente posible que un caso especial como múltiples suscripciones de Stripe quede fuera de las pruebas unitarias habituales. Como dice el artículo, probablemente tampoco habría sido fácil reproducirlo con una prueba de aceptación, y el foco en ChatGPT es algo exagerado tanto por parte del autor como de otros.
      Pasar por error una cadena a una función que espera un objeto callable en lugar de String es común. Si no hubieran usado un ORM, creo que este problema específico se habría evitado, aunque quizá sea mi sesgo personal contra los ORM. Bugs parecidos pueden aparecer perfectamente también en contextos que no sean bases de datos.
      Las personas que aseguran con tanta confianza que habrían detectado este bug o son ingenieros mucho mejores que yo, o, más probablemente, tienen una percepción algo equivocada de sus propias capacidades.
      Dicho eso, que no tuvieran logs o no los miraran sí es realmente difícil de entender. Si era ECS, me parece que la excepción de Duplicate Key se habría propagado a CloudWatch sin configuración adicional; me pregunto si no ocurrió, o si sí ocurrió pero nadie revisó durante la noche qué excepción se había producido.
    • Debido a este tipo de fallas de proceso, Amazon tiene un proceso de corrección de errores, es decir, de análisis post mortem: https://aws.amazon.com/blogs/mt/why-you-should-develop-a-cor...
      En una situación así, es útil preguntar por qué la detección fue tardía y por qué el diagnóstico tomó tanto tiempo.
  • He visto el mismo error varias veces incluso en código escrito por humanos. Especialmente en React / TypeScript / JavaScript, es común que alguien se olvide de una lambda
    El post del blog me dio la impresión de que no explica bien la causa raíz del problema y pasa directo a culpar a ChatGPT. Si trabajas con prisa y metes a main un cambio grande o un commit sin revisión de colegas, pasan estas cosas
    El verdadero problema es que, si te apuras, tomas atajos y no haces suficientes pruebas ni revisión de código por pares, aparecen errores. Creo que con solo tener pruebas que intentaran varias opciones de suscripción lo habrían encontrado de inmediato

    • Mi modelo mental de ChatGPT es el de un ingeniero junior que nunca va a ser ascendido. Es alguien que algún día van a despedir, pero que en cambio puede teclear infinitamente rápido, así que puede ser útil si se usa con muchísimo cuidado
      Si pones a alguien así cerca de código financieramente importante, van a surgir problemas parecidos, y me haría cuestionar el criterio de quien decidió desplegar ese código casi sin pruebas
    • Este tipo de problema es común en muchos lados. Por ejemplo, las propiedades de componentes en Vue pueden tener valores por defecto, pero si usas un objeto o arreglo literal como valor por defecto en vez de una función que devuelva un objeto o arreglo, estás en problemas
      Me sorprende que no hubiera una regla de lint para este caso
    • ChatGPT es solo un factor de distracción. Lo importante no es qué generó el código, sino qué haces con ese código
    • La estructura era que ChatGPT escribía el código, lo pusheaba y luego ChatGPT lo revisaba. /s
      Espero que no haya sido así
  • La parte de “el proyecto original era full-stack NextJS, pero primero quería migrarlo todo a Python/FastAPI” me abrió los ojos
    No sé cómo una startup sin clientes puede justificar una reescritura

    • Pensé lo mismo y creí que me estaba volviendo loco. Me parecía absurdo reescribir tan temprano, pero pensé que nadie en los comentarios lo estaba señalando
      Con o sin clientes, no entiendo por qué hacer tan pronto un movimiento básicamente lateral de Node a Python. Si tuvieran cientos de clientes y quisieran cambiar a algo como Go, quizá podría entenderlo un poco más, aunque seguiría siendo cuestionable
    • Además, estaban reescribiendo en un lenguaje en el que tenían tan poca experiencia que no podían detectar un bug que parecía evidente
    • En mi caso, si fuera un proyecto real en el que estoy trabajando ahora, podría ser porque C# es un buen lenguaje y tanto el runtime como el framework web están bien, pero reduce la velocidad de desarrollo y los puntos de dolor se siguen acumulando
      Por ejemplo, hay que crear un montón de objetos DTO, pero AutoMapper no funciona con la combinación de versiones y la configuración del proyecto que uso, y Entity Framework junto con la serialización/deserialización JSON generan más dolor del valor que aportan
      Claro que se puede resolver de forma incremental. Se puede profundizar en la documentación, mezclar hacks, actualizar paquetes y reescribir configuraciones. Pero, como humano, dan ganas de agarrar un bidón de gasolina metafórico, quemarlo todo y hacer que el segundo sistema sea mejor. Por supuesto, en realidad no necesariamente mejora: solo aparecen otros puntos de dolor, y quizá ni siquiera haga todo lo que hacía el primer sistema, o no lo haga bien
      En el trabajo me pasa lo mismo cada vez que veo sistemas legacy o engorrosos. Hace falta un esfuerzo activo y constante para ganarle al cerebro que grita que hay que reescribir. A veces una reescritura o un cambio de arquitectura, como adoptar contenedores, sale bien, pero por lo general termina en meterse en el fuego o en una tarea interminable
      Salvo que haya una alta confianza en que mejorará la operación del sistema o la experiencia de desarrollo de otros desarrolladores, es mejor no ceder a ese impulso
    • El único fallo de ChatGPT en este caso fue que, por sus capacidades, hizo que una reescritura antes del lanzamiento pareciera algo razonable y fácil en lo que gastar el runway
  • ChatGPT, más bien, fue quien permitió que la app generara dinero. Sin ChatGPT no tenían la capacidad de implementarla
    La falta de capacidad para programar, depurar, hacer logging y monitorear fue lo que tiró a la basura 10 mil dólares; en esta historia, el efecto neto de ChatGPT es positivo

    • Si miras los proyectos de este equipo en github.com/reworkd, la madurez del producto y del equipo queda clara de inmediato. Es desarrollo guiado por emojis
      Todos los mensajes de commit tienen emojis. Hay de todo: monos, bananas, cohetes, fuegos artificiales, etc.
    • Sobre todo si lo ves como un costo de 20 dólares al mes
    • 10 mil dólares es calderilla. Elon podría perder 500 mil millones de dólares por el error de xAI que salió hoy
      https://grook.ai/share?id=e269e88a7b1a71eff4f176c864b30161&x...
    • Parece que capacidad sí tenían, porque ya estaba implementado
      Dicen que originalmente era full-stack NextJS y que, al migrar el backend a Python/FastAPI, estaban traduciendo los modelos de base de datos de Prisma/Typescript a Python/SQLAlchemy. Como esa tarea era tediosa y vieron que ChatGPT la hacía bastante bien, lo usaron para casi toda la migración
      Si ChatGPT no hubiera existido desde el principio, probablemente no habrían intentado esa migración previa, así que es difícil decir que el efecto neto fue positivo. El stack existente pudo haber tenido mejor logging de errores, o no; y como era código escrito por ellos mismos, quizá conocían mejor su estructura y lo necesitaban menos
      La decisión de “volver a escribir todo el código por segunda vez” antes de activar la monetización también es interesante
  • Hay una frase que dice: “Primero quiero decir que las prácticas aquí fueron malas y que esto se pudo evitar. Ocurrió en otro momento, bajo una gran presión de tiempo. Por favor, léanlo teniendo eso en cuenta”
    Por este tipo de restricciones me dan miedo las suscripciones de software

    • Respeto mucho que el autor haya hecho pública esta historia, especialmente con una introducción así. Saber qué errores cometen otras personas es muy útil, pero publicar un error propio puede dar bastante vergüenza
    • Aunque no te gusten las suscripciones, el mundo anterior, en el que se compraban licencias de cientos de dólares por asiento de usuario, tampoco era maravilloso
    • Me ha tocado trabajar con código legacy de suscripciones y puede ser bastante sucio
      Una vez cobramos dos veces a un usuario por una condición de carrera. Por eso, cuando veo timeouts o errores relacionados con dinero, me vuelvo tan paranoico que primero asumo que el pago sí se hizo y luego lo vuelvo a verificar
    • La alternativa es escribirlo uno mismo, pero entonces todo lo demás queda condicionado
    • También es inusual hacer una reescritura bajo una “gran” presión de tiempo
  • ¿Código en TypeScript y Python, frameworks como Next.js, 8 tareas de AWS con 5 instancias cada una, y aun así los ingresos eran de 40 dólares, con apenas unas semanas de desarrollo?
    Me pregunto qué rayos estaba pasando. Es peor que hayan arreglado el código diciendo que estaba hecho un desastre por las limitaciones de tiempo, pero en realidad gastaron tiempo en refactorizar entre lenguajes y en armar un sistema distribuido sin ninguna razón.
    Es complejidad autoinfligida: hacer malabares al mismo tiempo con funcionalidades y una complejidad técnica absurda. No sé qué estaban pensando.
    Corrección: es una empresa de YC del verano de 2023, pero parece que en el verano de 2024 el producto seguía detrás de una lista de espera. Probablemente porque lo están reescribiendo en Rust.

    • Porque tienen 500 mil dólares de capital inicial, otros 1,2 millones encima de eso y también créditos gratuitos de AWS para quemar.
  • Parece que no ha escrito ni 1000 líneas de Python en total, pero identificó bien el problema.
    Python tiene un defecto: no copió correctamente la estrategia de evaluación de Common Lisp. Si en la expresión de valor por defecto de un argumento opcional de una función hay algo como foo=obj.whatever(), entonces obj.whatever() se evalúa cuando se procesa la definición de la función, no cuando se llama a la función.
    Sospecho que lo hicieron deliberadamente por eficiencia. Python tiene otro defecto: no cuenta con una sintaxis de literales reales para objetos comunes como listas. [1, 2, 3] no es un literal, sino más bien un constructor, y cada vez que se evalúa tiene que crear una lista nueva y llenarla con valores.
    El diseñador no habría querido que un parámetro como list=[] creara una nueva lista vacía cada vez que se omite el argumento. En Lisp, '(1 2 3) y '() son literales reales y apuntan al mismo objeto cada vez que se referencian. El programador puede elegir si usar (list 1 2 3) o '(1 2 3) como expresión de valor por defecto.
    El primero crea un objeto mutable nuevo cada vez, como [1, 2, 3], mientras que el segundo casi con toda seguridad devuelve el mismo objeto y no se puede modificar de forma confiable y portable. Como los lenguajes populares modernos tienen la mayoría de las funciones de Lisp, parece una broma eso de que no hay nada que perder.

    • Lo que dice es correcto en sí, pero eso no fue lo que ocurrió en el post del blog. El problema no estaba en una definición de función, sino dentro de una definición de clase.
    • No parece que pueda ser correcto decir que en foo=obj.whatever(), obj.whatever() se evalúa cuando se procesa la definición de la función y no cuando se llama a la función.
      No sé qué pasaría si .whatever() dependiera de un estado interno que cambia después de inicializar el objeto.