Поддержка: доставленное сообщение не теряется при сбое БД, отказы Telegram расходуют бюджет #3459
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3459
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/tg-support-db-and-ratelimit"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Два последних дефекта из разбора телеграм-стека, оба в ручках веб-поддержки. Предыдущие три PR (#3456, #3457, #3458) чинили клиент и мост — эти сами ручки. Закрывает скоуп «пофикси телеграм» целиком, кроме вебхука GlitchTip, исключённого пользователем явно.
1. Сбой БД уже ПОСЛЕ доставки в топик
Порядок «сначала Telegram, потом БД» осознанный, но блок записи не был обёрнут ничем, в отличие от шага отправки.
SQLAlchemyErrorтам означал: сообщение оператору доставлено, а клиент получил 500. Дальше по цепочке — пользователь шлёт повторно, в топике дубль, а на осиротевшее зеркало оператор отвечает в пустоту: треда в БД нет, и мост на реплай пишет только WARNING.Обе ручки теперь ловят
SQLAlchemyErrorвокруг блока БД, тихо откатывают сессию, предупреждают оператора реплаем к доставленному зеркалу и отдают клиенту успех. Успех, а не отказ: доставка правда состоялась, и отказ спровоцировал бы ровно тот дубль, которого избегаем.Анонимная ветка на этом пути дополнительно ставит куку, хотя штатно ставит её только на успехе: треда нет, но идентичность посетителя обязана пережить сбой, иначе следующее сообщение заведёт второй тред.
2. Успеха мало — клиент должен об этом узнать
Первая версия правки отдавала успех молча, и это было неотличимо от тишины. Нашло adversarial-ревью, подтверждено чтением фронта:
useSupportChat.ts:115-126выбрасывает тело POST,SupportChatPanel.tsx:163чистит только черновик, транскрипт рендерится исключительно изmessagesQuery, а баннер показывает лишь ошибки. То есть поле ввода очищается, в списке пусто, предупреждения нет — пользователь шлёт снова, и получается тот самый дубль.SupportMessageOutполучил полеpersistedсо значениемTrueпо умолчанию: все существующие пути иGET /support/messagesотдают его без изменений. На пути деградации приходитFalse, и панель показывает рядом с композером предупреждение:Баннер гаснет на следующей нормально записанной отправке. Анонимный виджет рендерит ту же панель и получает поведение автоматически.
Текст предупреждения оператору тоже переписан — он больше не рассчитывает на то, что клиент напишет снова, и прямо говорит, что ответить через бота не получится.
3. Рейт-лимит переставал считаться при недоступном Telegram
retry_after()— это peek, аrecord()звался только на успехе. Верно для «не наказывать за чужую аварию», но имеет обратную сторону: пока Telegram лежит, лимита нет вообще, и каждый повтор стоит до четырёх попыток кapi.telegram.org, не расходуя ни один бюджет. Двух-трёх вкладок с авто-повтором хватает, чтобы выесть лимиты группы ровно тогда, когда канал и так еле жив.Добавлен отдельный счётчик отказов на тех же ключах: 5 подряд в окне 30с включают cooldown, и ручка отвечает 429 не доходя до Telegram. Пять подряд на живом канале практически недостижимы, а
reset()на успехе стирает историю — считаем именно подряд. Тридцать секунд заведомо короче реальной недоступности, так что после восстановления пользователя не наказывают. Основной «успешный» бюджет и non-destructive peek не тронуты.SlidingWindowLimiter.reset(key)добавлен аддитивно, с оговоркой в докстринге, что лимитерам-бюджетам он противопоказан.Осознанное ограничение: барьер рассчитан на несколько вкладок с авто-повтором, а не на одиночного последовательного клиента — один отказавший запрос сам занимает до 23с, и пять таких в окно не укладываются. Ловить одиночку значило бы наказывать обычного пользователя за чужую аварию. Это не регресс: раньше барьера не было вовсе.
Тесты
Отказ БД в обеих ручках: клиент получает успех с
persisted=False, оператору уходит предупреждение, текст обращения в него не попадает, 500 не возникает. Отказ самого уведомления ручку не роняет. Серия отказов включает cooldown, и до Telegram запрос не доходит. Окончание окна cooldown снимает. Успешный путь и существующий рейт-лимит не изменились.Бэкенд: 117 passed,
ruff checkиruff format --checkчистые. Фронт:tsc --noEmitчистый,next lintбез новых замечаний (два предупреждения — предсуществующие, в чужих файлах).🤖 Generated with Claude Code
https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs