Стадия 3 — отдача файлов через FileResponse, удаление BaseHTTPMiddleware, расширение executor #3

Closed
opened 2026-07-05 04:01:56 +03:00 by agent_vscode · 0 comments
Collaborator

Цель

Заменить самодельный стриминг (ручной парсинг Range + asyncio.to_thread на каждые 64KB) на встроенный FileResponse Starlette; убрать оверхед BaseHTTPMiddleware; расширить дефолтный threadpool.

Полный план: plans/2026-07-05-stability-fix-plan.md

Контекст (что чиним)

  • prepare_file_response (api_server.py:203-335): свой парсер Range + генератор, где каждый 64KB-чанк — отдельный to_thread с open/seek/read/close. Отдача 500MB видео = ~8000 прыжков в threadpool, конкурирующих с SQLite/magic/pickle/os.walk за 5-6 потоков дефолтного executor в контейнере → стриминг замирает.
  • Starlette 0.45.3 (проверено по venv) поддерживает Range/206/416/If-Range в FileResponse из коробки.
  • RequestLoggingMiddleware на BaseHTTPMiddleware (api_server.py:54-64) — каждый чанк каждого стрима проходит через anyio memory stream ради debug-лога.

Задачи

  • Сначала httpx TestClient-тесты на текущую Range-семантику: bytes=0-499, bytes=-500, bytes=500-, start за EOF → 416, мусорный заголовок
  • Заменить тело prepare_file_response на FileResponse(path, media_type=..., filename=..., content_disposition_type="inline", headers={"Cache-Control": "public, max-age=86400, immutable"}, background=...)
  • Удалить ручной парсинг Range, file_chunk_generator, ручные заголовки; оставить пре-чек существования (404) и MIME-логику (magic + SQLite-кэш)
  • Удалить RequestLoggingMiddleware; при необходимости — чистый ASGI-middleware на ~10 строк или access-log uvicorn
  • В lifespan: loop.set_default_executor(ThreadPoolExecutor(max_workers=32, thread_name_prefix="io"))
  • Прогнать Range-тесты, осознанно принять расхождения семантики

Где не наебаться

  • Тесты ДО свапа, свап ПОСЛЕ: расхождения принимать осознанно (например, на кривой Range-заголовок Starlette может отдать 200-полный вместо 416 — это валидно по RFC 7233).
  • FileResponse проверяет файл на этапе отправки (ошибка → 500) — пре-чек на 404 оставить обязательно.
  • Content-Disposition с filename*=UTF-8''... FileResponse формирует сам — руками не собирать, иначе задвоится.
  • FileResponse ставит ETag/Last-Modified — убедиться, что они не конфликтуют с Cache-Control: immutable (не конфликтуют, но проверить в тестах, что 304 работает разумно).

DoD

Range-тесты зелёные; отдача большого файла не порождает поток-на-чанк; поведение эндпоинта эквивалентно (кроме осознанных RFC-допущений).

Зависимости: лучше после стадии 2 (меньше конфликтов в download-путях), но технически независима.

## Цель Заменить самодельный стриминг (ручной парсинг Range + `asyncio.to_thread` на каждые 64KB) на встроенный `FileResponse` Starlette; убрать оверхед `BaseHTTPMiddleware`; расширить дефолтный threadpool. Полный план: [plans/2026-07-05-stability-fix-plan.md](https://gitea.vvzvlad.xyz/vvzvlad/pyrogram-bridge/src/branch/fix/stability/plans/2026-07-05-stability-fix-plan.md) ## Контекст (что чиним) - `prepare_file_response` (api_server.py:203-335): свой парсер Range + генератор, где каждый 64KB-чанк — отдельный `to_thread` с open/seek/read/close. Отдача 500MB видео = ~8000 прыжков в threadpool, конкурирующих с SQLite/magic/pickle/os.walk за 5-6 потоков дефолтного executor в контейнере → стриминг замирает. - Starlette 0.45.3 (проверено по venv) поддерживает Range/206/416/If-Range в `FileResponse` из коробки. - `RequestLoggingMiddleware` на `BaseHTTPMiddleware` (api_server.py:54-64) — каждый чанк каждого стрима проходит через anyio memory stream ради debug-лога. ## Задачи - [ ] **Сначала** httpx TestClient-тесты на текущую Range-семантику: `bytes=0-499`, `bytes=-500`, `bytes=500-`, start за EOF → 416, мусорный заголовок - [ ] Заменить тело `prepare_file_response` на `FileResponse(path, media_type=..., filename=..., content_disposition_type="inline", headers={"Cache-Control": "public, max-age=86400, immutable"}, background=...)` - [ ] Удалить ручной парсинг Range, `file_chunk_generator`, ручные заголовки; оставить пре-чек существования (404) и MIME-логику (magic + SQLite-кэш) - [ ] Удалить `RequestLoggingMiddleware`; при необходимости — чистый ASGI-middleware на ~10 строк или access-log uvicorn - [ ] В lifespan: `loop.set_default_executor(ThreadPoolExecutor(max_workers=32, thread_name_prefix="io"))` - [ ] Прогнать Range-тесты, осознанно принять расхождения семантики ## Где не наебаться - Тесты ДО свапа, свап ПОСЛЕ: расхождения принимать осознанно (например, на кривой Range-заголовок Starlette может отдать 200-полный вместо 416 — это валидно по RFC 7233). - `FileResponse` проверяет файл на этапе отправки (ошибка → 500) — пре-чек на 404 оставить обязательно. - `Content-Disposition` с `filename*=UTF-8''...` FileResponse формирует сам — руками не собирать, иначе задвоится. - FileResponse ставит ETag/Last-Modified — убедиться, что они не конфликтуют с `Cache-Control: immutable` (не конфликтуют, но проверить в тестах, что 304 работает разумно). ## DoD Range-тесты зелёные; отдача большого файла не порождает поток-на-чанк; поведение эндпоинта эквивалентно (кроме осознанных RFC-допущений). **Зависимости**: лучше после стадии 2 (меньше конфликтов в download-путях), но технически независима.
Sign in to join this conversation.