fix(ops): алерт не теряется от одного сетевого отказа — ретрай в обеих notify() (#3059) #3095

Merged
bot-backend merged 1 commit from fix/3059-alert-retry into main 2026-08-26 07:30:06 +00:00
Owner

Что не так

Путь Selectel→Telegram теряет соединения. Замер 26.08 с Poincare, 40 подключений к закреплённому (#3093) 149.154.167.220:

успешно 37 из 40, отказов 3 (7.5%) — все TimeoutError
время успешных: min 0.14s  медиана 0.15s  max 0.17s

Отказы происходят на стадии подключения — быстрые стабильные успехи на фоне редких таймаутов, а не деградация под нагрузкой. Три остальных дата-центра Telegram с Selectel недостижимы вовсе (таймаут 8 с), поэтому закрепление адреса из #3093 потери убрать не может: запасного адреса просто нет.

Бот это переживает своими ретраями — 106 таймаутов getUpdates за сутки, из них 97 лечатся первой же повторной попыткой. Алерты — нет:

файл было последствие
ops/uptime-healthcheck.sh:61 один curl, дальше || log WARN уведомление терялось целиком
ops/lib-backup.sh:48 один curl → сразу почта (#3070) каждый транзиентный таймаут впустую сжигал последнее средство

Сторож, который не может дозваться, — худший вид самоскрывающейся поломки: чем хуже дела на проде, тем выше шанс, что о них не сообщат. Отдельная ирония — ниже в том же uptime-healthcheck.sh HTTP-проверки уже повторяются циклом for attempt in $(seq 1 ...): ретраили то, что измеряют, но не то, чем докладывают.

Что стало

Цикл из трёх попыток в обеих notify().

Почему цикл, а не curl --retry: семантика --max-time при ретраях зависит от версии curl, а цикл гарантирует таймаут на КАЖДУЮ попытку и повторяет идиому, уже принятую в этом файле.

Дубль вместо потери — осознанный размен. sendMessage не идемпотентен, но замер показал, что отказ случается ДО отправки запроса, так что повтор почти никогда не продублирует доставленное. Лишний алерт безвреден, пропущенный — нет.

Верхняя граница не менее важна нижней: ровно три попытки, hourly-крон не зависает на недоступном Telegram. Задержка между попытками — NOTIFY_RETRY_DELAY (по умолчанию 2 с), чтобы тесты не спали.

Тесты

backend/tests/ops/test_3059_alert_retry.py, 7 штук. Исполняют РЕАЛЬНЫЕ notify(), извлечённые из обоих скриптов (не копию, не пересказ), с подставным curl, который отказывает заданное число раз кодом 28 (operation timeout — ровно то, что наблюдалось), и считают ФАКТИЧЕСКОЕ число вызовов:

  1. один отказ → доставлено со второй попытки, curl позван 2 раза (оба скрипта);
  2. все отказы → ровно 3 попытки, не больше (оба скрипта);
  3. недоставленный алерт логируется громко;
  4. один таймаут не трогает почтовый фолбэк;
  5. три отказа подряд → почта уходит, ровно один раз.

Фальсификация: на исходных скриптах краснеют 6 из 7. Проходит только test_backup_falls_back_to_mail_when_telegram_is_really_down — он фиксирует сохранённое поведение, а не регресс, и это честно отмечено в его докстринге.

Проверено помимо тестов

  • bash -n обоих скриптов — та же проверка, что в CI (ci.yml:125: shellcheck там нет намеренно);
  • конструкция [[ ... ]] && cmd в конце тела цикла безопасна под set -euo pipefail (стоит в uptime-healthcheck.sh:25) — проверено исполнением, а не рассуждением о POSIX;
  • в staged-блобах нет CR: правил под Windows, а для shell-скрипта CRLF смертелен;
  • tests/ops целиком — 22 passed;
  • правка доедет до прода: deploy.yml матчит ops/*.sh глобом (#2203) и делает git reset --hard.

Что этот PR НЕ чинит

Сами потери. Это свойство сетевого пути Selectel↔Telegram, кодом не лечится — варианты (прокси-пул / бот на Beget / запрос в поддержку) остаются в #3059 за владельцем. Здесь только то, что алерт переживает единичный отказ.

Отдельная находка — сторож, похоже, нигде не запущен

Проверил оба VPS: файл /opt/gendesign/ops/uptime-healthcheck.sh на месте, но ни cron, ни systemd-таймера, ни лога /var/log/gendesign-uptime.log — ни на Beget, ни на Poincare, ни под root, ни под gendesign.

Утверждать «не работает нигде» не могу: по замыслу (#75 B6-1, шапка скрипта) он и должен крутиться на ВНЕШНЕЙ машине — ноутбуке, free-tier боксе или cron'е shared-хостинга Beget, куда у меня доступа нет. Но если он всё же живёт на внешнем хосте, туда эта правка сама не доедет: ops/*.sh копируется только на прод-VM. Стоит проверить и обновить копию вручную.

Refs #3059, #3093

## Что не так Путь Selectel→Telegram теряет соединения. Замер 26.08 с Poincare, 40 подключений к **закреплённому** (#3093) `149.154.167.220`: ``` успешно 37 из 40, отказов 3 (7.5%) — все TimeoutError время успешных: min 0.14s медиана 0.15s max 0.17s ``` Отказы происходят на **стадии подключения** — быстрые стабильные успехи на фоне редких таймаутов, а не деградация под нагрузкой. Три остальных дата-центра Telegram с Selectel недостижимы вовсе (таймаут 8 с), поэтому закрепление адреса из #3093 потери убрать не может: запасного адреса просто нет. Бот это переживает своими ретраями — 106 таймаутов `getUpdates` за сутки, из них 97 лечатся первой же повторной попыткой. **Алерты — нет:** | файл | было | последствие | |---|---|---| | `ops/uptime-healthcheck.sh:61` | один `curl`, дальше `\|\| log WARN` | уведомление терялось целиком | | `ops/lib-backup.sh:48` | один `curl` → сразу почта (#3070) | каждый транзиентный таймаут впустую сжигал последнее средство | Сторож, который не может дозваться, — худший вид самоскрывающейся поломки: чем хуже дела на проде, тем выше шанс, что о них не сообщат. Отдельная ирония — ниже в том же `uptime-healthcheck.sh` HTTP-**проверки** уже повторяются циклом `for attempt in $(seq 1 ...)`: ретраили то, что измеряют, но не то, чем докладывают. ## Что стало Цикл из трёх попыток в обеих `notify()`. **Почему цикл, а не `curl --retry`:** семантика `--max-time` при ретраях зависит от версии curl, а цикл гарантирует таймаут на КАЖДУЮ попытку и повторяет идиому, уже принятую в этом файле. **Дубль вместо потери — осознанный размен.** `sendMessage` не идемпотентен, но замер показал, что отказ случается ДО отправки запроса, так что повтор почти никогда не продублирует доставленное. Лишний алерт безвреден, пропущенный — нет. Верхняя граница не менее важна нижней: ровно три попытки, hourly-крон не зависает на недоступном Telegram. Задержка между попытками — `NOTIFY_RETRY_DELAY` (по умолчанию 2 с), чтобы тесты не спали. ## Тесты `backend/tests/ops/test_3059_alert_retry.py`, 7 штук. Исполняют **РЕАЛЬНЫЕ** `notify()`, извлечённые из обоих скриптов (не копию, не пересказ), с подставным `curl`, который отказывает заданное число раз кодом 28 (`operation timeout` — ровно то, что наблюдалось), и считают ФАКТИЧЕСКОЕ число вызовов: 1. один отказ → доставлено со второй попытки, `curl` позван 2 раза (оба скрипта); 2. все отказы → ровно 3 попытки, не больше (оба скрипта); 3. недоставленный алерт логируется громко; 4. один таймаут **не** трогает почтовый фолбэк; 5. три отказа подряд → почта уходит, ровно один раз. **Фальсификация:** на исходных скриптах краснеют **6 из 7**. Проходит только `test_backup_falls_back_to_mail_when_telegram_is_really_down` — он фиксирует сохранённое поведение, а не регресс, и это честно отмечено в его докстринге. ## Проверено помимо тестов - `bash -n` обоих скриптов — та же проверка, что в CI (`ci.yml:125`: shellcheck там нет намеренно); - конструкция `[[ ... ]] && cmd` в конце тела цикла безопасна под `set -euo pipefail` (стоит в `uptime-healthcheck.sh:25`) — проверено **исполнением**, а не рассуждением о POSIX; - в staged-блобах нет CR: правил под Windows, а для shell-скрипта CRLF смертелен; - `tests/ops` целиком — 22 passed; - правка доедет до прода: `deploy.yml` матчит `ops/*.sh` глобом (#2203) и делает `git reset --hard`. ## Что этот PR НЕ чинит **Сами потери.** Это свойство сетевого пути Selectel↔Telegram, кодом не лечится — варианты (прокси-пул / бот на Beget / запрос в поддержку) остаются в #3059 за владельцем. Здесь только то, что алерт переживает единичный отказ. ## Отдельная находка — сторож, похоже, нигде не запущен Проверил оба VPS: файл `/opt/gendesign/ops/uptime-healthcheck.sh` на месте, но **ни cron, ни systemd-таймера, ни лога** `/var/log/gendesign-uptime.log` — ни на Beget, ни на Poincare, ни под `root`, ни под `gendesign`. Утверждать «не работает нигде» не могу: по замыслу (`#75 B6-1`, шапка скрипта) он и должен крутиться на ВНЕШНЕЙ машине — ноутбуке, free-tier боксе или cron'е shared-хостинга Beget, куда у меня доступа нет. Но если он всё же живёт на внешнем хосте, **туда эта правка сама не доедет**: `ops/*.sh` копируется только на прод-VM. Стоит проверить и обновить копию вручную. Refs #3059, #3093
lekss361 added 1 commit 2026-08-26 07:04:08 +00:00
fix(ops): алерт не теряется от одного сетевого отказа (#3059)
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m48s
CI / backend-tests (pull_request) Successful in 22m26s
c7df2732bd
Путь Selectel-Telegram теряет соединения. Замер 26.08 с Poincare, 40
подключений к закреплённому (#3093) 149.154.167.220:

    успешно 37 из 40, отказов 3 (7.5%) - все TimeoutError
    время успешных: min 0.14s, медиана 0.15s, max 0.17s

Отказы происходят на стадии ПОДКЛЮЧЕНИЯ - быстрые стабильные успехи на фоне
редких таймаутов. Три остальных дата-центра Telegram с Selectel недостижимы
вовсе, так что закрепление адреса потери убрать не может: запасного адреса
нет. Бот это переживает своими ретраями (106 таймаутов за сутки, 97 лечатся
первой же повторной попыткой), а вот алерты - нет.

uptime-healthcheck.sh: был один curl и `|| log WARN` - каждый отказ терял
уведомление целиком. Сторож, который не может дозваться, - худший вид
самоскрывающейся поломки: чем хуже дела на проде, тем выше шанс, что о них
не сообщат. Ирония в том, что ниже в этом же файле HTTP-проверки уже
повторяются циклом: ретраили то, что измеряют, но не то, чем докладывают.

lib-backup.sh: тоже один curl, но с падением в почту (#3070). Алерт не
терялся, зато каждый транзиентный таймаут впустую сжигал последнее средство
вместо простого переподключения.

Стало: цикл из трёх попыток в обеих notify(). Не `curl --retry` - семантика
--max-time при ретраях зависит от версии curl, а цикл даёт таймаут на КАЖДУЮ
попытку и повторяет идиому, уже принятую в uptime-healthcheck.sh.

Дубль вместо потери - осознанный размен: sendMessage не идемпотентен, но
отказ случается ДО отправки запроса, так что повтор почти никогда не
продублирует доставленное. Лишний алерт безвреден, пропущенный - нет.

Тесты (backend/tests/ops/test_3059_alert_retry.py, 7 шт) исполняют РЕАЛЬНЫЕ
notify(), извлечённые из обоих скриптов, с подставным curl, отказывающим
заданное число раз, и считают фактическое число попыток.

Фальсификация: на исходных скриптах краснеют 6 из 7. Проходит только
test_backup_falls_back_to_mail_when_telegram_is_really_down - он фиксирует
сохранённое поведение, а не регресс.

Проверено: `bash -n` обоих скриптов (та же проверка, что в CI - shellcheck
там нет); конструкция `[[ ]] && cmd` в конце тела цикла безопасна под
`set -euo pipefail`, который стоит в uptime-healthcheck.sh:25 (проверено
исполнением, не рассуждением); tests/ops целиком - 22 passed.
bot-backend merged commit c0fcf6a78f into main 2026-08-26 07:30:06 +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#3095
No description provided.