Неделя 133. Имя на двери: семь тысяч строк CRM за пятницу и пул соединений, умиравший с каждым запросом

Неделя 133. Имя на двери: семь тысяч строк CRM за пятницу и пул соединений, умиравший с каждым запросом

Разбор этой статьи

AI-подкаст BotsellerИмя на двери: семь тысяч строк CRM за пятницу и пул соединений, умиравший с каждым запросом
0:00 / 0:00

Эту тему разобрали в подкасте. Слушай параллельно с чтением.

У Стругацких понедельник начинается в субботу. Неделя 133 у нас началась в воскресенье вечером: в 22:09 пятнадцатого февраля я закрыл разбор инцидента, из-за которого очередь сообщений выросла с пяти тысяч до семидесяти. А закончилась она через семь дней, тоже поздно вечером, серией фиксов для чатов, которые молча возвращали пустоту. Между этими двумя точками уместились почти сто пятьдесят коммитов в двенадцати проектах, три поздних смены и одна суббота, заложившая фундамент целого канала.

Я Дмитрий Дьяконов, основатель Botseller AI. Это седьмой выпуск серии «Ретроспектива», бортжурнал в прошлое: от настоящего к первому коммиту. В выпуске о неделе 134 я разбирал рождение массовых рассылок и эндпоинт, которого не было. Сегодня отматываю ещё на неделю назад, на 16-22 февраля 2026 года. И у этой серии по-прежнему есть привилегия, которой нет у обычного дневника: я знаю, чем всё закончилось. Знаю, что субботний марафон вокруг официального WhatsApp через три недели превратится в полноценный канал. Знаю, что скромная QR-модалка для WhatsApp станет началом самообслуживания всех каналов. И знаю, что самый противный баг недели вернётся к нам в другом обличье уже через семь дней.

Понедельник начинается в воскресенье: очередь на семьдесят тысяч

Усиление ретраев: 14 ошибок, 70 тысяч сообщений в очереди и механика перемножения повторов

Формально инцидент случился ещё пятнадцатого февраля, но чинился он в воскресный вечер, а в понедельник утром фикс уехал в прод, так что я считаю его открытием недели. Картина была такая: система мониторинга зафиксировала четырнадцать ошибок 500 за сутки, очередь сообщений выросла до семидесяти тысяч при норме в пять, а в базе появились дубликаты транзакций.

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

Лечение вышло на четыре фикса: идемпотентность платёжного вебхука, чтобы повторная доставка не создавала вторую транзакцию; быстрый отказ при сломанном движке логики вместо упорных повторов; дедупликация фоновой синхронизации через Redis; и подтверждение сообщения очереди даже при неудачной генерации, потому что генератор внутри и так повторяет сам. Урок, который я тогда сформулировал для себя: ретраи не складываются, а перемножаются. С тех пор любой новый повтор в системе мы проектируем с вопросом «а что повторяется уровнем выше».

Шторм 5xx: пул соединений, который умирал с каждым запросом

Шторм 5xx: 146 ошибок за 15 минут и HTTP-клиент, который пересоздавался на каждый запрос

В понедельник ночью, в 23:13, закрылась вторая детективная история. Несколько дней нас донимали всплески ошибок 5xx: около ста сорока шести за пятнадцать минут в пике. Настройки пула соединений были выставлены аккуратно: размер, таймауты, лимиты. И не работали вообще.

Причина оказалась из тех, за которые одновременно стыдно и смешно. HTTP-клиент создавался заново на каждый запрос и уничтожался сразу после него. Вместе с ним умирал и пул соединений. Все наши тонкие настройки пула были декоративными: мы тщательно настраивали то, что жило доли секунды. Починка свелась к переводу всех внутренних клиентов на разделяемые экземпляры с нормальным жизненным циклом: пул на двадцать соединений для одного сервиса, на сто для другого, на пятьдесят для третьего.

Сейчас, зная продолжение, я читаю этот эпизод как первую главу большой темы. Через неделю, на 134-й, мы обнаружим, что логи из обработчиков очередей молча исчезают. Ещё через месяц начнём строить полноценную наблюдаемость. Неделя 133 была первым звонком: система уже была достаточно большой, чтобы ломаться не в коде, а в связках между сервисами, и достаточно немой, чтобы не уметь об этом рассказать.

QR-код вместо инструкции: рождение самообслуживания

QR-авторизация WhatsApp: модалка, опрос статуса, Redis-замок и начало самообслуживания каналов

В ночь с понедельника на вторник, между часом и половиной второго, парой коммитов на фронте и бэке появилась фича, значение которой я тогда недооценивал: авторизация WhatsApp по QR-коду прямо из личного кабинета.

До этого подключение WhatsApp было процедурой с участием нашей команды: написать в поддержку, дождаться настройки, получить инструкцию. Теперь клиент открывал модалку, сканировал QR-код телефоном, и канал подключался сам. Под капотом это сложнее, чем звучит: опрос статуса раз в секунду, машина состояний от «готовим код» до «авторизован», защита от двойного создания инстанса через атомарный замок в Redis, обнаружение ситуации «уже авторизован», типизированные ошибки на каждый случай.

Читатель прошлого выпуска уже знает, во что это выросло: через неделю по тому же образцу подключение по QR-коду получит мессенджер MAX, а самообслуживание раскатится на девять каналов разом. Родословная у всей этой ветки одна, и она начинается здесь, в ночной модалке для WhatsApp. Если бы я тогда понимал, что строю не фичу, а паттерн, я бы сразу заложил общий каркас. Не понимал. Каркас появился позже, после того как один и тот же код был скопирован в третий раз.

Среда: выключатель для ботов

Выключатель для ботов: 48 файлов, 2 миграции, жизненный цикл для всех каналов разом

Середина недели ушла на фичу с самым скучным названием и самым заметным эффектом: флаг «включён/выключен» для ботов. Сорок восемь файлов, две миграции, все каналы разом: почта, WhatsApp, Telegram, MAX и весь стек агрегатора с его девятью мессенджерами.

За скучным названием стояла живая боль. Бот в нашей системе это процесс, который где-то запущен. Клиент приостановил работу, а процесс продолжает жить: держит ресурсы, ловит сообщения, иногда отвечает из архива, что особенно неловко. До этой недели «выключить бота» означало «удалить бота». Теперь появился настоящий выключатель, а вместе с ним и логика перезапуска: включённые боты после рестарта системы поднимаются сами, выключенные остаются лежать.

В тот же день в сервисе личных Telegram-аккаунтов появился парный фикс из двух строк с прекрасным сообщением коммита: «Написал логику которая гасит телетон ботов которые не должны работать». Два репозитория, одна мысль: у бота должен быть жизненный цикл, а не только кнопка «создать». Задним числом это выглядит очевидным. Но очевидным оно стало именно после того, как мы наступили на все грабли ботов-сирот, живущих в системе без хозяина.

Двойное списание и страховка, сработавшая через сутки

Двойное списание при диалоговом биллинге: фикс и вторая линия обороны через сервис данных

Среда же принесла самый неприятный тип бага: денежный. При диалоговом биллинге списание происходило дважды: один раз сразу при создании транзакции и второй раз при пакетной обработке. Клиент платил за один диалог как за два.

Сам фикс был обычным: флаг «пропустить обновление баланса» там, где транзакция уже учтена. Интересно другое, и именно ради этого я рассказываю историю. Вместе с фиксом мы добавили вторую линию обороны: страховку на уровне сервиса данных. Если кто-то из вызывающего кода забудет передать флаг, страховка сама не даст списать баланс повторно и громко напишет об этом в лог с пометкой предупреждения.

Страховка сработала через сутки. В четверг утром обнаружился вызов, где флаг забыли прописать. Забыл, честно говоря, я сам. Но вместо повторного двойного списания у клиентов мы получили предупреждение в логе и пятиминутный фикс. Один день жизни защитного кода окупил все сорок минут, потраченные на его написание. С тех пор у меня простое правило для любого кода, который трогает деньги: первая линия обороны делает правильно, вторая линия не даёт сделать неправильно, и обе пишут в лог.

Пятница: CRM обретает лицо

Интерфейс CRM за одну пятницу: 67 файлов, 7000 строк, конструктор сущностей и фильтры

Если считать по строкам, главный день недели случился в пятницу. В 16:11 в CRM уехал бэкенд-коммит почти на две тысячи строк с пятью миграциями. В 16:22, через одиннадцать минут, фронтовый коммит на 67 файлов и без малого семь тысяч строк. Вместе они собрали то, что сегодня выглядит самой обычной частью продукта: интерфейс CRM.

Страница настроек с конструктором типов сущностей. Универсальный сайдбар лида с динамическими табами. Чаты внутри карточки: окно переписки, отправка первого сообщения, счётчик непрочитанных. Конструктор колонок таблицы лидов и фильтры шести типов на каждую колонку: текст, дата, число, флажок, множественный выбор. История служебных признаков. Всё то, что пользователь простой CRM воспринимает как данность, появилось за одну пятницу.

Ночью работа продолжилась: в 02:17 бэкенд научился переименовывать группы полей и перетаскивать воронки, в 02:18 подтянулся фронт. Парные коммиты с разницей в минуту: по ним видно, как один человек жонглирует двумя репозиториями. А в 02:49 закрылся системный фикс доступов сразу в двенадцати сервисах: боты без привязанной логики блокировали пользователей на этапе авторизации, потому что проверка прав не предусматривала состояние «бот создан, логика ещё не назначена».

Зная будущее, я смотрю на эту пятницу с двойным чувством. С одной стороны, это фундамент: именно на этом интерфейсе через неделю вырастут массовые рассылки, а сага их рождения описана в выпуске о неделе 134. С другой стороны, семь тысяч строк фронта одним коммитом это ровно тот стиль, который неделей позже подарит нам восемь тысяч строк рассылок одним коммитом и две недели фиксов следом. Мы умели строить быстро. Мы ещё не умели за это не платить.

Суббота: официальный канал и имя на двери

Суббота: 20 коммитов, шаблоны WhatsApp Business API, визарды и парсинг чужого API

А теперь главный сюжет недели. Подключение WhatsApp по QR-коду, о котором я рассказывал выше, быстрое и удобное, но у официального WhatsApp Business API другой уровень: верифицированный отправитель, шаблоны сообщений, проходящие модерацию, предсказуемые правила игры. К этому каналу мы шли давно, и суббота 21 февраля целиком легла в его фундамент.

С 09:28 до 14:56 в двух репозиториях синхронно легло около двадцати коммитов: страница шаблонов WhatsApp, отправка сообщений через визард, статусы, уведомления об ошибках. Бэкенд и фронтенд шли парами с шагом в десять-тридцать минут: по хронологии видно, как одна и та же пара рук перекидывает мяч между двумя половинами системы.

Марафон парсинга: 7 фиксов подряд, борьба с непредвиденными статусами и ребрендинг Platform в Botseller

Из субботних коммитов торчат детали, по которым я сейчас реконструирую то состояние. Семь фиксов парсинга шаблонов подряд: внешний API отдавал шаблон склеенным текстом, из которого пришлось вырезать заголовок, тело, подвал и кнопки по очереди, и каждый следующий шаблон ломал предыдущую логику. Краш страницы из-за статуса «отклонён», который мы не предусмотрели. Дублирование заголовка в теле. По коммитам виден человек, который держит в голове дедлайн и коллекционирует сюрпризы чужого API в режиме реального времени.

В этот же день случилось событие, которое я нежно люблю: в разгар марафона платформа впервые назвала себя по имени. Коммит «логотип и „Ботселлер” вместо „Platform”». Почти всю свою жизнь личный кабинет носил рабочее имя Platform, и собственное имя он получил не на торжественной презентации, а буднично, между двумя фиксами парсинга шаблонов. Настоящие вехи обычно так и выглядят: их не замечаешь, пока не перечитаешь историю.

Итог субботы: временный код, через три недели ставший фундаментом полноценного канала WhatsApp Business API

Теперь то, чего я не знал той субботой. Через три недели официальный канал WhatsApp родится как отдельный сервис за 48 часов, и «временный» субботний код честно умрёт, что для временного кода редкая судьба. Канал станет одним из главных в платформе, а о подключении WhatsApp Business API мы напишем отдельный гайд. Одна суббота фундамента окажется одной из самых рентабельных суббот в истории компании.

Воскресенье: чаты, которые молчали

Пустые чаты: запрос по несуществующему полю, проглоченное исключение и правдоподобная ложь

Финал недели вышел симметричным её началу: снова вечер выходного, снова тихий баг. Список чатов в CRM возвращался пустым. Не ошибкой, не пятисоткой, а именно пустотой: у клиента есть переписки, а система показывает ноль.

Причина заслуживает места в учебнике. Запрос фильтровал чаты по полю, которого в таблице чатов не существует. Запрос падал, но базовый слой доступа к данным глушил исключение и возвращал «ничего не нашлось». Система не сообщала об ошибке. Она врала, что всё в порядке, просто данных нет. Фикс переписал выборку на честный JOIN через сущности к лидам, а заодно в воскресную серию вошли совместимость со старым форматом токенов и сервисная авторизация между модулями.

Если вам кажется, что тема повторяется, вам не кажется. Усилитель ретраев, декоративный пул, проглоченное исключение: вся неделя 133 состоит из историй о том, как система молчит о своих проблемах. Через неделю к ним добавится форматтер логов, молча съедающий записи из обработчиков очередей. Сегодня я знаю, что это была не серия совпадений, а диагноз этапа: мы построили распределённую систему быстрее, чем научили её разговаривать.

За пределами платформы: два инструмента за одну минуту

Диагностическая матрица недели 133: четыре инцидента, истинные причины и уровень шума каждого

В субботу в 20:08, через пару часов после юридических страниц, в двух новых репозиториях одновременно появились первые коммиты: почти двадцать пять тысяч строк на двоих. Два внутренних инструмента, готовившихся параллельно и отгруженных в одну минуту.

Первый: монитор индексации. Семь таблиц, клиент к вебмастерским API, дашборд с трендами, ежедневный автосбор. Мы тогда сражались за индексацию сайта в поисковиках, и ручная проверка «сколько страниц в индексе» съедала время каждый день. Второй: конвейер коротких видео из новостей. Сбор новостей, сценарий, озвучка, генерация видео, публикация на шесть площадок. Забегая вперёд: монитор индексации жив до сих пор и каждое утро присылает сводку, а конвейер видео прошёл свой путь проб и остановок, о котором ещё будет отдельный рассказ.

Тут же, рядом, всю неделю рос третий инструмент: мониторинг команды, который научился собирать утренний план дня, считать скорость по задачам и трижды в день делать контрольные срезы. Его история недели это отдельная комедия про фильтрацию шума: сначала он видел 9800 задач по 41 проекту и слал отчёты, которые никто не читал. К воскресенью после серии фильтров осталось около тридцати задач на двух досках, и отчёты внезапно стали полезными. Инструмент, который видит всё, не видит ничего.

Урок недели: молчаливая ошибка дороже громкой

Фреймворк трёх вопросов: кто узнает о падении, перемножаются ли повторы, говорит ли страховка вслух

Каждый выпуск я стараюсь заканчивать одним уроком, который пережил свою неделю. Урок недели 133 сформулировался сам, из четырёх её историй сразу.

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

Из этого вырос фреймворк, по которому мы с тех пор проверяем любой код на пути денег и сообщений. Три вопроса. Первый: если этот код упадёт, кто узнает и как быстро? Ответ «никто» означает, что падение будет молчаливым, и это надо чинить до релиза. Второй: что повторяется уровнем выше и не перемножатся ли повторы? Третий: есть ли вторая линия обороны, и говорит ли она вслух, когда срабатывает? Молчаливая страховка почти так же опасна, как её отсутствие: она прячет проблему, вместо того чтобы дать её починить.

Громкая ошибка стоит столько, сколько стоит её фикс. Молчаливая стоит столько, сколько успело сломаться, пока вы о ней не знали. Разница между этими суммами и есть цена наблюдаемости.

Цифры недели

  • Почти 150 коммитов в двенадцати проектах за семь дней
  • Больше 44 тысяч добавленных строк, из них около 25 тысяч: два новых внутренних инструмента, рождённых в одну минуту
  • 67 файлов и почти семь тысяч строк: самый крупный коммит недели, интерфейс CRM за одну пятницу
  • Около 20 коммитов за субботу вокруг официального канала WhatsApp
  • 48 файлов и две миграции: выключатель ботов во всех каналах разом
  • Около 146 ошибок 5xx за 15 минут в пике шторма и ноль после фикса пула соединений
  • Очередь сообщений: рост с 5 до 70 тысяч из-за усиления ретраев
  • Три поздних смены: две ночи до 02:49 и воскресный вечер до 23:31

Что было дальше

Дальше: неделя 134 с массовыми рассылками, эндпоинтом статусов и началом наблюдаемости

Дальше была неделя 134: рождение массовых рассылок ночным коммитом на восемь тысяч строк, подключение MAX по QR-коду по образцу субботней модалки WhatsApp и эндпоинт статусов, которого не оказалось. Ещё через три недели заложенный в субботу фундамент превратится в полноценный канал WhatsApp Business API. А тема молчаливых ошибок, открытая на этой неделе, растянется на месяцы и закончится строительством полноценной наблюдаемости: трассировки, алерты, дашборды.

Все выпуски серии собраны в категории ретроспектива, а текущая хроника разработки продолжается в бортовом журнале.

FAQ

Что такое ретроспектива Botseller?

Серия «Ретроспектива», бортжурнал в прошлое: от настоящего к первому коммиту. Каждый выпуск разбирает одну неделю разработки с git-логами, решениями, ошибками и личными уроками основателя. Особенность серии в обратной оптике: события описываются с уже известным финалом. Все выпуски собраны в категории ретроспектива.

Чем официальный WhatsApp Business API отличается от подключения по QR-коду?

Подключение по QR-коду использует обычный аккаунт WhatsApp: быстро и удобно, но с ограничениями обычного аккаунта. WhatsApp Business API это официальный канал: шаблоны сообщений проходят модерацию, отправитель верифицируется, правила прозрачны и предсказуемы. Подробный разбор есть в статье о подключении WhatsApp Business API.

Почему очередь сообщений может внезапно вырасти в десятки раз?

Самая частая причина: усиление ретраев. Если ошибка повторяется на нескольких уровнях сразу (HTTP-клиент, очередь, вебхук отправителя), количество попыток перемножается: пять повторов внутри сервиса при трёх повторах очереди дают пятнадцать вызовов на одну ошибку. Лечится идемпотентностью обработчиков, быстрым отказом при недоступности зависимости и правилом «повторяет только один уровень».

Что такое двойное списание и как от него защищаются?

Это ситуация, когда за одну услугу баланс клиента уменьшается дважды, обычно из-за того, что транзакцию учитывают два независимых участка кода. Защита строится в два слоя: основная логика передаёт признак «баланс уже обновлён», а слой данных дополнительно проверяет этот случай сам и пишет предупреждение в лог, если признак забыли. Вторая линия обороны окупается первой же пойманной ошибкой.

Зачем ИИ-боту встроенная CRM?

Бот, который отвечает клиентам, генерирует главную ценность не в переписке, а в данных: кто написал, что спросил, на каком этапе воронки остановился. Встроенная CRM позволяет боту самому создавать лида, вести его по воронке и передавать менеджеру уже с контекстом. Как это устроено в Botseller, мы разбирали в статье как устроен ИИ-бот под капотом.

Что значит «молчаливая ошибка» в распределённой системе?

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