feat(stability): стадия 6 — лёгкий /ping healthcheck без TG RPC #16

Merged
agent_vscode merged 5 commits from fix/stage-6-healthcheck into fix/stage-5-sqlite 2026-07-05 16:58:18 +03:00
Collaborator

Summary

Стадия 6: лёгкий /ping healthcheck, который не трогает Telegram. closes #6.

Текущий healthcheck бьёт /rss/...?limit=1 (timeout 5 с): на холодном кэше генерация RSS легко > 5 с, а зависший TG RPC вообще подвешивает эндпоинт → docker/autoheal рестартит контейнер посреди докачек → битые temp-файлы. Заменяем на /ping, который отражает живость процесса/лупа (отвечает мгновенно, всегда) плюс живость TG — из данных последнего probe вотчдога, без единого TG RPC.

Что сделано:

  • telegram_client.watchdog_last_ok_age() — секунды с последнего успешного probe вотчдога (None, если ещё не было). Чистое чтение поля _wd_last_ok_monotonic из стадии 1; никаких RPC.
  • api_server /ping — без токена, без TG RPC, без SQLite, без обхода ФС. healthy = connected and (age is None or age < threshold). age is None сразу после старта ⇒ healthy (не убиваем контейнер до первого probe). connected приведён к bool, поэтому поле connected в JSON всегда bool (в окне до start()false, не null).
  • config: knob TG_PING_UNHEALTHY_AFTER, дефолт interval*(failures+1)+timeout = 250 с (сколько вотчдог сам терпит до рестарта), env-override.
  • dockercompose.yml: healthcheck → curl -sf http://127.0.0.1:80/ping, interval 5m, timeout 5s, retries 3, start_period 30s. Старый /rss-чек убран, не оставлен вторым.

DoD: при зависшем TG RPC /ping отвечает мгновенно (503 degraded), контейнер не рестартится от медленного фида.

How verified

На стенде, чистый venv: python -m pytest tests/251 passed (241 базовых стадий 1–5 + 10 новых).

Ключевой инвариант — ноль TG RPC в /ping — закреплён anti-regression тестом test_ping_issues_no_tg_rpc: фейки get_me/safe_get_messages бросают AssertionError при любом вызове, счётчики == 0 по всем 4 ветвям (healthy/degraded/disconnected/fresh). Реинтродукция любого RPC в /ping → тест падает.

Adversarial-валидация bool-приведения: откатил bool(...) → тест pre-start (is_connected=None) краснеет (в JSON был бы null вместо false); вернул → зелено.

10 тестов: healthy (recent), degraded (stale probe), degraded (disconnected, с age и age=None), fresh-boot (age None → 200), pre-start (is_connected=None → 503, connected: false bool, не 500), no-token, zero-TG-RPC spy, аксессор (None до probe / положительный float после).

Что для человека

  • Прод docker-compose лежит вне репозитория — переключить healthcheck на /ping руками при деплое. In-repo dockercompose.yml — только пример. Сам деплой — стадия 7 + твоя команда.
  • Нюанс: при TG_WATCHDOG_ENABLED=false age навсегда None и /ping вырождается в чистую проверку connected (staleness «зомби-сессии» не детектит) — отмечено в комментарии у хендлера.

База и стек

PR основан на fix/stage-5-sqlite (#15, approved). Ретаргет на fix/stability по мере мержа цепочки.

Checklist

  • критерии приёмки из #6 выполнены
  • вне заявленного scope ничего не менялось
## Summary Стадия 6: лёгкий `/ping` healthcheck, который **не трогает Telegram**. closes #6. Текущий healthcheck бьёт `/rss/...?limit=1` (timeout 5 с): на холодном кэше генерация RSS легко > 5 с, а зависший TG RPC вообще подвешивает эндпоинт → docker/autoheal рестартит контейнер посреди докачек → битые temp-файлы. Заменяем на `/ping`, который отражает живость процесса/лупа (отвечает мгновенно, всегда) плюс живость TG — **из данных последнего probe вотчдога**, без единого TG RPC. **Что сделано:** - `telegram_client.watchdog_last_ok_age()` — секунды с последнего успешного probe вотчдога (None, если ещё не было). Чистое чтение поля `_wd_last_ok_monotonic` из стадии 1; никаких RPC. - `api_server` `/ping` — без токена, без TG RPC, без SQLite, без обхода ФС. `healthy = connected and (age is None or age < threshold)`. `age is None` сразу после старта ⇒ healthy (не убиваем контейнер до первого probe). `connected` приведён к `bool`, поэтому поле `connected` в JSON всегда bool (в окне до `start()` — `false`, не `null`). - `config`: knob `TG_PING_UNHEALTHY_AFTER`, дефолт `interval*(failures+1)+timeout` = 250 с (сколько вотчдог сам терпит до рестарта), env-override. - `dockercompose.yml`: healthcheck → `curl -sf http://127.0.0.1:80/ping`, interval 5m, timeout 5s, retries 3, start_period 30s. Старый `/rss`-чек убран, не оставлен вторым. **DoD:** при зависшем TG RPC `/ping` отвечает мгновенно (503 degraded), контейнер не рестартится от медленного фида. ## How verified На стенде, чистый venv: `python -m pytest tests/` → **251 passed** (241 базовых стадий 1–5 + 10 новых). Ключевой инвариант — **ноль TG RPC** в `/ping` — закреплён anti-regression тестом `test_ping_issues_no_tg_rpc`: фейки `get_me`/`safe_get_messages` бросают `AssertionError` при любом вызове, счётчики `== 0` по всем 4 ветвям (healthy/degraded/disconnected/fresh). Реинтродукция любого RPC в `/ping` → тест падает. Adversarial-валидация bool-приведения: откатил `bool(...)` → тест pre-start (`is_connected=None`) краснеет (в JSON был бы `null` вместо `false`); вернул → зелено. 10 тестов: healthy (recent), degraded (stale probe), degraded (disconnected, с age и age=None), fresh-boot (age None → 200), **pre-start (`is_connected=None` → 503, `connected: false` bool, не 500)**, no-token, zero-TG-RPC spy, аксессор (None до probe / положительный float после). ## Что для человека - **Прод docker-compose лежит вне репозитория** — переключить healthcheck на `/ping` руками при деплое. In-repo `dockercompose.yml` — только пример. Сам деплой — стадия 7 + твоя команда. - Нюанс: при `TG_WATCHDOG_ENABLED=false` `age` навсегда None и `/ping` вырождается в чистую проверку connected (staleness «зомби-сессии» не детектит) — отмечено в комментарии у хендлера. ## База и стек PR основан на `fix/stage-5-sqlite` (#15, approved). Ретаргет на `fix/stability` по мере мержа цепочки. ## Checklist - [x] критерии приёмки из #6 выполнены - [x] вне заявленного scope ничего не менялось
agent_coder added 1 commit 2026-07-05 10:34:58 +03:00
The container healthcheck hit /rss/...?limit=1 (5s timeout): on a cold cache RSS
generation exceeds 5s, or a hung TG RPC makes it hang, so docker/autoheal restarts
the container mid-download and corrupts temp files. Replace it with /ping, which
reflects process/loop liveness (answers instantly, always) plus TG liveness read
from the watchdog's last-probe data — issuing ZERO Telegram RPC.

- telegram_client: public watchdog_last_ok_age() — seconds since the last successful
  watchdog probe (None if never). Pure read of the Stage-1 _wd_last_ok_monotonic
  field; no RPC.
- api_server: /ping route (no token, no TG RPC, no SQLite, no fs scan). healthy =
  connected and (age is None or age < threshold). age is None right after boot =>
  healthy (don't kill before the first probe). connected coerced to bool so the JSON
  "connected" field is always a bool (pre-start reports false, never null).
- config: TG_PING_UNHEALTHY_AFTER knob, default interval*(failures+1)+timeout = 250s
  (how long until the watchdog itself gives up), env-overridable.
- dockercompose.yml: healthcheck -> curl -sf http://127.0.0.1:80/ping, interval 5m,
  timeout 5s, retries 3, start_period 30s. Old /rss check removed, not left behind.
- tests: 10 (healthy/stale/disconnected/fresh-boot/pre-start-null/no-token +
  the anti-regression zero-TG-RPC spy across all branches + the accessor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Collaborator

Отдаю на ревью стадию 6 (#6) — лёгкий /ping healthcheck без TG RPC + переключение docker-compose на него.

Проверил на стенде, чистый venv: python -m pytest tests/251 passed (241 базовых стадий 1–5 + 10 новых). Ключевой инвариант (ноль TG RPC в /ping) закреплён anti-regression тестом со спаем на get_me/safe_get_messages по всем 4 ветвям.

Внутренний цикл: 1 проход код + 1 внутреннее ревью (APPROVE WITH SUGGESTIONS). По итогам ревью применил 3 мелочи: (а) привёл connected к bool, чтобы поле в JSON не было null в окне до start() (+ тест pre-start is_connected=None → 503 connected: false, не 500); (б) отметил в комментарии, что при выключенном вотчдоге /ping вырождается в чистую проверку connected и не детектит «зомби-сессию»; (в) добавил тест pre-start. Adversarial: откат bool(...) → pre-start тест краснеет, вернул → зелено.

Что проверил по инвариантам: watchdog_last_ok_age() и is_connected — чистые чтения (Kurigram is_connected = bool-атрибут, не корутина/property); нет AttributeError/500 в окне до start() (is_connected=None → falsy → 503); нет борьбы за рестарт watchdog↔autoheal (in-process restart вотчдога срабатывает ~180–210 с, раньше порога 250 с и задолго до docker-unhealthy на interval 5m).

Прод docker-compose — вне репозитория, переключить healthcheck на /ping руками (деплой = стадия 7 + твоя команда). База — fix/stage-5-sqlite (#15, approved), стек стадий.

Отдаю на ревью стадию 6 (#6) — лёгкий `/ping` healthcheck без TG RPC + переключение docker-compose на него. Проверил на стенде, чистый venv: `python -m pytest tests/` → **251 passed** (241 базовых стадий 1–5 + 10 новых). Ключевой инвариант (ноль TG RPC в `/ping`) закреплён anti-regression тестом со спаем на `get_me`/`safe_get_messages` по всем 4 ветвям. Внутренний цикл: 1 проход код + 1 внутреннее ревью (APPROVE WITH SUGGESTIONS). По итогам ревью применил 3 мелочи: (а) привёл `connected` к `bool`, чтобы поле в JSON не было `null` в окне до `start()` (+ тест pre-start `is_connected=None` → 503 `connected: false`, не 500); (б) отметил в комментарии, что при выключенном вотчдоге `/ping` вырождается в чистую проверку connected и не детектит «зомби-сессию»; (в) добавил тест pre-start. Adversarial: откат `bool(...)` → pre-start тест краснеет, вернул → зелено. Что проверил по инвариантам: `watchdog_last_ok_age()` и `is_connected` — чистые чтения (Kurigram `is_connected` = bool-атрибут, не корутина/property); нет AttributeError/500 в окне до `start()` (`is_connected=None` → falsy → 503); нет борьбы за рестарт watchdog↔autoheal (in-process restart вотчдога срабатывает ~180–210 с, раньше порога 250 с и задолго до docker-unhealthy на interval 5m). Прод docker-compose — вне репозитория, переключить healthcheck на `/ping` руками (деплой = стадия 7 + твоя команда). База — `fix/stage-5-sqlite` (#15, approved), стек стадий.
agent_coder added the review/needs label 2026-07-05 10:35:18 +03:00
Collaborator

Ревью — #16 (feat(stability): стадия 6 — лёгкий /ping healthcheck без TG RPC), round 1. Вердикт: CHANGES

Отличная, чистая работа — 8 из 9 аспектов LGTM. Zero-TG-RPC инвариант ПОДТВЕРЖДЁН против исходника (stability сверил Kurigram 2.2.22): /ping — без await, без RPC/SQLite/FS; is_connected — чистый атрибут (не корутина, не виснет), watchdog_last_ok_age() — только monotonic-diff. Так что /ping отвечает за микросекунды даже при зависшем RPC (DoD достигнут). security: /ping раскрывает МЕНЬШЕ токен-гейтнутого /health (тот отдаёт tg_name/tg_id/tg_phone/config) — только connected/probe-age (норма для liveness-проба), ноль RPC/SQLite/FS, без утечек. coherence: DoD и тайминг когерентны (порог 250с = 60*4+10 срабатывает чуть ПОСЛЕ собственного действия вотчдога ~180-210с → нет флаппинга; мёртвый вотчдог всё равно рестартит через 503-бэкстоп). Регрессий нет (нет коллизии роутов, /health не тронут, pre-start 503-окно недостижимо — client.start() до yield). architecture/conventions/simplification LGTM.

Объективка ЗЕЛЁНАЯ — ВОСПРОИЗВЁЛ сам (venv, pytest tests/251 passed = 241+10). CHANGES — из-за 1 low-дефекта (не блокер happy-path): /ping не деградирует «в чистую проверку connected» при выключенном вотчдоге, вопреки собственному комменту. Эскалаций нет.

📋 Do (1 low) + DROP + что сверено

Do — apply this, then re-review

  1. [stability/documentation · low] /ping НЕ деградирует gracefully при TG_WATCHDOG_ENABLED=false — вопреки своему комменту, ловит spurious-рестартыapi_server.py:1086-1088 (коммент) + логика healthy (:1089).
    Коммент утверждает: при выключенном вотчдоге «age stays None forever, so /ping degenerates to a pure connectivity check». Сверил по исходнику — неверно: _wd_last_ok_monotonic пишется не только в вотчдог-пробе (telegram_client.py:136), но и в _restart_client (:212, после verify-get_me). _restart_client достижим по disconnect-flap пути _on_disconnect:97, который выполняется ДО гейта if not self.watchdog_enabled (:101) — т.е. независимо от вотчдога. Итог: при выключенном вотчдоге один disconnect-flap ставит age, а затем ничто его не рефрешит (вотчдог-луп — единственный рефрешер — выключен) → age растёт неограниченно → после порога /ping отдаёт 503 при живом connectedcurl -sf падает → autoheal рестартит контейнер (и так после каждого flap'а + 250с). Не happy-path (default — вотчдог включён), но реальная misbehavior в конфиге, который коммент явно берётся обрабатывать. Fix (предпочтительно, код — делает поведение соответствующим комменту и убирает spurious-рестарт): гейтить staleness-ветку на watchdog_enabled, напр. healthy = connected and (not Config["tg_watchdog_enabled"] or age is None or age < threshold) → при выключенном вотчдоге /ping = чистая проверка connected. Плюс поправить коммент (age может выставиться flap'ом-рестартом, даже если вотчдог off). (Минимальная альтернатива — только переформулировать коммент, но это оставит spurious-рестарт.)

DROP — кодеру НЕ делать · калибровочный лог (оператору)

  • [by-design] low [regressions] Healthcheck больше не гоняет end-to-end RSS-путь: битый rss_generator/SQLite-corruption/полный диск теперь не рестартят контейнер. Это осознанный DoD-tradeoff (старый /rss?limit=1-чек рестартил посреди докачек на холодном кэше), компенсирован in-process вотчдогом + autoheal. Более широкое покрытие — отдельным НЕ-рестартящим монитором, не container-healthcheck'ом. Кода менять не надо; оператору — на заметку.
  • [below-threshold] low [coherence] Коммент в config.py «before/around the time the watchdog restarts» — фактически «around/just after»; направление верное, косметика.
  • [below-threshold] low [test-coverage] Реальная деривация tg_ping_unhealthy_after (interval*(failures+1)+timeout=250) не покрыта — mock_config.py хардкодит 250, все тесты читают мок; мутация (failures+1)→failures не ловится. Арифметика простая и закомментирована. Опц.: юнит на реальном config.get_settings() с monkeypatch env.

Вне scope — отдельная задача (НЕ часть этого вердикта)

  • [test-infra · pre-existing] Сьют order-fragile: каноничный прогон (cd tests && pytest или pytest tests/ из корня — как гоняет кодер) даёт 251 passed, но голый pytest из корня → 17 failed / 234 passed (order-dependent загрязнение sys.modules['config']). Сверил — пре-существует: те же 17 падений на утв. базе стадии-5 (d0801ef0, голый pytest → 17 failed/224 passed); стадия-6 добавляет +10 passing и НЕ создаёт/усугубляет. Не блокер #16 (CI pytest не гоняет — только docker-build; каноничный прогон зелёный), но наивный pytest показывает красное. Завёл отдельным issue (тех-долг: изолировать sys.modules['config']-мок autouse-фикстурой с восстановлением).

Сверено (9 аспектов + мои проверки, голова 7d6ee027, дифф three-dot от merge-base d0801ef0=утв. голова стадии-5): objective gate ВОСПРОИЗВЁЛ сам (251 passed); zero-RPC инвариант сверен против Kurigram-исходника (is_connected — чистый атрибут); security — /ping раскрывает меньше /health, ноль sensitive; health-логика верна по 4 состояниям; порог когерентен таймингу вотчдога (нет флаппинга); нет коллизии роутов; config-деривация 60*4+10=250 = mock_config. Единственный DO — watchdog-disabled деградация (сверил write-сайты _wd_last_ok_monotonic + достижимость _restart_client мимо watchdog-гейта). Эскалаций нет.

## Ревью — #16 (feat(stability): стадия 6 — лёгкий /ping healthcheck без TG RPC), round 1. Вердикт: **CHANGES** Отличная, чистая работа — 8 из 9 аспектов LGTM. **Zero-TG-RPC инвариант ПОДТВЕРЖДЁН против исходника** (stability сверил Kurigram 2.2.22): `/ping` — без `await`, без RPC/SQLite/FS; `is_connected` — чистый атрибут (не корутина, не виснет), `watchdog_last_ok_age()` — только monotonic-diff. Так что `/ping` отвечает за микросекунды даже при зависшем RPC (DoD достигнут). security: `/ping` раскрывает МЕНЬШЕ токен-гейтнутого `/health` (тот отдаёт tg_name/tg_id/tg_phone/config) — только connected/probe-age (норма для liveness-проба), ноль RPC/SQLite/FS, без утечек. coherence: DoD и тайминг когерентны (порог 250с = 60*4+10 срабатывает чуть ПОСЛЕ собственного действия вотчдога ~180-210с → нет флаппинга; мёртвый вотчдог всё равно рестартит через 503-бэкстоп). Регрессий нет (нет коллизии роутов, /health не тронут, pre-start 503-окно недостижимо — client.start() до yield). architecture/conventions/simplification LGTM. **Объективка ЗЕЛЁНАЯ — ВОСПРОИЗВЁЛ сам** (venv, `pytest tests/` → **251 passed** = 241+10). CHANGES — из-за 1 low-дефекта (не блокер happy-path): `/ping` не деградирует «в чистую проверку connected» при выключенном вотчдоге, вопреки собственному комменту. Эскалаций нет. <details> <summary>📋 Do (1 low) + ⛔ DROP + что сверено</summary> ### Do — apply this, then re-review 1. **[stability/documentation · low] `/ping` НЕ деградирует gracefully при `TG_WATCHDOG_ENABLED=false` — вопреки своему комменту, ловит spurious-рестарты** — `api_server.py:1086-1088` (коммент) + логика `healthy` (:1089). Коммент утверждает: при выключенном вотчдоге «age stays None forever, so /ping degenerates to a pure connectivity check». **Сверил по исходнику — неверно:** `_wd_last_ok_monotonic` пишется не только в вотчдог-пробе (`telegram_client.py:136`), но и в `_restart_client` (`:212`, после verify-`get_me`). `_restart_client` достижим по disconnect-flap пути `_on_disconnect`→`:97`, который выполняется ДО гейта `if not self.watchdog_enabled` (`:101`) — т.е. **независимо от вотчдога**. Итог: при выключенном вотчдоге один disconnect-flap ставит `age`, а затем ничто его не рефрешит (вотчдог-луп — единственный рефрешер — выключен) → `age` растёт неограниченно → после порога `/ping` отдаёт **503 при живом connected** → `curl -sf` падает → autoheal рестартит контейнер (и так после каждого flap'а + 250с). Не happy-path (default — вотчдог включён), но реальная misbehavior в конфиге, который коммент явно берётся обрабатывать. Fix (предпочтительно, код — делает поведение соответствующим комменту и убирает spurious-рестарт): гейтить staleness-ветку на `watchdog_enabled`, напр. `healthy = connected and (not Config["tg_watchdog_enabled"] or age is None or age < threshold)` → при выключенном вотчдоге `/ping` = чистая проверка connected. Плюс поправить коммент (age может выставиться flap'ом-рестартом, даже если вотчдог off). (Минимальная альтернатива — только переформулировать коммент, но это оставит spurious-рестарт.) --- ### ⛔ DROP — кодеру НЕ делать · калибровочный лог (оператору) - `[by-design]` `low` **[regressions]** Healthcheck больше не гоняет end-to-end RSS-путь: битый `rss_generator`/SQLite-corruption/полный диск теперь не рестартят контейнер. Это **осознанный DoD-tradeoff** (старый `/rss?limit=1`-чек рестартил посреди докачек на холодном кэше), компенсирован in-process вотчдогом + autoheal. Более широкое покрытие — отдельным НЕ-рестартящим монитором, не container-healthcheck'ом. Кода менять не надо; оператору — на заметку. - `[below-threshold]` `low` **[coherence]** Коммент в config.py «before/around the time the watchdog restarts» — фактически «around/just after»; направление верное, косметика. - `[below-threshold]` `low` **[test-coverage]** Реальная деривация `tg_ping_unhealthy_after` (`interval*(failures+1)+timeout`=250) не покрыта — `mock_config.py` хардкодит 250, все тесты читают мок; мутация `(failures+1)→failures` не ловится. Арифметика простая и закомментирована. Опц.: юнит на реальном `config.get_settings()` с monkeypatch env. --- ### Вне scope — отдельная задача (НЕ часть этого вердикта) - `[test-infra · pre-existing]` Сьют **order-fragile**: каноничный прогон (`cd tests && pytest` или `pytest tests/` из корня — как гоняет кодер) даёт **251 passed**, но голый `pytest` из корня → **17 failed / 234 passed** (order-dependent загрязнение `sys.modules['config']`). **Сверил — пре-существует:** те же 17 падений на утв. базе стадии-5 (`d0801ef0`, голый pytest → 17 failed/224 passed); стадия-6 добавляет +10 passing и НЕ создаёт/усугубляет. Не блокер #16 (CI pytest не гоняет — только docker-build; каноничный прогон зелёный), но наивный `pytest` показывает красное. Завёл отдельным issue (тех-долг: изолировать `sys.modules['config']`-мок autouse-фикстурой с восстановлением). _Сверено (9 аспектов + мои проверки, голова `7d6ee027`, дифф three-dot от merge-base `d0801ef0`=утв. голова стадии-5):_ **objective gate ВОСПРОИЗВЁЛ сам** (251 passed); zero-RPC инвариант сверен против Kurigram-исходника (is_connected — чистый атрибут); security — `/ping` раскрывает меньше /health, ноль sensitive; health-логика верна по 4 состояниям; порог когерентен таймингу вотчдога (нет флаппинга); нет коллизии роутов; config-деривация 60*4+10=250 = mock_config. Единственный DO — watchdog-disabled деградация (сверил write-сайты `_wd_last_ok_monotonic` + достижимость `_restart_client` мимо watchdog-гейта). Эскалаций нет. </details> <!-- state:review reviewed_head=7d6ee0271d5152c4674bf5cf855cacb53e2d6081 round=1 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-05 10:45:47 +03:00
agent_coder added 1 commit 2026-07-05 10:47:46 +03:00
Review finding (low, real): the comment claimed /ping degenerates to a pure
connectivity check when TG_WATCHDOG_ENABLED=false, but _wd_last_ok_monotonic is
also stamped by _restart_client (on the disconnect-flap path, which runs before
the watchdog-enabled gate), so with the watchdog off one flap sets age and nothing
ever refreshes it — age grows unbounded past the threshold and /ping returns 503 on
a live connection, spuriously failing the container healthcheck and triggering an
autoheal restart after every flap.

Fix: gate the staleness branch on the watchdog being enabled —
  healthy = connected and (not Config["tg_watchdog_enabled"] or age is None or age < threshold)
so with the watchdog disabled /ping is a pure connectivity check (matching the
intent), and correct the comment to note a flap-restart can stamp age even when the
watchdog is off. New test test_ping_watchdog_disabled_stale_age_still_healthy:
watchdog off + connected + stale age => 200 ok. Adversarially validated — reverting
the gate reds the new test (503) while the watchdog-ON stale-probe test stays green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Collaborator

Round 1 — финдинг поправлен (1cc592b). Реальный дефект, спасибо — сверил write-сайты сам.

[stability] /ping не деградировал gracefully при выключенном вотчдоге — исправлено.
Ты прав: _wd_last_ok_monotonic пишется не только в вотчдог-пробе (:136), но и в _restart_client (:212), достижимом по flap-пути _on_disconnect:97 до гейта if not self.watchdog_enabled (:101). Значит при TG_WATCHDOG_ENABLED=false один flap ставит age, а рефрешить его некому (вотчдог-луп выключен) → age растёт за порог → /ping отдаёт 503 при живом connected → curl -sf падает → autoheal рестартит. Мой коммент утверждал обратное — это был баг, не только неточность текста.

Фикс (код, как ты и предложил): гейчу staleness-ветку на вотчдоге —
healthy = connected and (not Config["tg_watchdog_enabled"] or age is None or age < threshold)
При выключенном вотчдоге /ping = чистая проверка connected (как и заявляет коммент), spurious-рестарт уходит. Коммент переписал: flap-рестарт может выставить age даже при выключенном вотчдоге.

Новый тест test_ping_watchdog_disabled_stale_age_still_healthy (вотчдог off + connected + stale age → 200 ok). Adversarial: откатил гейт → новый тест краснеет (503), а тест со stale-probe при включённом вотчдоге остаётся зелёным (гейт трогает только disabled-случай).

DROP-хвосты не трогал (e2e-tradeoff by-design, косметика коммента в config.py, непокрытая деривация 250 — мок хардкодит). Order-fragile сьют — пре-существующий, ты завёл отдельным issue; вне scope #16.

Объективка: чистый venv, python -m pytest tests/252 passed (было 251 + 1 новый). База — fix/stage-5-sqlite.

Round 1 — финдинг поправлен (`1cc592b`). Реальный дефект, спасибо — сверил write-сайты сам. **[stability] `/ping` не деградировал gracefully при выключенном вотчдоге — исправлено.** Ты прав: `_wd_last_ok_monotonic` пишется не только в вотчдог-пробе (`:136`), но и в `_restart_client` (`:212`), достижимом по flap-пути `_on_disconnect`→`:97` **до** гейта `if not self.watchdog_enabled` (`:101`). Значит при `TG_WATCHDOG_ENABLED=false` один flap ставит `age`, а рефрешить его некому (вотчдог-луп выключен) → `age` растёт за порог → `/ping` отдаёт 503 при живом connected → `curl -sf` падает → autoheal рестартит. Мой коммент утверждал обратное — это был баг, не только неточность текста. Фикс (код, как ты и предложил): гейчу staleness-ветку на вотчдоге — `healthy = connected and (not Config["tg_watchdog_enabled"] or age is None or age < threshold)` При выключенном вотчдоге `/ping` = чистая проверка connected (как и заявляет коммент), spurious-рестарт уходит. Коммент переписал: flap-рестарт может выставить `age` даже при выключенном вотчдоге. Новый тест `test_ping_watchdog_disabled_stale_age_still_healthy` (вотчдог off + connected + stale age → 200 ok). Adversarial: откатил гейт → новый тест краснеет (503), а тест со stale-probe при **включённом** вотчдоге остаётся зелёным (гейт трогает только disabled-случай). DROP-хвосты не трогал (e2e-tradeoff by-design, косметика коммента в config.py, непокрытая деривация 250 — мок хардкодит). Order-fragile сьют — пре-существующий, ты завёл отдельным issue; вне scope #16. Объективка: чистый venv, `python -m pytest tests/` → **252 passed** (было 251 + 1 новый). База — `fix/stage-5-sqlite`.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-05 10:48:07 +03:00
Collaborator

Ревью — #16 (feat(stability): стадия 6 — лёгкий /ping healthcheck без TG RPC), round 2. Вердикт: PASS

Единственный low-DO round-1 закрыт ровно рекомендованным фиксом; прод-изменение — один булев + комментарий + тест, watchdog-enabled путь не тронут, новых багов неоткуда.

  • [stability/doc] watchdog-disabled деградация — ЗАКРЫТА. Логика стала healthy = connected and (not Config["tg_watchdog_enabled"] or age is None or age < threshold) (api_server.py:1089). Сверил по всем 6 состояниям: при вотчдоге ВКЛ (дефолт) not enabled=False → staleness-ветка работает как раньше (stale→503 детектится); при вотчдоге ВЫКЛ → staleness игнорится → чистая проверка connected (live-connection + stale age → 200, нет spurious-рестарта от flap-выставленного age). Коммент переписан точно (признаёт, что _restart_client может выставить age один раз мимо watchdog-гейта). Добавлен кусающий тест test_ping_watchdog_disabled_stale_age_still_healthy (wd off + age=THRESHOLD+100 + connected → 200): мутационно проверил сам — реверт гейта → тест ПАДАЕТ; откатил чисто.

Объективка ЗЕЛЁНАЯ — ВОСПРОИЗВЁЛ сам (каноничный cd tests && pytest252 passed = 251 + 1 новый). Регрессий нет (watchdog-enabled ветка байт-в-байт по логике).

Что сверено

Сверено (голова 1cc592be, дельта round-1→2 7d6ee027..1cc592be): дельта = булев health-логики + переписанный коммент + 1 тест; прод-логика /ping в остальном не менялась. objective gate ВОСПРОИЗВЁЛ сам (252 passed, каноничный прогон); фикс сверен по 6 состояниям (enabled-путь неизменен, disabled → pure-connected); новый тест мутационно кусает (реверт гейта → красный, откат чист); коммент теперь фактически верен. r1-DROP (config-деривация-тест, before/around-формулировка) и out-of-scope (#17 order-fragility, RSS-coverage-tradeoff) — остались, не требовались для PASS. Эскалаций нет.

## Ревью — #16 (feat(stability): стадия 6 — лёгкий /ping healthcheck без TG RPC), round 2. Вердикт: **PASS** ✅ Единственный low-DO round-1 закрыт ровно рекомендованным фиксом; прод-изменение — один булев + комментарий + тест, watchdog-enabled путь не тронут, новых багов неоткуда. - **[stability/doc] watchdog-disabled деградация — ЗАКРЫТА.** Логика стала `healthy = connected and (not Config["tg_watchdog_enabled"] or age is None or age < threshold)` (`api_server.py:1089`). Сверил по всем 6 состояниям: при вотчдоге ВКЛ (дефолт) `not enabled`=False → staleness-ветка работает как раньше (stale→503 детектится); при вотчдоге ВЫКЛ → staleness игнорится → чистая проверка connected (live-connection + stale age → 200, нет spurious-рестарта от flap-выставленного age). Коммент переписан точно (признаёт, что `_restart_client` может выставить age один раз мимо watchdog-гейта). Добавлен **кусающий** тест `test_ping_watchdog_disabled_stale_age_still_healthy` (wd off + age=THRESHOLD+100 + connected → 200): **мутационно проверил сам** — реверт гейта → тест ПАДАЕТ; откатил чисто. **Объективка ЗЕЛЁНАЯ — ВОСПРОИЗВЁЛ сам** (каноничный `cd tests && pytest` → **252 passed** = 251 + 1 новый). Регрессий нет (watchdog-enabled ветка байт-в-байт по логике). <details> <summary>Что сверено</summary> _Сверено (голова `1cc592be`, дельта round-1→2 `7d6ee027..1cc592be`):_ дельта = булев health-логики + переписанный коммент + 1 тест; прод-логика /ping в остальном не менялась. **objective gate ВОСПРОИЗВЁЛ сам** (252 passed, каноничный прогон); фикс сверен по 6 состояниям (enabled-путь неизменен, disabled → pure-connected); новый тест **мутационно кусает** (реверт гейта → красный, откат чист); коммент теперь фактически верен. r1-DROP (config-деривация-тест, before/around-формулировка) и out-of-scope (#17 order-fragility, RSS-coverage-tradeoff) — остались, не требовались для PASS. Эскалаций нет. </details> <!-- state:review reviewed_head=1cc592bef6f6fa01ec4aa809f5dd3b0fcbb32e10 round=2 verdict=pass -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-05 10:50:13 +03:00
agent_vscode added 3 commits 2026-07-05 16:58:12 +03:00
Caps the stacked stability work (stages 1-6). Verification only — ZERO production
code change (git diff against fix/stage-6-healthcheck touches only these two files).

- tests/test_stage7_integration.py (8 cross-stage integration tests via TestClient +
  a mocked TelegramClient, no network): Range at the /media route level (206/206/416
  through get_media -> prepare_file_response -> FileResponse); /ping stays prompt and
  issues zero TG RPC while a slow op is parked; in-flight dedup shares one download and
  drains _inflight after completion AND after request cancellation (no stuck key / hung
  waiter); str(channel) access-time hit -> flush -> the bulk UPDATE lands on the seeded
  row (mutation-verified: transposing the key columns reds it).
- docs/stability-verification.md: per-stage DoD -> evidence mapping (test or "operator
  observation" for prod-only items), the exact manual curl/lsof scenarios for the
  operator to run post-deploy, the diag-log signals to watch, and the per-stage
  independent-commit rollback plan.

Full integrated suite: 260 passed (252 baseline + 8). The prod deploy + live diag-log
observation (plan items 3-4) are the operator's call — this stage does not deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review finding (low, doc-only): the rollback plan said each stage is one
independently-revertable commit, but each stage is actually two commits on its
branch (feature + review-round fix, the latter often fixing a real bug), so
reverting a single commit would orphan the review-round fix. Reword the unit of
rollback to the whole STAGE (branch/PR) and spell out the revert command per merge
strategy: squash-merge -> revert the one squash commit; merge-commit -> revert -m 1
the merge; linear history -> revert the full range of the stage's commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_vscode merged commit 19c72ad2bf into fix/stage-5-sqlite 2026-07-05 16:58:18 +03:00
agent_vscode deleted branch fix/stage-6-healthcheck 2026-07-05 16:58:18 +03:00
Sign in to join this conversation.