Предисловие

В начале я хотел бы сказать, что описанные в статье практики очень плохи и неправильны (с тех пор мы добавили надёжные юнит-тесты и интеграционные тесты, а также систему алертов/логирования), что их следует избегать и в первую очередь это человеческие ошибки, которые задним умом кажутся очевидными.

Описанные в статье события произошли в условиях сильного дефицита времени на очень ранних этапах работы компании (первые несколько недель). По большей мере я публикую это как забавную историю с уникальными условиями, связанными с воспроизводимостью бага в продакшене (тоже из-за нашей собственной глупости). Пожалуйста, помните об этом, читая текст.

Asim Shrestha
Колесо крутится, но хомячок подписки помер. Оно просто крутится.

Впервые мы включили монетизацию нашего стартапа в мае прошлого года. Мы не ожидали многого, но были приятно удивлены тем, что меньше чем за час после запуска у нас появился первый клиент. Это был волшебный момент. Мы отправили ему благодарность, произнесли всей командой тост и пошли спать, ведь на подготовку мы потратили две ночи.

Проснувшись утром, мы увидели больше сорока уведомлений Gmail с жалобами пользователей. Похоже было, что за ночь поломалось всё. Никто из пользователей не мог подписаться. И мы понятия не имели, почему.

Наш путь к монетизации

Должен сказать, что в мае начался батч S23 YCombinator, а мы не знали точно, какое направление развития будет оптимальным после запуска. Наш групповой партнёр с YC Dalton порекомендовал использовать как ориентир платные подписки и при этом удвоить стоимость любого ежемесячного тарифа, какой бы мы ни придумали. В конечном итоге мы остановились (неохотно) на сумме $40 в месяц. После совещания мы незамедлительно приступили к работе над подготовкой монетизации. Изначально весь стек нашего проекта состоял из NextJS, но мы сначала хотели выполнить миграцию всего на Python/FastAPI. Мы сделали это (частично с помощью ChatGPT), реализовали полную интеграцию Stripe… после чего выдались пять суток самого жестокого за весь месяц недосыпа. (Да, пять дней — долгий срок для нахождения этого бага.)

В течение этих пяти дней мы просыпались в страхе, зная что, увидим в почте 30/40/50 жалоб. Мне бы хотелось чётко донести, сколько клиентов мы потеряли из-за этого. 50 писем в день x 5 дней x $40 =$10000 в месяц потерь на продажах. И это только от тех людей, которые не поленились оставить жалобу. Мы ежедневно отвечали на эти письма, как будто это была наша основная работа. Пользователи жаловались на бесконечный индикатор загрузки, появлявшийся при нажатии на кнопку подписки. Мы начали изучение проблемы с создания нового аккаунта, убедившись, что у нас подписки работают без проблем. Это сбило нас с толку. Ничто из того, что мы делали, не позволяло воссоздать ошибку. Более того, в течение наших часов работы мы практически не получали жалоб.

Галлюцинация на $10 тысяч

По нашим ощущениям, от момента выявления ошибки до её устранения прошёл месяц. После пяти дней бесконечных электронных писем, сотен логов Sentry, долгих переписок с инженерами Stripe и многих часов изучения пяти ключевых файлов мы её обнаружили. Прежде чем продолжить чтение, попробуйте найти её самостоятельно.

Asim Shrestha

Виновником оказалась одна невинно выглядящая строка. Строка, которая отравляла нам жизнь целую неделю. Строка, которая в буквальном смысле стоила нам $10 тысяч. Зловещая строка 56.

Asim Shrestha

Произошло следующее: в рамках миграции бэкенда мы переносили модели баз данных с Prisma/Typescript на Python/SQLAlchemy. Это был очень монотонный процесс. Мы обнаружили, что ChatGPT превосходно справляется с этой трансляцией, поэтому использовали его на протяжении почти всей миграции. Мы копипастили генерируемый им код, видели, что всё работает хорошо, пробовали в продакшене, видели, что он тоже работает, а затем продолжали процесс. Однако на этом этапе мы по-прежнему использовали для всех вставок в базу данных наш Next API. а Python выполнял лишь чтение из базы данных. Впервые мы начали вставлять записи БД на Python, только когда реализовывали подписки. Хотя в процессе этого мы создавали совершенно новые модели SQLAlchemy вручную, получилось так, что мы просто скопировали тот же формат, который ChatGPT написал для старых моделей. Однако мы не заметили того, что продолжали копипастить одну и ту же ошибку генерации ID во всех наших моделях.

Ищем баг

Проблема со строкой 56 заключалась в том, что для генерации UUID наших записей мы просто передавали единственную строку ID. Из-за этого после того, как один новый пользователь выполнял подписку и получал этот ID в каждом отдельном инстансе бэкенда, ни один другой пользователь не мог заново выполнить процесс подписки, потому что это приводило к коллизии уникального ID. Эта проблема оказалась очень хорошо спрятанной из-за структуры нашего бэкенда. У нас было восемь задач ECS в AWS, все они управляли пятью инстансами нашего бэкенда (да, знаю, это перебор, но, честно говоря, у нас были лишние кредиты AWS). Это означало, что у каждого пользователя был пул из сорока потенциально уникальных ID, в которые он мог попасть.

В течение рабочего дня всё было нормально. Мы выполняли коммиты по 10-20 раз в день (разумеется, прямиком в основную ветвь), что вызывало новые развёртывания бэкенда, давая нам сорок новых ID, которые потенциально могли использовать клиенты. Однако ночью, когда мы наконец переставали вносить коммиты (какие мы лентяи, правда?), единственный ID на каждом сервере задействовался, а все новые подписки приводили к коллизиям ID. Пользователи начинали с сорока потенциальных серверов, на которых можно было бы выполнить подписку, но в течение ночи это число быстро снижалось до нуля. Решив наконец эту проблему, мы ощутили, как будто сбросили гору с плеч. Обнаружив источник проблемы, моей коллега Адам быстро запушил исправление, и впервые за эту неделю мы наконец могли спокойно поспать (на самом деле, не совсем, потому что у нас было ещё с десяток проблем, но это уже тема для отдельной статьи).

Заключение

Оглядываясь назад, могу сказать что несмотря на мучительность этих пяти дней, это был один из тех моментов стартапа, которые мы никогда не забудем. Как и все стартапы, мы совершили в процессе развития миллион ошибок, но эта, вероятно, была наихудшей. Возможно, позже я расскажу и об остальных. Сейчас мы просто рады, что можем вспомнить те дни и посмеяться. Да, нам нужно было больше тестировать код. Да, нам не следовало копипастить код. Да, нам не следовало пушить напрямую в основную ветвь. Тем не менее, я не сожалею об этом опыте.

Комментарии (18)


  1. grvelvet
    10.06.2024 09:17
    +19

    Гениальный план, надёжный как швейцарские часы.


  1. astec
    10.06.2024 09:17
    +13

    Интересно, а строку 45 они поправили?...


  1. vtal007
    10.06.2024 09:17
    +15

    Сами накосячили, а виноват чат-гпт :)


  1. IIopy4uk
    10.06.2024 09:17
    +5

    Тестировать нельзя релизить


  1. vagon333
    10.06.2024 09:17
    +1

    Умничать не вижу смысла, лишь сделал для себя do and don't выводы.
    Кстати, тоже пользуюсь GPT для трансляции, интерпретации и визуализации логики кода.


  1. leon-mbs
    10.06.2024 09:17
    +4

    Логично. Перед восстанием машин ИИ сначала пустит всех по миру


  1. Wiggin2014
    10.06.2024 09:17
    +3

    никак не пойму почему это каждый раз при вызове uuid.uui4() получался один и тот же uuid.


    1. alexac
      10.06.2024 09:17
      +3

      UUID получается разный, но дефолт для колонки будет сохранен от одного единственного вызова.


    1. pqbd
      10.06.2024 09:17
      +3

      default вычистится один раз на инстанс и будет передаваться одно и то же значение


    1. santjagocorkez
      10.06.2024 09:17

      Всё дело в том, что в данном случае скобки были не нужны, да и вообще, надо было через server_default и text() делать. Да и вообще, там в первой таблице ещё два кандидата в PK, а в этой subscription_id. id в обеих таблицах не нужен. Просто у них фронтендеры полезли в бэкенд, итог закономерен. Не удивлюсь, если они скоро телепортируются в масло и запилят графкул


  1. Aleator13
    10.06.2024 09:17

    Желаю удачи Команде в решении глобальных и не очень ошибок !


  1. Vplusplus
    10.06.2024 09:17
    +1

    Интересно, каков смысл использовать uuid4 в этом проекте? Типа сразу строим систему планетарного масштаба, где все данные распределены между континентами и разделены во времени? Если бы был обычный инкрементный primarykey, то такой детской ошибки не было.


    1. AxeFizik
      10.06.2024 09:17
      +3

      Ну это нормальная ситуация, когда микросервисов больше чем пользователей)


  1. titan_pc
    10.06.2024 09:17

    Тот случай когда надо было попросить чат переложить все миграции на чистый sql. А оно само напечаталось "Хочу и дальше орм юзать"


  1. CitizenOfDreams
    10.06.2024 09:17
    +2

    Мы выполняли коммиты по 10-20 раз в день (разумеется, прямиком в основную ветвь)

    Здесь есть программисты? Подскажите, а это вообще нормально?


    1. tarielr
      10.06.2024 09:17
      +12

      Для компании, которая полностью полагается на код написанный ИИ и не тестирует его от слова совсем, абсолютно нормально.


  1. BrNikita
    10.06.2024 09:17

    Что за проект?


  1. fugasio
    10.06.2024 09:17

    40$?