Стадия 2 — статика: атомарные скачивания больших видео, FloodWait→429, семафор с таймаутом #2

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

Цель

Убрать флаки отдачи /media/...: невозможность отдать клиенту неполный файл, честный 429 на флуд вместо 404, ограниченное ожидание семафора.

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

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

  • Видео >100MB качается напрямую в финальное имя temp_{fid} (api_server.py:379-396): параллельный Range-запрос браузерного плеера видит частичный файл и отдаёт его; при таймауте (120с на >100MB — заведомо мало) обрезок НЕ удаляется и целый час отдаётся как «готовый». Это основной источник «то работает, то нет».
  • except errors.RPCError → 404 (api_server.py:1001-1003): FloodWait — подкласс RPCError → при флуде медиа массово отдаёт 404, ридеры кэшируют «битые» картинки.
  • Свипер удаляет temp_* по mtime>1ч — файл удаляется «из-под» зрителя длинного видео.
  • HTTP_DOWNLOAD_SEMAPHORE(3): хвост запросов ждёт минутами без ответа.

Задачи

  • Хелпер _download_atomic(file_id, final_path, timeout): скачивание в {final}.part.{uuid.hex} → валидация размера → os.rename (atomic) → в finally всегда удалять .part. (таймаут/отмена/проигрыш гонки)
  • Использовать хелпер для ОБОИХ путей (обычные файлы и большие видео; финальное имя больших остаётся temp_{fid})
  • safe_download_media: параметр timeout; для больших — min(1800, max(120, size // (256*1024))), env-конфигурируемо
  • Проверку «temp существует → отдаём» оставить: теперь файл с финальным именем гарантированно полный
  • In-flight registry: dict[(ch,pid,fid), asyncio.Future] — дедуп конкурентных скачиваний
  • except errors.FloodWait в get_media до except errors.RPCError → 429 + Retry-After: min(e.value + jitter, 300)
  • Touch mtime (os.utime) при каждой отдаче temp_*-файла
  • Семафор: явный acquire под wait_for(30) → таймаут = 503 + Retry-After: 30
  • Regex свипера: новый суффикс .part.{hex} + оставить старый .tmp.{hex} на переходный период
  • Тесты: конкурентные запросы большого видео на медленном фейк-клиенте (второй НЕ получает частичный файл); таймаут → .part. удалён; FloodWait → 429; свипер чистит оба суффикса

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

  • In-flight future — классическая ловушка: качать в detached asyncio.create_task, а НЕ в корутине первого HTTP-запроса (отвал его клиента отменит скачивание и повесит всех ожидающих). В finally таска — set_result/set_exception И pop ключа, иначе после первой ошибки ключ занят навсегда.
  • Порядок except-веток: FloodWait — подкласс RPCError, поставишь после — не сработает никогда.
  • После правки grep-ом убедиться, что download_media нигде не получает финальный путь напрямую (инвариант «финальное имя = полный файл»).
  • Первый запрос большого видео по-прежнему ждёт полного скачивания — осознанно; прогрессивный стриминг через stream_media — отдельная будущая фича.

DoD

Неполный файл не может быть отдан ни при каком сценарии; флуд = 429; обрезки живут < 1 ч; активно просматриваемые файлы не удаляются.

Зависимости: стадия 1 (п. 1.3 — FloodWait в воркере).

## Цель Убрать флаки отдачи `/media/...`: невозможность отдать клиенту неполный файл, честный 429 на флуд вместо 404, ограниченное ожидание семафора. Полный план: [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) ## Контекст (что чиним) - Видео >100MB качается **напрямую в финальное имя** `temp_{fid}` (api_server.py:379-396): параллельный Range-запрос браузерного плеера видит частичный файл и отдаёт его; при таймауте (120с на >100MB — заведомо мало) обрезок НЕ удаляется и целый час отдаётся как «готовый». Это основной источник «то работает, то нет». - `except errors.RPCError → 404` (api_server.py:1001-1003): `FloodWait` — подкласс `RPCError` → при флуде медиа массово отдаёт 404, ридеры кэшируют «битые» картинки. - Свипер удаляет `temp_*` по mtime>1ч — файл удаляется «из-под» зрителя длинного видео. - `HTTP_DOWNLOAD_SEMAPHORE(3)`: хвост запросов ждёт минутами без ответа. ## Задачи - [ ] Хелпер `_download_atomic(file_id, final_path, timeout)`: скачивание в `{final}.part.{uuid.hex}` → валидация размера → `os.rename` (atomic) → в `finally` всегда удалять `.part.` (таймаут/отмена/проигрыш гонки) - [ ] Использовать хелпер для ОБОИХ путей (обычные файлы и большие видео; финальное имя больших остаётся `temp_{fid}`) - [ ] `safe_download_media`: параметр `timeout`; для больших — `min(1800, max(120, size // (256*1024)))`, env-конфигурируемо - [ ] Проверку «temp существует → отдаём» оставить: теперь файл с финальным именем гарантированно полный - [ ] In-flight registry: `dict[(ch,pid,fid), asyncio.Future]` — дедуп конкурентных скачиваний - [ ] `except errors.FloodWait` в `get_media` **до** `except errors.RPCError` → 429 + `Retry-After: min(e.value + jitter, 300)` - [ ] Touch mtime (`os.utime`) при каждой отдаче `temp_*`-файла - [ ] Семафор: явный `acquire` под `wait_for(30)` → таймаут = 503 + `Retry-After: 30` - [ ] Regex свипера: новый суффикс `.part.{hex}` + **оставить** старый `.tmp.{hex}` на переходный период - [ ] Тесты: конкурентные запросы большого видео на медленном фейк-клиенте (второй НЕ получает частичный файл); таймаут → `.part.` удалён; FloodWait → 429; свипер чистит оба суффикса ## Где не наебаться - **In-flight future — классическая ловушка**: качать в detached `asyncio.create_task`, а НЕ в корутине первого HTTP-запроса (отвал его клиента отменит скачивание и повесит всех ожидающих). В finally таска — `set_result`/`set_exception` И `pop` ключа, иначе после первой ошибки ключ занят навсегда. - Порядок except-веток: FloodWait — подкласс RPCError, поставишь после — не сработает никогда. - После правки grep-ом убедиться, что `download_media` нигде не получает финальный путь напрямую (инвариант «финальное имя = полный файл»). - Первый запрос большого видео по-прежнему ждёт полного скачивания — осознанно; прогрессивный стриминг через `stream_media` — отдельная будущая фича. ## DoD Неполный файл не может быть отдан ни при каком сценарии; флуд = 429; обрезки живут < 1 ч; активно просматриваемые файлы не удаляются. **Зависимости**: стадия 1 (п. 1.3 — FloodWait в воркере).
Sign in to join this conversation.