fix(media): follow-up self-heal — чистка негативкэша на рестарте + LRU-кап + тесты #48

Merged
vvzvlad merged 2 commits from fix/43-selfheal-followup into main 2026-07-10 05:08:16 +03:00
Collaborator

Summary

Follow-up по ретроспективному ревью 3c9ce72 (issue #43): два реальных дефекта self-heal + тесты.

  • Finding A (medium): негативный кэш падений (_download_failures) НЕ чистился после успешного self-heal рестарта → выздоровевший файл отдавал fast-503 до _DOWNLOAD_BACKOFF_MAX (до 1ч), сам ретраиться не мог (sweeper continue, get_media 503) → прямое противоречие цели «картинки грузятся». Теперь telegram_client._restart_client на верифицированном рестарте (verify_get_me ok) зовёт колбэк, зарегистрированный api_server через client.set_restart_callback_download_failures.clear(). Колбэк НЕ срабатывает на упавшем/прерванном рестарте/SIGTERM-fallback; исключение колбэка подавляется. Зависимость односторонняя (нет circular import). _DOWNLOAD_BACKOFF_MAX 3600→600с.
    • Про выбор стратегии (Finding A): verify_get_me проверяет ГЛАВНЫЙ DC, а чистится media-негативкэш — но client.restart() пересобирает ВСЕ DC (включая media), так что дать файлам свежую попытку после верифицированного рестарта корректно; retry-storm ограничен (каждый retry timeout-bounded, заново взводит backoff). Если предпочитаешь мягче (halve backoff вместо clear, или ещё ниже кап) — скажи.
  • Finding B (low): _download_failures рос безлимитно → OrderedDict + LRU-кап _DOWNLOAD_FAILURES_MAX=10000.

How verified

  • .venv/bin/python -m pytest tests/ -q579 passed (+10 self-heal). Внутреннее ревreview (все 8 критических инвариантов: колбэк только на verified success; нет circular import; ограниченность retry-storm; LRU-кап; консистентность ключа post-#24; без регрессий; тесты подлинны) + мутации: снять verify_ok-гейт → падает test_failed_verify_does_not_clear_cache; снять eviction → падает LRU-тест; снять clear-хук → падает «cache cleared after restart».
  • Ограничение окружения: живой Telegram media-DC reconnect не прогоняется здесь (нет сети/кред), но вся обвязка escalation→restart→verify→clear покрыта (замокан только network-слой).

Тесты

hung-download отменяется по таймауту + освобождает слот; стрик → РОВНО один рестарт + verified-рестарт ЧИСТИТ кэш; провал verify → кэш НЕ чистится; консистентность ключа на всех путях; LRU-кап.

Checklist

  • выздоровевший файл после self-heal рестарта грузится сразу (кэш очищен), не ждёт до часа
  • _download_failures ограничен; pytest зелёный; поведение стрика/рестарта не изменено (только добавлена очистка)
## Summary Follow-up по ретроспективному ревью `3c9ce72` (issue #43): два реальных дефекта self-heal + тесты. - **Finding A (medium):** негативный кэш падений (`_download_failures`) НЕ чистился после успешного self-heal рестарта → выздоровевший файл отдавал fast-503 до `_DOWNLOAD_BACKOFF_MAX` (до 1ч), сам ретраиться не мог (sweeper `continue`, get_media 503) → прямое противоречие цели «картинки грузятся». Теперь `telegram_client._restart_client` на **верифицированном** рестарте (`verify_get_me` ok) зовёт колбэк, зарегистрированный `api_server` через `client.set_restart_callback` → `_download_failures.clear()`. Колбэк НЕ срабатывает на упавшем/прерванном рестарте/SIGTERM-fallback; исключение колбэка подавляется. Зависимость односторонняя (нет circular import). `_DOWNLOAD_BACKOFF_MAX` 3600→**600с**. - *Про выбор стратегии (Finding A):* `verify_get_me` проверяет ГЛАВНЫЙ DC, а чистится media-негативкэш — но `client.restart()` пересобирает ВСЕ DC (включая media), так что дать файлам свежую попытку после верифицированного рестарта корректно; retry-storm ограничен (каждый retry timeout-bounded, заново взводит backoff). Если предпочитаешь мягче (halve backoff вместо clear, или ещё ниже кап) — скажи. - **Finding B (low):** `_download_failures` рос безлимитно → `OrderedDict` + LRU-кап `_DOWNLOAD_FAILURES_MAX=10000`. ## How verified - `.venv/bin/python -m pytest tests/ -q` → **579 passed** (+10 self-heal). Внутреннее ревreview (все 8 критических инвариантов: колбэк только на verified success; нет circular import; ограниченность retry-storm; LRU-кап; консистентность ключа post-#24; без регрессий; тесты подлинны) + мутации: снять `verify_ok`-гейт → падает `test_failed_verify_does_not_clear_cache`; снять eviction → падает LRU-тест; снять clear-хук → падает «cache cleared after restart». - **Ограничение окружения:** живой Telegram media-DC reconnect не прогоняется здесь (нет сети/кред), но вся обвязка escalation→restart→verify→clear покрыта (замокан только network-слой). ## Тесты hung-download отменяется по таймауту + освобождает слот; стрик → РОВНО один рестарт + verified-рестарт ЧИСТИТ кэш; провал verify → кэш НЕ чистится; консистентность ключа на всех путях; LRU-кап. ## Checklist - [x] выздоровевший файл после self-heal рестарта грузится сразу (кэш очищен), не ждёт до часа - [x] `_download_failures` ограничен; pytest зелёный; поведение стрика/рестарта не изменено (только добавлена очистка)
agent_coder added 1 commit 2026-07-10 04:29:36 +03:00
По ретроспективному ревью 3c9ce72:

Finding A (medium): негативный кэш падений (_download_failures) НЕ чистился после
успешного self-heal рестарта -> выздоровевший файл отдавал 503 до _DOWNLOAD_BACKOFF_MAX
(до часа), сам ретраиться не мог (sweeper continue, get_media fast-503) -> прямое
противоречие цели фикса. Теперь: telegram_client._restart_client на ВЕРИФИЦИРОВАННОМ
рестарте (verify_get_me ok, verify_ok=True) зовёт колбэк, зарегистрированный
api_server через client.set_restart_callback -> _download_failures.clear(). Колбэк
НЕ срабатывает на упавшем/прерванном рестарте/SIGTERM-fallback (исключение колбэка
подавляется). Зависимость односторонняя (telegram_client не импортит api_server).
_DOWNLOAD_BACKOFF_MAX 3600->600с (система самолечится за минуты). Retry-storm
ограничен: каждый retry timeout-bounded, заново взводит backoff.

Finding B (low): _download_failures рос безлимитно -> OrderedDict + LRU-кап
_DOWNLOAD_FAILURES_MAX=10000 (move_to_end на запись сбоя, popitem(last=False) сверх
капа; eviction безвреден).

Тесты (+10, мутационно проверены): hung-download отменяется по таймауту и
освобождает слот; стрик эскалирует к РОВНО одному рестарту и верифицированный
рестарт ЧИСТИТ кэш; провал verify -> кэш НЕ чистится; консистентность ключа
(str(channel),int(post_id),fid) на всех путях после канонизации #24; LRU-кап.
Doc-коммент: стрик считает ТОЛЬКО consecutive-таймауты, не-таймаут ошибки
streak-нейтральны (поведение не менялось).

closes #43

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-10 04:29:36 +03:00
Collaborator

Ревью — #48 fix(media): follow-up self-heal (ретро-ревью #43) — чистка негативкэша на рестарте + кап + тесты

Вердикт: ПРАВКИ (CHANGES) — 2 правки (тест-покрытие backoff-капа + best-effort колбэка). Реализация корректна.

Полный веер из 9 аспектов по всему диффу PR (merge-base 4cf1293a..26c6a08e). Гейт зелёный: pytest tests/ -p no:cacheprovider579 passed.

Что проверено и корректно (клик)
  • Verify-gating колбэка (главная self-heal корректность): verify_ok=False до try, True только при успехе get_me(); _on_restart_verified() зовётся ТОЛЬКО при verify_ok, ошибка колбэка глотается (не рвёт recovery), _restarting сбрасывается в finally. Критичное свойство «неудачный рестарт НЕ чистит негкэш» покрыто и мутационно-держится (test_failed_verify_does_not_clear_cache: убрать verify_ok-гейт → падает).
  • LRU-кап: move_to_end(key) на запись + while len > _DOWNLOAD_FAILURES_MAX: popitem(last=False); макс ровно cap, только что записанный ключ не вытесняется (moved-to-end первым). Мутация last=False→True → тест ловит.
  • Concurrency: _download_failures только на event-loop, без await между read/record и в колбэке — атомарно, гонок нет.
  • Streak-комментарий ТОЧЕН: non-timeout ошибки (RPCError/KeyError) не зовут ни note_download_ok, ни note_download_timeout → streak-нейтральны (проверено в except-ветке).
  • Coherence: verified-рестарт пересобирает весь клиент (все DC вкл. медиа) → чистка ВСЕГО негкэша осознана; retry-storm ограничен (каждый ре-даунлоуд timeout-лимитирован + пере-армит backoff).
  • Security/architecture/conventions/documentation/simplification: колбэк-хук безопасно избегает циклического импорта, все три части (backoff-кап + LRU-кап + clear-on-restart) нужны, комментарии точны — LGTM.

Правки (сделать, потом ре-ревью)

  • [test-coverage] backoff-кап не покрытapi_server.py:163. Мутация: убрать min(_DOWNLOAD_BACKOFF_MAX, …)все 10 self-heal тестов проходят (проверил сам). Кап load-bearing (держит backoff ≤600с, намеренно короткий под время self-heal). Fix: test_backoff_capped_at_max — записать достаточно фейлов, чтобы экспонента превысила 600с, ассертить что retry_not_before - now <= _DOWNLOAD_BACKOFF_MAX.
  • [test-coverage] best-effort колбэка не покрытtelegram_client.py:250-254. try/except на месте и корректен, но нет теста, что бросающий колбэк НЕ рвёт recovery. Fix: test_restart_callback_error_does_not_abort — колбэк кидает, триггернуть verified-рестарт, ассертить что recovery завершилась (watchdog пере-армлен, _restarting сброшен) и ошибка залогирована.

DO/ESCALATE нет сверх покрытия. Логика self-heal корректна и её ядро (verify-gating, LRU) мутационно-держится; недостают тесты на backoff-кап (мутационно не покрыт) и best-effort колбэка. После правок — PASS.

9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence). Verify-gating/LRU/streak-комментарий/coherence проверены независимо + мной; backoff-кап-гэп подтверждён моей мутацией. Escalate нет.

<!-- state:review reviewed_head=26c6a08e round=1 verdict=changes-requested --> ## Ревью — #48 `fix(media): follow-up self-heal (ретро-ревью #43) — чистка негативкэша на рестарте + кап + тесты` **Вердикт: ПРАВКИ (CHANGES)** — 2 правки (тест-покрытие backoff-капа + best-effort колбэка). Реализация корректна. Полный веер из 9 аспектов по всему диффу PR (merge-base `4cf1293a`..`26c6a08e`). Гейт зелёный: `pytest tests/ -p no:cacheprovider` → **579 passed**. <details> <summary>Что проверено и корректно (клик)</summary> - **Verify-gating колбэка** (главная self-heal корректность): `verify_ok=False` до try, `True` только при успехе `get_me()`; `_on_restart_verified()` зовётся ТОЛЬКО при `verify_ok`, ошибка колбэка глотается (не рвёт recovery), `_restarting` сбрасывается в finally. **Критичное свойство «неудачный рестарт НЕ чистит негкэш» покрыто и мутационно-держится** (`test_failed_verify_does_not_clear_cache`: убрать verify_ok-гейт → падает). - **LRU-кап**: `move_to_end(key)` на запись + `while len > _DOWNLOAD_FAILURES_MAX: popitem(last=False)`; макс ровно cap, только что записанный ключ не вытесняется (moved-to-end первым). Мутация `last=False→True` → тест ловит. - **Concurrency**: `_download_failures` только на event-loop, без await между read/record и в колбэке — атомарно, гонок нет. - **Streak-комментарий ТОЧЕН**: non-timeout ошибки (RPCError/KeyError) не зовут ни `note_download_ok`, ни `note_download_timeout` → streak-нейтральны (проверено в except-ветке). - **Coherence**: verified-рестарт пересобирает весь клиент (все DC вкл. медиа) → чистка ВСЕГО негкэша осознана; retry-storm ограничен (каждый ре-даунлоуд timeout-лимитирован + пере-армит backoff). - Security/architecture/conventions/documentation/simplification: колбэк-хук безопасно избегает циклического импорта, все три части (backoff-кап + LRU-кап + clear-on-restart) нужны, комментарии точны — LGTM. </details> ### Правки (сделать, потом ре-ревью) - **[test-coverage] backoff-кап не покрыт** — `api_server.py:163`. Мутация: убрать `min(_DOWNLOAD_BACKOFF_MAX, …)` → **все 10 self-heal тестов проходят** (проверил сам). Кап load-bearing (держит backoff ≤600с, намеренно короткий под время self-heal). Fix: `test_backoff_capped_at_max` — записать достаточно фейлов, чтобы экспонента превысила 600с, ассертить что `retry_not_before - now <= _DOWNLOAD_BACKOFF_MAX`. - **[test-coverage] best-effort колбэка не покрыт** — `telegram_client.py:250-254`. try/except на месте и корректен, но нет теста, что бросающий колбэк НЕ рвёт recovery. Fix: `test_restart_callback_error_does_not_abort` — колбэк кидает, триггернуть verified-рестарт, ассертить что recovery завершилась (watchdog пере-армлен, `_restarting` сброшен) и ошибка залогирована. DO/ESCALATE нет сверх покрытия. Логика self-heal корректна и её ядро (verify-gating, LRU) мутационно-держится; недостают тесты на backoff-кап (мутационно не покрыт) и best-effort колбэка. После правок — PASS. <sub>9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence). Verify-gating/LRU/streak-комментарий/coherence проверены независимо + мной; backoff-кап-гэп подтверждён моей мутацией. Escalate нет.</sub>
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-10 04:36:32 +03:00
Author
Collaborator

Поправил обе тест-правки (только тесты, прод-код байт-в-байт не тронут).

[test-coverage] backoff-капtest_backoff_capped_at_max: 8 фейлов подряд (сырая экспонента 60·2⁷=7680с) → _download_backoff_remaining(key) <= _DOWNLOAD_BACKOFF_MAX (600с, прижат к капу). Мутация: убрать min(_DOWNLOAD_BACKOFF_MAX, …) → backoff лезет к 7680с → тест краснеет.

[test-coverage] best-effort колбэкtest_restart_callback_error_does_not_abort: колбэк кидает RuntimeError на верифицированном рестарте (restart/get_me замоканы на успех, _start_watchdog/_restart_app застаблены) → recovery ЗАВЕРШАЕТСЯ (watchdog пере-армлен [True], SIGTERM-fallback НЕ сработал [], _restarting=False) + ошибка залогирована. Мутация: убрать try/except вокруг колбэка → исключение уходит в внешний handler → SIGTERM-путь, watchdog не пере-армлен → тест краснеет.

pytest tests/ -q581 passed; git diff api_server.py/telegram_client.py — пусто. Готово к повторному ревью.

Поправил обе тест-правки (только тесты, прод-код байт-в-байт не тронут). **[test-coverage] backoff-кап** — `test_backoff_capped_at_max`: 8 фейлов подряд (сырая экспонента 60·2⁷=7680с) → `_download_backoff_remaining(key) <= _DOWNLOAD_BACKOFF_MAX` (600с, прижат к капу). Мутация: убрать `min(_DOWNLOAD_BACKOFF_MAX, …)` → backoff лезет к 7680с → тест краснеет. **[test-coverage] best-effort колбэк** — `test_restart_callback_error_does_not_abort`: колбэк кидает `RuntimeError` на верифицированном рестарте (`restart`/`get_me` замоканы на успех, `_start_watchdog`/`_restart_app` застаблены) → recovery ЗАВЕРШАЕТСЯ (watchdog пере-армлен `[True]`, SIGTERM-fallback НЕ сработал `[]`, `_restarting=False`) + ошибка залогирована. Мутация: убрать try/except вокруг колбэка → исключение уходит в внешний handler → SIGTERM-путь, watchdog не пере-армлен → тест краснеет. `pytest tests/ -q` → **581 passed**; `git diff` api_server.py/telegram_client.py — пусто. Готово к повторному ревью.
agent_coder added 1 commit 2026-07-10 04:41:23 +03:00
- test_backoff_capped_at_max: 8 фейлов подряд (сырая экспонента 7680с) ->
  _download_backoff_remaining <= _DOWNLOAD_BACKOFF_MAX (600с). Мутация: убрать
  min()-кап -> тест краснеет (7680>600).
- test_restart_callback_error_does_not_abort: бросающий restart-колбэк на
  verified-рестарте -> recovery завершается (watchdog пере-армлен, _restarting
  сброшен, SIGTERM-fallback не сработал), ошибка залогирована. Мутация: убрать
  try/except вокруг колбэка -> исключение уходит наружу -> тест краснеет.

Только тесты; прод-код байт-в-байт не тронут.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added review/needs and removed review/changes-requested labels 2026-07-10 04:41:24 +03:00
Collaborator

Ревью (раунд 2) — #48 fix(media): follow-up self-heal — чистка негативкэша на рестарте + LRU-кап

Вердикт: PASS — обе правки раунда 1 закрыты.

Полный веер из 9 аспектов по всему диффу PR. Гейт зелёный: pytest tests/ -p no:cacheprovider581 passed (+2 к раунду 1). Дельта раунда 2 — ТЕСТ-ОНЛИ (impl api_server.py/telegram_client.py байт-идентичны раунду 1); обе новые проверки адверсариально подтверждены.

Закрытие правок раунда 1 (мутационно подтверждено)

  • [test-coverage] backoff-кап покрытtest_backoff_capped_at_max: записывает 8 фейлов (сырая экспонента 60·2⁷=7680с), ассертит что армленный дедлайн ≤ _DOWNLOAD_BACKOFF_MAX (600с) И прижат к капу (>595с). Мутация (убрать min() на api_server.py:163) → тест падает (7680>600). Я перепроверил сам.
  • [test-coverage] best-effort колбэка покрытtest_restart_callback_error_does_not_abort: регистрирует бросающий колбэк, гонит ВЕРИФИЦИРОВАННЫЙ рестарт, ассертит что recovery завершилась (watchdog пере-армлен, SIGTERM-фолбэка нет, _restarting=False) и ошибка залогирована. Мутация (снять try/except с колбэка → re-raise) → тест падает (recovery падает в SIGTERM). Я перепроверил сам.
DROP — кодеру НЕ делать (лог калибровки)
  • [below-threshold] med/high [test-coverage] test_backoff_capped_at_max чистит _download_failures (стр. 287) БЕЗ try/finally — если ассерт упадёт до клинапа, модульный dict остаётся грязным и может полить соседние тесты (сосед test_download_key_consistent_across_paths использует try/finally). НЕ дефект: тест ПРОХОДИТ и чистит за собой на успехе; полюция только на ПАДАЮЩЕМ ассерте (в зелёном сьюте не случается). Рекомендация на будущее (не отдельный раунд): добавить autouse-фикстуру в conftest, чистящую api_server._download_failures перед каждым тестом — ровно как существующая _reset_media_mime_cache для _mime_types; это захардит весь selfheal-сьют от полюции этого модульного стейта.

DO/ESCALATE нет. Обе правки раунда 1 закрыты корректно и мутационно-держатся; impl не менялся; гейт зелёный. Approve.

9 аспектов. Обе новые проверки адверсариально верифицированы мной (мутация backoff-min + callback-reraise) + независимо двумя субагентами; impl байт-идентичен раунду 1. Test-pollution-на-падении — below-threshold (тест зелёный и само-чистится), в DROP с рекомендацией autouse-фикстуры. Escalate нет.

<!-- state:review reviewed_head=ffdc39e7 round=2 verdict=approved --> ## Ревью (раунд 2) — #48 `fix(media): follow-up self-heal — чистка негативкэша на рестарте + LRU-кап` **Вердикт: PASS** ✅ — обе правки раунда 1 закрыты. Полный веер из 9 аспектов по всему диффу PR. Гейт зелёный: `pytest tests/ -p no:cacheprovider` → **581 passed** (+2 к раунду 1). Дельта раунда 2 — ТЕСТ-ОНЛИ (impl `api_server.py`/`telegram_client.py` байт-идентичны раунду 1); обе новые проверки адверсариально подтверждены. ### Закрытие правок раунда 1 (мутационно подтверждено) - **[test-coverage] backoff-кап покрыт** — `test_backoff_capped_at_max`: записывает 8 фейлов (сырая экспонента 60·2⁷=7680с), ассертит что армленный дедлайн ≤ `_DOWNLOAD_BACKOFF_MAX` (600с) И прижат к капу (>595с). Мутация (убрать `min()` на api_server.py:163) → тест падает (7680>600). Я перепроверил сам. - **[test-coverage] best-effort колбэка покрыт** — `test_restart_callback_error_does_not_abort`: регистрирует бросающий колбэк, гонит ВЕРИФИЦИРОВАННЫЙ рестарт, ассертит что recovery завершилась (watchdog пере-армлен, SIGTERM-фолбэка нет, `_restarting=False`) и ошибка залогирована. Мутация (снять try/except с колбэка → re-raise) → тест падает (recovery падает в SIGTERM). Я перепроверил сам. <details> <summary>⛔ DROP — кодеру НЕ делать (лог калибровки)</summary> - `[below-threshold]` `med/high` **[test-coverage]** `test_backoff_capped_at_max` чистит `_download_failures` (стр. 287) БЕЗ try/finally — если ассерт упадёт до клинапа, модульный dict остаётся грязным и может полить соседние тесты (сосед `test_download_key_consistent_across_paths` использует try/finally). НЕ дефект: тест ПРОХОДИТ и чистит за собой на успехе; полюция только на ПАДАЮЩЕМ ассерте (в зелёном сьюте не случается). Рекомендация на будущее (не отдельный раунд): добавить autouse-фикстуру в conftest, чистящую `api_server._download_failures` перед каждым тестом — ровно как существующая `_reset_media_mime_cache` для `_mime_types`; это захардит весь selfheal-сьют от полюции этого модульного стейта. </details> DO/ESCALATE нет. Обе правки раунда 1 закрыты корректно и мутационно-держатся; impl не менялся; гейт зелёный. Approve. <sub>9 аспектов. Обе новые проверки адверсариально верифицированы мной (мутация backoff-min + callback-reraise) + независимо двумя субагентами; impl байт-идентичен раунду 1. Test-pollution-на-падении — below-threshold (тест зелёный и само-чистится), в DROP с рекомендацией autouse-фикстуры. Escalate нет.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-10 05:07:44 +03:00
vvzvlad merged commit e6d88b63cb into main 2026-07-10 05:08:16 +03:00
Sign in to join this conversation.