fix(cache): свипер медиа-кеша — безусловный DB-diff + интервал в конфиг + дедуп очереди #45

Merged
agent_coder merged 1 commits from fix/25-media-sweeper into main 2026-07-10 03:29:55 +03:00
Collaborator

Summary

Пакет D спеки cache-optimization: фиксы свипера медиа-кеша + дедуп очереди фоновых загрузок.

  • Задание 12 (баг): диф+удаление строк из SQLite в cache_media_files считались только if files_removed > 0. Запись старше 20 дней БЕЗ файла на диске выпадала из remove_old_cached_files_sync (ветка continue без инкремента счётчика) и оставалась в БД навсегда, если в том же проходе не удалился ни один реальный файл. Теперь диф считается безусловно: removed_entries = старый набор минус выживший; purge при непустом removed_entries независимо от files_removed.
  • Задание 13: cache_sweep_interval (env CACHE_SWEEP_INTERVAL, дефолт 900, пол 60 — значение ниже отвергается на старте _parse_int_env) вместо хардкода delay=60 (полный скан таблицы + os.walk всего кеша каждую минуту — расточительно).
  • Задание 14: дедуп очереди фоновых загрузок через module-level _queued_media: enqueue пропускает уже-в-очереди ключ (str(channel),int(post_id),file_unique_id), discard в finally воркера (упавшая/затротленная загрузка переочередится позже), discard+break при QueueFull.

closes #25

How verified

  • .venv/bin/python -m pytest tests/ -q455 passed (+3 новых в test_media_sweeper.py: протухшая запись без файла при files_removed==0 исчезает за один проход; два подряд download_new_files → один элемент в очереди; ключ вычищается из _queued_media после воркера).
  • git diff --stat — только api_server.py, config.py, tests/mock_config.py + новый тест.

Внутренний цикл: coder → внутреннее ревью (семантика множеств задания 12 — инверсный баг проверен и не найден; консистентность ключа дедупа enqueue↔worker; отсутствие утечки ключа) → APPROVE, поправлен только неточный коммент про «clamp» (реально reject+exit).

Checklist

  • строка SQLite старше 20 дней с отсутствующим файлом исчезает из БД за один проход
  • pytest зелёный; scope — только заявленные файлы
## Summary Пакет D спеки cache-optimization: фиксы свипера медиа-кеша + дедуп очереди фоновых загрузок. - **Задание 12 (баг):** диф+удаление строк из SQLite в `cache_media_files` считались только `if files_removed > 0`. Запись старше 20 дней БЕЗ файла на диске выпадала из `remove_old_cached_files_sync` (ветка `continue` без инкремента счётчика) и оставалась в БД **навсегда**, если в том же проходе не удалился ни один реальный файл. Теперь диф считается безусловно: `removed_entries` = старый набор минус выживший; purge при непустом `removed_entries` независимо от `files_removed`. - **Задание 13:** `cache_sweep_interval` (env `CACHE_SWEEP_INTERVAL`, дефолт 900, пол 60 — значение ниже отвергается на старте `_parse_int_env`) вместо хардкода `delay=60` (полный скан таблицы + `os.walk` всего кеша каждую минуту — расточительно). - **Задание 14:** дедуп очереди фоновых загрузок через module-level `_queued_media`: enqueue пропускает уже-в-очереди ключ `(str(channel),int(post_id),file_unique_id)`, `discard` в `finally` воркера (упавшая/затротленная загрузка переочередится позже), `discard`+`break` при `QueueFull`. closes #25 ## How verified - `.venv/bin/python -m pytest tests/ -q` → **455 passed** (+3 новых в `test_media_sweeper.py`: протухшая запись без файла при `files_removed==0` исчезает за один проход; два подряд `download_new_files` → один элемент в очереди; ключ вычищается из `_queued_media` после воркера). - `git diff --stat` — только `api_server.py`, `config.py`, `tests/mock_config.py` + новый тест. Внутренний цикл: coder → внутреннее ревью (семантика множеств задания 12 — инверсный баг проверен и не найден; консистентность ключа дедупа enqueue↔worker; отсутствие утечки ключа) → APPROVE, поправлен только неточный коммент про «clamp» (реально reject+exit). ## Checklist - [x] строка SQLite старше 20 дней с отсутствующим файлом исчезает из БД за один проход - [x] pytest зелёный; scope — только заявленные файлы
agent_coder added 1 commit 2026-07-10 00:41:39 +03:00
Задание 12 (баг): диф+удаление строк из SQLite в cache_media_files считались только
if files_removed>0, поэтому протухшая (>20д) запись без файла на диске выпадала из
remove_old_cached_files_sync (ветка continue без инкремента счётчика) и оставалась в
БД навсегда. Диф теперь считается безусловно: removed_entries = старый набор минус
выживший, purge при непустом removed_entries независимо от files_removed.

Задание 13: cache_sweep_interval (env CACHE_SWEEP_INTERVAL, дефолт 900, пол 60 —
значение ниже отвергается на старте) по существующему _parse_int_env-паттерну;
delay=60 -> Config['cache_sweep_interval'].

Задание 14: дедуп очереди фоновых загрузок через module-level _queued_media
(set[tuple[str,int,str]]): enqueue пропускает уже-в-очереди ключ, discard в finally
воркера (упавшая/затротленная загрузка переочередится), discard+break при QueueFull.
Однопоточный loop, между проверкой и мутацией нет await -> гонок нет.

closes #25

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

Ревью — #45 fix(cache): свипер медиа-кеша — безусловный DB-diff + интервал в конфиг + дедуп очереди

Вердикт: PASS

Полный веер из 9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence). Гейт зелёный: pytest tests/ -p no:cacheprovider455 passed, exit 0. Три ключевых механизма подтверждены мутационными пробами (ревертишь фикс — падает соответствующий тест).

Что проверено по каждой задаче (клик)

Задание 12 — безусловный DB-diff (главный баг). Раньше purge SQLite шёл только внутри if files_removed > 0. Строка >20 дней, у которой файл на диске уже отсутствует, выбрасывается из updated_media_files в remove_old_cached_files_sync без инкремента files_removed → при files_removed == 0 diff не запускался и строка висела в БД вечно. Фикс: removed_entries = (строки media_files) − updated_set считается безусловно и purge при непустом наборе. Проверил независимо: over-purge структурно невозможен — живая строка (файл на месте) всегда попадает в updated_media_files → в updated_set → не в removed_entries. Если remove_old_cached_files_sync бросит, внешний try/except не даёт diff-у выполниться (нет ложного purge).

Задание 14 — дедуп очереди загрузок. Модульный _queued_media: set. Ключ на enqueue (str(channel), int(post_id), file_unique_id) байт-в-байт совпадает с bg_key в воркере → discard в finally всегда попадает по add, утечки слота нет. На QueueFull ключ снимается в except. Гонок нет: однопоточный event-loop, между in-проверкой и add нет await. Окно корректно — слот освобождается после обработки, всё ещё отсутствующий файл переедет в очередь на следующем свипе.

Задание 13 — интервал свипа в конфиг. delay = Config["cache_sweep_interval"] вместо хардкода 60. _parse_int_env("CACHE_SWEEP_INTERVAL", 900, minimum=60) — при значении ниже 60 или не-int sys.exit(1) на старте (пол подтверждён). Дефолт 900 намеренный (реже гонять скан таблицы + os.walk).


DROP — кодеру НЕ делать (лог калибровки, только для оператора)
  • [speculative] low/high [test-coverage] тест «свежая строка (файл на месте) не выпилена безусловным diff-ом» — over-purge структурно невозможен (живая строка всегда в updated_set); тест защищал бы гипотетическую будущую правку, а не дефект этого PR.
  • [below-threshold] low/med [test-coverage] тест «CACHE_SWEEP_INTERVAL ниже 60 → sys.exit» — пол реально работает (проверено), _parse_int_env — дешёвая общая обвязка конфига; рисковые пути (purge/дедуп) покрыты.
  • [speculative] low/med [test-coverage] тест discard-на-QueueFull — узкая ветка (очередь на maxsize 100), маргинально.

Замечаний уровня DO/ESCALATE нет — фикс чистый, корректный и покрыт тестами на трёх load-bearing путях. Approve.

<!-- state:review reviewed_head=f997cbef80 round=1 verdict=approved --> ## Ревью — #45 `fix(cache): свипер медиа-кеша — безусловный DB-diff + интервал в конфиг + дедуп очереди` **Вердикт: PASS** ✅ Полный веер из 9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence). Гейт зелёный: `pytest tests/ -p no:cacheprovider` → **455 passed, exit 0**. Три ключевых механизма подтверждены мутационными пробами (ревертишь фикс — падает соответствующий тест). <details> <summary>Что проверено по каждой задаче (клик)</summary> **Задание 12 — безусловный DB-diff (главный баг).** Раньше purge SQLite шёл только внутри `if files_removed > 0`. Строка >20 дней, у которой файл на диске уже отсутствует, выбрасывается из `updated_media_files` в `remove_old_cached_files_sync` **без** инкремента `files_removed` → при `files_removed == 0` diff не запускался и строка висела в БД вечно. Фикс: `removed_entries = (строки media_files) − updated_set` считается **безусловно** и purge при непустом наборе. Проверил независимо: over-purge **структурно невозможен** — живая строка (файл на месте) всегда попадает в `updated_media_files` → в `updated_set` → не в `removed_entries`. Если `remove_old_cached_files_sync` бросит, внешний try/except не даёт diff-у выполниться (нет ложного purge). **Задание 14 — дедуп очереди загрузок.** Модульный `_queued_media: set`. Ключ на enqueue `(str(channel), int(post_id), file_unique_id)` **байт-в-байт** совпадает с `bg_key` в воркере → `discard` в `finally` всегда попадает по `add`, утечки слота нет. На `QueueFull` ключ снимается в except. Гонок нет: однопоточный event-loop, между `in`-проверкой и `add` нет `await`. Окно корректно — слот освобождается после обработки, всё ещё отсутствующий файл переедет в очередь на следующем свипе. **Задание 13 — интервал свипа в конфиг.** `delay = Config["cache_sweep_interval"]` вместо хардкода 60. `_parse_int_env("CACHE_SWEEP_INTERVAL", 900, minimum=60)` — при значении ниже 60 или не-int `sys.exit(1)` на старте (пол подтверждён). Дефолт 900 намеренный (реже гонять скан таблицы + os.walk). </details> --- <details> <summary>⛔ DROP — кодеру НЕ делать (лог калибровки, только для оператора)</summary> - `[speculative]` `low/high` **[test-coverage]** тест «свежая строка (файл на месте) не выпилена безусловным diff-ом» — over-purge структурно невозможен (живая строка всегда в `updated_set`); тест защищал бы гипотетическую будущую правку, а не дефект этого PR. - `[below-threshold]` `low/med` **[test-coverage]** тест «CACHE_SWEEP_INTERVAL ниже 60 → sys.exit» — пол реально работает (проверено), `_parse_int_env` — дешёвая общая обвязка конфига; рисковые пути (purge/дедуп) покрыты. - `[speculative]` `low/med` **[test-coverage]** тест discard-на-QueueFull — узкая ветка (очередь на maxsize 100), маргинально. </details> Замечаний уровня DO/ESCALATE нет — фикс чистый, корректный и покрыт тестами на трёх load-bearing путях. Approve.
agent_reviewer added review/approved and removed review/needs labels 2026-07-10 01:06:13 +03:00
agent_coder merged commit 45e79f5b0e into main 2026-07-10 03:29:55 +03:00
Sign in to join this conversation.