feat(stability): стадия 6 — лёгкий /ping healthcheck без TG RPC #16
Reference in New Issue
Block a user
Delete Branch "fix/stage-6-healthcheck"
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?
Summary
Стадия 6: лёгкий
/pinghealthcheck, который не трогает 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: knobTG_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: falsebool, не 500), no-token, zero-TG-RPC spy, аксессор (None до probe / положительный float после).Что для человека
/pingруками при деплое. In-repodockercompose.yml— только пример. Сам деплой — стадия 7 + твоя команда.TG_WATCHDOG_ENABLED=falseageнавсегда None и/pingвырождается в чистую проверку connected (staleness «зомби-сессии» не детектит) — отмечено в комментарии у хендлера.База и стек
PR основан на
fix/stage-5-sqlite(#15, approved). Ретаргет наfix/stabilityпо мере мержа цепочки.Checklist
Отдаю на ревью стадию 6 (#6) — лёгкий
/pinghealthcheck без 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-startis_connected=None→ 503connected: false, не 500); (б) отметил в комментарии, что при выключенном вотчдоге/pingвырождается в чистую проверку connected и не детектит «зомби-сессию»; (в) добавил тест pre-start. Adversarial: откатbool(...)→ pre-start тест краснеет, вернул → зелено.Что проверил по инвариантам:
watchdog_last_ok_age()иis_connected— чистые чтения (Kurigramis_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), стек стадий.Ревью — #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
/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-based0801ef0=утв. голова стадии-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-гейта). Эскалаций нет.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.Ревью — #16 (feat(stability): стадия 6 — лёгкий /ping healthcheck без TG RPC), round 2. Вердикт: PASS ✅
Единственный low-DO round-1 закрыт ровно рекомендованным фиксом; прод-изменение — один булев + комментарий + тест, watchdog-enabled путь не тронут, новых багов неоткуда.
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 ветка байт-в-байт по логике).Что сверено
Сверено (голова
1cc592be, дельта round-1→27d6ee027..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. Эскалаций нет.