Ответ оператора не исчезает при сбое БД, и реплай на собственный ответ снова маршрутизируется #3479

Merged
lekss361 merged 2 commits from fix/3471-bridge-db-failure-reply-loss into main 2026-09-12 11:26:30 +00:00
Owner

Пункт P0 из #3471. Дефект, который PR #3456—#3459 не закрыли: они чинили HTTP-клиент, транспорт и ручки API, а эта ветка живёт в мосте.

Что было

В process_update ветка except SQLAlchemyError делала rollback(), после чего безусловно выполнялись save_offset и commit. Для веб-треда запись record_web_out_message — это и есть доставка клиенту: веб-чат рисует переписку из базы. Откат записи вместе со сдвинутым offset означал, что Telegram этот апдейт больше не отдаст, ответ оператора не попадёт никуда, оператор будет уверен, что ответил, а клиент останется ждать.

Случай уже наблюдался на проде 31.08.2026. Обращение оказалось тестовым, так что настоящий клиент не пострадал, — но только по везению.

Что сделано

Ответ не теряется молча. Веб-ветка в _handle_group_reply обёрнута в try/except SQLAlchemyError. На сбое: откат, затем реплай оператору в топик с прямым текстом «ответ НЕ доставлен, отправьте ещё раз». Offset после этого двигается осознанно: апдейт уже частично применён на стороне Telegram, переигрывать его нельзя. Образец взят из соседней обработки 403 «бот заблокирован».

Уведомление не может уронить поток. _notify_topic теперь возвращает признак успеха, её собственный перехват по-прежнему не выпускает исключение наружу. Если и уведомление не ушло, остаётся logger.error с идентификаторами треда и сообщения — по ним человек найдёт ответ в топике. Текст переписки в лог не пишется, это персональные данные.

Реплай на свой же ответ снова работает. У строк с direction='out' поле topic_message_id всегда было пустым, поэтому оператор, отвечавший реплаем на собственное предыдущее сообщение, говорил в пустоту. Теперь идентификатор сообщения оператора сохраняется и для веб-, и для TG-пути, а поиск по зеркалу больше не фильтрует по направлению.

Миграция не потребовалась: колонка и частичный уникальный индекс уже существуют (186 и 187). Коллизий нет, Telegram выдаёт разным сообщениям в одном чате разные идентификаторы, и индекс это же гарантирует на уровне базы.

Тесты

pytest tests/services/tgbot/test_bridge.py -q — 41 passed. Новые случаи: сбой базы на веб-ответе с успешным уведомлением; сбой базы плюс провал уведомления; реплай на свой предыдущий веб-ответ; то же для TG-пути.

Хвост

COMMENT ON COLUMN в data/sql/187_web_support_chat.sql всё ещё говорит «только для direction='in'». На поведение не влияет, DDL не нужен, поправлю отдельным PR без кода.

Пункт P0 из #3471. Дефект, который PR #3456—#3459 не закрыли: они чинили HTTP-клиент, транспорт и ручки API, а эта ветка живёт в мосте. ## Что было В `process_update` ветка `except SQLAlchemyError` делала `rollback()`, после чего безусловно выполнялись `save_offset` и `commit`. Для веб-треда запись `record_web_out_message` — это и есть доставка клиенту: веб-чат рисует переписку из базы. Откат записи вместе со сдвинутым offset означал, что Telegram этот апдейт больше не отдаст, ответ оператора не попадёт никуда, оператор будет уверен, что ответил, а клиент останется ждать. Случай уже наблюдался на проде 31.08.2026. Обращение оказалось тестовым, так что настоящий клиент не пострадал, — но только по везению. ## Что сделано **Ответ не теряется молча.** Веб-ветка в `_handle_group_reply` обёрнута в `try/except SQLAlchemyError`. На сбое: откат, затем реплай оператору в топик с прямым текстом «ответ НЕ доставлен, отправьте ещё раз». Offset после этого двигается осознанно: апдейт уже частично применён на стороне Telegram, переигрывать его нельзя. Образец взят из соседней обработки 403 «бот заблокирован». **Уведомление не может уронить поток.** `_notify_topic` теперь возвращает признак успеха, её собственный перехват по-прежнему не выпускает исключение наружу. Если и уведомление не ушло, остаётся `logger.error` с идентификаторами треда и сообщения — по ним человек найдёт ответ в топике. Текст переписки в лог не пишется, это персональные данные. **Реплай на свой же ответ снова работает.** У строк с `direction='out'` поле `topic_message_id` всегда было пустым, поэтому оператор, отвечавший реплаем на собственное предыдущее сообщение, говорил в пустоту. Теперь идентификатор сообщения оператора сохраняется и для веб-, и для TG-пути, а поиск по зеркалу больше не фильтрует по направлению. Миграция не потребовалась: колонка и частичный уникальный индекс уже существуют (186 и 187). Коллизий нет, Telegram выдаёт разным сообщениям в одном чате разные идентификаторы, и индекс это же гарантирует на уровне базы. ## Тесты `pytest tests/services/tgbot/test_bridge.py -q` — 41 passed. Новые случаи: сбой базы на веб-ответе с успешным уведомлением; сбой базы плюс провал уведомления; реплай на свой предыдущий веб-ответ; то же для TG-пути. ## Хвост `COMMENT ON COLUMN` в `data/sql/187_web_support_chat.sql` всё ещё говорит «только для direction='in'». На поведение не влияет, DDL не нужен, поправлю отдельным PR без кода.
lekss361 added 1 commit 2026-09-12 11:00:45 +00:00
fix(tg): ответ оператора на веб-чат не теряется молча при сбое БД
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 14s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m56s
99f123e646
Для веб-треда запись в web_support_messages(direction='out') И ЕСТЬ доставка
клиенту (веб-фронт читает её polling'ом). process_update на SQLAlchemyError
безусловно делал rollback() и всё равно сдвигал offset — Telegram апдейт
больше не отдавал, ответ оператора пропадал навсегда, а сам оператор был
уверен, что ответил. Воспроизведено на проде 31.08.2026 (клиент kopylov).

- `_handle_group_reply`: сбой БД на `record_web_out_message` теперь ловится
  локально — rollback → уведомление оператору реплаем в топик, что ответ НЕ
  доставлен и его нужно повторить; offset всё равно сдвигается (апдейт уже
  частично применён в Telegram, переигрывать нельзя).
- `_notify_topic` возвращает bool: если само уведомление тоже упало (Telegram
  недоступен), пишем `logger.error` с thread_id/message_id (без текста
  переписки — ПДн в лог не идёт), чтобы это не осталось полностью немым.
- Второй дефект того же узла: `direction='out'`-строки никогда не сохраняли
  topic_message_id, из-за чего реплай оператора на СВОЙ предыдущий ответ не
  резолвился (маршрут держался только на зеркале клиента). Теперь TG- и
  веб-путь сохраняют id ответа оператора в топике, `find_chat_by_topic_message`
  / `find_thread_by_topic_message` больше не фильтруют по direction. Колонка и
  partial unique индекс уже существовали (186/187) — миграция не потребовалась.

Refs #3471
bot-backend added 1 commit 2026-09-12 11:16:09 +00:00
fix(tg): out-строки писались с support_chat_id=NULL — вечный wildcard-матч
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 16s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 6m17s
5e80b56bdc
Deep review PR #3479 нашёл дефект в предыдущем фиксе (#3471 пункт 3): новые
direction='out' строки стали видимы резолверам (find_chat_by_topic_message,
find_thread_by_topic_message), но писались без support_chat_id. Резолверы
матчат support_chat_id IS NULL как лениентный wildcard "любой текущий чат"
(легаси-строки до 187/188) — то есть КАЖДАЯ out-строка становилась таким
wildcard. При ротации support-группы новый message_id мог бы случайно
совпасть со старой out-строкой: TG-путь увёл бы ответ ЧУЖОМУ клиенту через
copyMessage, веб-путь записал бы ответ в чужой тред. Ровно от этого
защищали миграции 187/188 (review M1).

- bridge.py: TG- и веб-ветка `_handle_group_reply` теперь передают
  support_chat_id=settings.telegram_support_chat_id в record_message /
  record_web_out_message (симметрично уже существующей in-ветке).
- web_support_storage.record_outbound: добавлен параметр support_chat_id,
  пишется в INSERT (колонка уже существовала, DDL не нужен).
- Тест test_group_reply_to_own_previous_tg_reply_resolves_target_chat сидел
  предыдущую out-строку с уже заполненным support_chat_id вручную, хотя код
  писал NULL — маскировал дефект. Добавлены прямые проверки на записанное
  support_chat_id (TG и веб), обе падают на прежней реализации (проверено
  локальным откатом изменения — 2 failed, restore — 41 passed).
- Комментарий про "апдейт частично применён в Telegram" в except-ветке
  веб-ответа был неверен для этого случая (на веб-пути ничего не уходит в
  Telegram до сбоя БД) — переписан на настоящую причину: сбой БД не
  переигрывается по общей политике process_update, а не из-за частичной
  доставки.

Refs #3471
lekss361 merged commit 2edaae148f into main 2026-09-12 11:26:30 +00:00
lekss361 deleted branch fix/3471-bridge-db-failure-reply-loss 2026-09-12 11:26:30 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3479
No description provided.