fix(tradein/support): один повтор терял каждое одиннадцатое сообщение в поддержку #3309

Merged
lekss361 merged 1 commit from fix/tgsupport-retry-budget into main 2026-09-01 07:00:21 +00:00
Owner

Что не так

Ручка веб-поддержки ходила в Telegram с max_retries=1, то есть двумя попытками. Канал до api.telegram.org с прод-хоста рвётся постоянно, и при измеренной доле отказов до пользователя доходило порядка 9% отказов — каждое одиннадцатое сообщение возвращало 502 «сервис недоступен».

Замер, на котором это стоит

Всё измерено 01.09.2026 из контейнера tradein-tgbot, на живом проде.

Канал рвётся, и это не наш клиент. Пять проб подряд, доля отказов на попытку: 3/8, 15/40, 5/20, 3/20, 1/25 — то есть 15-38% всплесками. В логе long-polling'а за сутки 353 ConnectTimeout. Транспорт ни при чём: в чередующемся замере httpx (чем ходит бот) дал 25% отказов, сырой urllib — 35%.

Прокси не решение — проверено, а не предположено. Через SCRAPER_PROXY_URL к api.telegram.org: 0 из 20. Узел скрейпинга туда просто не пускает, так что напрашивавшийся ход отброшен по замеру.

Два числа задают всю конструкцию правки:

успешный запрос 0.13с, максимум из 25 проб — 0.18с
неудачный запрос всегда упирается в таймаут целиком (10.02с при timeout=10.0), быстрых отказов — ноль

Отсюда следует, что десятисекундный таймаут не покупал ничего, кроме цены за неудачу. И что экспоненциальная пауза 2→4→8с здесь бессмысленна: отказ — это неустановленное соединение, а не троттлинг, удалённой стороне нечего «остывать»; пауза лишь добавляла 14 секунд к ожиданию пользователя.

Правка

Бюджет интерактивной отправки: 3 повтора, таймаут 5с, потолок паузы 1с.

  • таймаут 5с — ~28-кратный запас к измеренному максимуму успешного ответа;
  • худший случай: 4 попытки × 5с + 3 паузы × 1с = 23с, и он требует четырёх отказов подряд;
  • типичный случай не меняется — 0.13с;
  • расчётная потеря падает с ~9% до ~0.8%.

В TelegramClient добавлен необязательный max_backoff.

Что здесь легко сломать

Воркерная политика ретраев обязана остаться прежней. Без явного потолка откат прежний экспоненциальный до 30с, а retry_after из тела 429 уважается целиком — иначе мы долбимся в лимит и Telegram затягивает его жёстче.

Эту границу держит отдельный тест, и он не декоративный: первая версия правки её сломала. Потолок применялся к retry_after безусловно, и воркер начинал спать 30с там, где Telegram просил 60. Тест поймал это до коммита. Теперь потолок на retry_after применяется только когда его передали явно — интерактивному пути ждать 30-60с нельзя ни при каких обстоятельствах, за ним стоит открытый HTTP-запрос от браузера.

Тесты

8 новых (tests/test_tgsupport_retry_budget.py): потолок на network/429/5xx, неизменность воркерного пути в обеих ветках, арифметика «max_retries=N → N+1 попыток», границы бюджета ручки.

Локально: 43 passed по test_tgsupport_retry_budget.py + test_support.py, 105 passed по всему срезу -k "tgbot or telegram or support or bridge", ruff чист.

Границы

Не трогает: приём (long-polling), маршрутизацию ответов операторов, схему, анонимную ветку по существу (она получает тот же бюджет, что и авторизованная). Сам разрыв канала до Telegram эта правка не чинит — она делает его переживаемым. Настоящее решение — отдельный egress до Telegram, но какой именно, надо подбирать замером; в этот PR не входит.

## Что не так Ручка веб-поддержки ходила в Telegram с `max_retries=1`, то есть **двумя попытками**. Канал до `api.telegram.org` с прод-хоста рвётся постоянно, и при измеренной доле отказов до пользователя доходило порядка **9% отказов** — каждое одиннадцатое сообщение возвращало 502 «сервис недоступен». ## Замер, на котором это стоит Всё измерено 01.09.2026 из контейнера `tradein-tgbot`, на живом проде. **Канал рвётся, и это не наш клиент.** Пять проб подряд, доля отказов на попытку: 3/8, 15/40, 5/20, 3/20, 1/25 — то есть 15-38% всплесками. В логе long-polling'а за сутки **353 `ConnectTimeout`**. Транспорт ни при чём: в чередующемся замере `httpx` (чем ходит бот) дал 25% отказов, сырой `urllib` — 35%. **Прокси не решение — проверено, а не предположено.** Через `SCRAPER_PROXY_URL` к `api.telegram.org`: **0 из 20**. Узел скрейпинга туда просто не пускает, так что напрашивавшийся ход отброшен по замеру. **Два числа задают всю конструкцию правки:** | | | |---|---| | успешный запрос | **0.13с**, максимум из 25 проб — 0.18с | | неудачный запрос | **всегда упирается в таймаут целиком** (10.02с при `timeout=10.0`), быстрых отказов — ноль | Отсюда следует, что десятисекундный таймаут не покупал ничего, кроме цены за неудачу. И что экспоненциальная пауза 2→4→8с здесь бессмысленна: отказ — это неустановленное соединение, а не троттлинг, удалённой стороне нечего «остывать»; пауза лишь добавляла 14 секунд к ожиданию пользователя. ## Правка Бюджет интерактивной отправки: **3 повтора, таймаут 5с, потолок паузы 1с**. - таймаут 5с — ~28-кратный запас к измеренному максимуму успешного ответа; - худший случай: 4 попытки × 5с + 3 паузы × 1с = **23с**, и он требует четырёх отказов подряд; - типичный случай не меняется — 0.13с; - расчётная потеря падает с ~9% до **~0.8%**. В `TelegramClient` добавлен необязательный `max_backoff`. ## Что здесь легко сломать **Воркерная политика ретраев обязана остаться прежней.** Без явного потолка откат прежний экспоненциальный до 30с, а `retry_after` из тела 429 уважается целиком — иначе мы долбимся в лимит и Telegram затягивает его жёстче. Эту границу держит отдельный тест, и он не декоративный: **первая версия правки её сломала.** Потолок применялся к `retry_after` безусловно, и воркер начинал спать 30с там, где Telegram просил 60. Тест поймал это до коммита. Теперь потолок на `retry_after` применяется только когда его передали явно — интерактивному пути ждать 30-60с нельзя ни при каких обстоятельствах, за ним стоит открытый HTTP-запрос от браузера. ## Тесты 8 новых (`tests/test_tgsupport_retry_budget.py`): потолок на network/429/5xx, неизменность воркерного пути в обеих ветках, арифметика «`max_retries=N` → N+1 попыток», границы бюджета ручки. Локально: 43 passed по `test_tgsupport_retry_budget.py` + `test_support.py`, 105 passed по всему срезу `-k "tgbot or telegram or support or bridge"`, ruff чист. ## Границы Не трогает: приём (long-polling), маршрутизацию ответов операторов, схему, анонимную ветку по существу (она получает тот же бюджет, что и авторизованная). Сам разрыв канала до Telegram эта правка не чинит — она делает его переживаемым. Настоящее решение — отдельный egress до Telegram, но какой именно, надо подбирать замером; в этот PR не входит.
lekss361 added 1 commit 2026-09-01 06:54:38 +00:00
fix(tradein/support): один повтор терял каждое одиннадцатое сообщение в поддержку
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 11s
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 4m50s
d708f15019
Замер прода 01.09.2026 из контейнера бота: канал до api.telegram.org рвётся
всплесками, доля отказов на попытку 15-38% (пять проб: 3/8, 15/40, 5/20, 3/20,
1/25), в логе long-polling'а 353 ConnectTimeout за сутки. Транспорт ни при чём —
httpx и сырой сокет отваливаются одинаково (25% против 35% в чередующемся
замере), и прокси не помогает, а мешает: через SCRAPER_PROXY_URL 0 из 20.

Ручка веб-поддержки ходила с max_retries=1, то есть двумя попытками. При 30%
отказов на попытку до пользователя доходило ~9% отказов — каждое одиннадцатое
сообщение возвращало 502 «сервис недоступен».

Два других числа из того же замера задают конструкцию. Успешный запрос отвечает
за 0.13с (максимум из 25 проб — 0.18с), а неудачный НИКОГДА не отваливается
быстро: все отказы упираются в таймаут целиком (10.02с при timeout=10.0). Значит
десятисекундный таймаут не покупал ничего, кроме цены за неудачу, — снижен до 5с,
это ~28-кратный запас к измеренному максимуму. И экспоненциальная пауза 2→4→8с
здесь бессмысленна: отказ — неустановленное соединение, а не троттлинг, пережидать
нечего; она лишь добавляла 14с к ожиданию.

Правка: бюджет ручки — 3 повтора, таймаут 5с, потолок паузы 1с. Худший случай
4 попытки × 5с + 3 паузы × 1с = 23с и требует четырёх отказов подряд; типичный
случай не меняется (0.13с). Расчётная потеря падает с ~9% до ~0.8%.

В TelegramClient добавлен необязательный max_backoff. Воркерная политика НЕ
меняется: без явного потолка откат прежний экспоненциальный до 30с, а retry_after
из 429 уважается целиком — эту границу держит отдельный тест, потому что первая
версия правки её сломала (капала 60с до 30с и для воркера тоже). Потолок на
retry_after применяется только когда его передали явно: интерактивному пути
нельзя ждать Telegram-овские 30-60с, за ним стоит открытый запрос от браузера.

Тесты: 8 новых (потолок на network/429/5xx, неизменность воркерного пути,
арифметика «max_retries=N → N+1 попыток», границы бюджета ручки).
lekss361 merged commit 879fe57e35 into main 2026-09-01 07:00:21 +00:00
lekss361 deleted branch fix/tgsupport-retry-budget 2026-09-01 07:00:21 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#3309
No description provided.