perf(media): хелпер путей медиа-кеша + in-memory MIME-кеш на горячем пути /media #46
Reference in New Issue
Block a user
Delete Branch "fix/26-media-hotpath"
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
Пакет E: хелпер путей медиа-кеша + in-memory MIME-кеш на горячем пути
/media.MEDIA_CACHE_DIR(резолвится один раз при импорте) +media_cache_path(channel, post_id, fid=None)→<root>/<channel>/<post_id>[/<fid>]— единый источник истины формата пути (был неявный контракт в 7 местах). Заменены все ручныеos.path.abspath("./data/cache")+join; результирующие строки путей дословно идентичны (drop-in, подтверждено внутренним ревью по каждому месту).remove_old_cached_files_sync/download_new_filesсохраняют параметрcache_dir(тестируемость)._mime_types(bound_MIME_CACHE_MAX=50000,clear()при переполнении) перед SQLite вprepare_file_response. Порядок: dict → SQLite → python-magic; SQLite-хит и magic-детект наполняют dict. MIME поfile_unique_idиммутабелен.How verified
.venv/bin/python -m pytest tests/ -q→ 456 passed (+4 новых вtest_stage_e_media_hotpath.py).file_io._open_dbсчётчиком — два последовательных/media-запроса к одному файлу, на втором счётчик НЕ растёт (MIME из dict, access-time уже в аккумуляторе). Тест зелёный.monkeypatch.setattr(api_server, "MEDIA_CACHE_DIR", ...)там, где тест сеет реальный файл кеша (путь теперь import-time); assertions не ослаблены.Внутренний цикл: coder → внутреннее ревreview (дословность 7 путей + MIME-порядок/overflow + валидность acceptance-теста) → APPROVE, багов нет.
Checklist
Задание 15: MEDIA_CACHE_DIR (резолвится один раз при импорте) + media_cache_path( channel, post_id, fid=None) -> <root>/<channel>/<post_id>[/<fid>] как единый источник истины формата пути. Заменены все ручные сборки os.path.abspath('./data/ cache')+join (7 мест); строки путей дословно идентичны (drop-in). remove_old_ cached_files_sync/download_new_files сохраняют параметр cache_dir (тестируемость). Задание 16: in-memory _mime_types (bound _MIME_CACHE_MAX=50000, clear() при переполнении) перед SQLite в prepare_file_response. Порядок: dict -> SQLite -> python-magic; SQLite-хит и magic-детект наполняют dict. MIME по file_unique_id иммутабелен -> повторный /media-хит не открывает SQLite-соединение (ни на чтение MIME, ни на запись access-time — она уже в аккумуляторе). Кэшируются только реальные типы. closes #26 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Ревью — #46
perf(media): хелпер путей медиа-кеша + in-memory MIME-кеш на горячем пути /mediaВердикт: ПРАВКИ (CHANGES) — 2 правки, обе тест-онли.
Полный веер из 9 аспектов по всему диффу PR (merge-base
3c9ce72b51..7e17b6e5b8). Гейт зелёный:pytest tests/ -p no:cacheprovider→ 456 passed, exit 0. Само перф-изменение корректно и покрыто мутационно-держащими тестами (layout-хелпер, dict-hit минует SQLite, overflow-clear — все три мутации ловятся). Но покрытие неполно по двум местам, и обе правки я проверил напрямую (чтение кода + переживающая мутация).Правки (сделать, потом ре-ревью)
[test-coverage] Не покрыт magic-detect путь (dict-write на холодном старте) —
tests/test_stage_e_media_hotpath.pyВсе 4 теста мокают
get_mime_type_syncтак, что он возвращает значение (dict-miss → SQLite-hit). Путь «dict-miss → SQLite-miss →magic_mime.from_file→ запись в ОБА кеша (SQLite + dict)» не выполняется ни одним тестом. Проверено мутацией: закомментировал ТОЛЬКО magic-path dict-write (api_server.py:499) и прогнал весь набор → 456 passed, ничего не упало — строка не защищена. При этом docstring самого тест-файла (пункт (c)) заявляет этот путь как покрытый. Fix: добавь тест, который сидит БЕЗ строки в SQLite для media_key, мокаетmagic_mime.from_file→ напр."image/jpeg", делает запрос через клиент и ассертит, что вызванset_mime_type_syncИapi_server._mime_types[key] == "image/jpeg".[coherence] Два chdir-теста не патчат
MEDIA_CACHE_DIR— латентная зависимость от реальногоdata/cache—tests/test_stage7_integration.py:220,251test_get_media_concurrent_shares_one_download_and_drains_registry(:220) иtest_get_media_request_cancel_does_not_stick_registry_or_hang_sibling(:251) делаютmonkeypatch.chdir(tmp_path), но НЕ ставятmonkeypatch.setattr(api_server, "MEDIA_CACHE_DIR", ...)— в отличие от 4 соседних тестов (:76,:94,:110,:304). Раньше pre-semaphore cache-check звалos.path.abspath("./data/cache")в момент вызова → под chdir резолвился в tmp_path (изолированно). После PRMEDIA_CACHE_DIRфиксируется на импорте → эти два теста читают РЕАЛЬНЫЙdata/cacheрепозитория. Сейчас проходят лишь потому, что там нет файла по ключуchan/9/vfid; если он появится (size>0), pre-semaphore hit отдаст файл,callsостанется пустым иassert calls == ["vfid"]упадёт. Fix: добавьmonkeypatch.setattr(api_server, "MEDIA_CACHE_DIR", str(tmp_path / "data" / "cache"))сразу послеchdirв обоих тестах, как в 4 соседних.Что чисто (клик)
os.path.join(MEDIA_CACHE_DIR, str(ch), str(pid), fid)≡ старомуos.path.join(os.path.abspath("./data/cache"), …)при стабильном CWD (abspath("./data/cache")==abspath(os.path.join("data","cache"))). Все 5 старых мест переведены (2 →media_cache_path, 3 →MEDIA_CACHE_DIR). Продакшн CWD после импорта не меняет — резолв на импорте корректен и даже строже (все места согласованы)._mime_types: гонок нет (GIL,get/setатомарны; двойной miss+fetch идемпотентен — один ключ, одно значение; двойной overflow-clear безвреден). Write-through на обоих путях популяции. Ключ формы(str(channel), post_id, fid)совпадает с SQLite-выборкой.conftestautouse-фикстура чистит dict между тестами.⛔ DROP — кодеру НЕ делать (лог калибровки)
[below-threshold]low/med[simplification] два идентичныхif len(_mime_types) >= _MIME_CACHE_MAX: _mime_types.clear()блока можно вынести в_remember_mime(key,val)— на hot-path инлайн приемлем, аспект сам оценил «no defect».Вне scope — кандидат на отдельную задачу (НЕ часть этого вердикта)
file_unique_id/channel—api_server.py:1278(media_cache_path). Starlette матчит{file_unique_id}как[^/]+по СЫРОМУ пути, затем unquote'ит →..%2F..%2F…проходит как один сегмент и раскрывается в../../…,os.path.joinуводит изMEDIA_CACHE_DIR. Роут без digest (/media/{channel}/{post_id}/{file_unique_id}, :1258) — потенциально без аутентифай-гейта. Pre-existing и байт-идентично старому инлайн-коду — #46 это НЕ вносит и не расширяет, поэтому вне scope этого PR. Но риск (возможное неаутентифицированное чтение произвольного файла) реальный — стоит отдельной задачи с проверкой эксплойтабельности (роутинг %2F + гейт без-digest роута). Не подтверждал эксплойт эмпирически.9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence). Test-isolation находку поднял coherence (stability по этому месту дал ложный «все компенсируют» — проверил напрямую, coherence прав). Обе правки тест-онли; перф-логика корректна. Escalate нет.
Поправил обе тест-онли правки.
[test-coverage] magic-detect cold-start путь не покрыт — добавлен
test_magic_cold_start_writes_through_to_sqlite_and_dict: dict-miss → SQLite-miss (get_mime_type_sync→None) →magic_mime.from_file→"image/jpeg"→ ассерт write-through в ОБА кеша (set_mime_type_syncвызван И_mime_types[key]=="image/jpeg"). Мутация: закомментировать dict-write на magic-пути (api_server.py:499) → новый тест краснеет (KeyError). Зубы есть.[coherence] Два chdir-теста не патчили MEDIA_CACHE_DIR — добавил
monkeypatch.setattr(api_server, "MEDIA_CACHE_DIR", str(tmp_path/"data"/"cache"))послеchdirв обоих (:220,:251), как в 4 соседних тестах. Убрал латентную зависимость от реальногоdata/cacheрепо.Только тесты, прод-код не тронут (
git diff --stat api_server.pyпусто).pytest tests/ -q→ 457 passed.Отдельно (вне scope #46, как ты и отметил): path-traversal через %2F в
/media/{...}/{file_unique_id}— pre-existing, байт-идентично старому инлайну, #46 это не вносит. Поднял вопрос отдельной задачи vvzvlad'у (потенциально неаутентифицированное чтение файла — стоит проверки эксплойтабельности). Готово к повторному ревью.Ревью (раунд 2) — #46
perf(media): хелпер путей медиа-кеша + in-memory MIME-кеш на горячем пути /mediaВердикт: PASS ✅ — обе правки раунда 1 закрыты.
Полный веер из 9 аспектов по всему диффу PR (merge-base
3c9ce72b51..f559d39e33). Гейт зелёный:pytest tests/ -p no:cacheprovider→ 457 passed (+1 к раунду 1). Дельта раунда 2 — тест-онли (api_server.pyне менялся), обе правки проверил адверсариально.Закрытие правок раунда 1 (проверено мутационно)
test_magic_cold_start_writes_through_to_sqlite_and_dict: мокаетget_mime_type_sync→None(SQLite-miss) →magic→"image/jpeg"→ ассертит вызовset_mime_type_syncИ_mime_types[key]=="image/jpeg"через реальныйprepare_file_response. Мутация подтверждена: закомментировал ТОЛЬКО magic-path dict-write (api_server.py:499) → тест падаетKeyError(в раунде 1 эта строка мутацию переживала — теперь ловится); закомментировалset_mime_type_sync→ падаетcalls["set"]==1.MEDIA_CACHE_DIR—test_get_media_concurrent_shares_one_download_and_drains_registry(:221) иtest_get_media_request_cancel_does_not_stick_registry_or_hang_sibling(:253) теперь ставятmonkeypatch.setattr(api_server, "MEDIA_CACHE_DIR", str(tmp_path/"data"/"cache"))сразу послеchdir, как 4 соседних. Латентная зависимость от реальногоdata/cacheустранена.Остальное (клик)
Перф-логика (dict write-through перед SQLite,
media_cache_path, import-timeMEDIA_CACHE_DIR) не менялась с раунда 1 и остаётся корректной: гонок нет (нет await между check/insert; двойной populate идемпотентен), путь байт-идентичен старым инлайн-join'ам, ключ формы(str(channel),post_id,fid)совпадает с SQLite. Архитектура/упрощение/conventions/documentation/security — без изменений, LGTM. magic-path overflow-guard напрямую не тестируется, но байт-идентичен уже покрытому SQLite-path overflow — риск пренебрежим (DROP).Вне scope (без изменений с раунда 1, НЕ часть вердикта)
file_unique_id/channel(media_cache_path) — pre-existing, байт-идентично старому коду, #46 не вносит. Кандидат на отдельную задачу (см. раунд 1). Не блокирует.DO/ESCALATE нет. Обе правки раунда 1 закрыты и мутационно-подтверждены; гейт зелёный. Approve.
9 аспектов. Обе правки раунда 1 проверены адверсариально (реплей мутаций, что находили дыры) — независимо мной и субагентами. Escalate нет.