Files
pyrogram-bridge/docs/stability-verification.md
T
agent_coder 3a133c349d docs(stability): stage 7 review round 1 — correct rollback granularity to per-stage
Review finding (low, doc-only): the rollback plan said each stage is one
independently-revertable commit, but each stage is actually two commits on its
branch (feature + review-round fix, the latter often fixing a real bug), so
reverting a single commit would orphan the review-round fix. Reword the unit of
rollback to the whole STAGE (branch/PR) and spell out the revert command per merge
strategy: squash-merge -> revert the one squash commit; merge-commit -> revert -m 1
the merge; linear history -> revert the full range of the stage's commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 11:17:49 +03:00

15 KiB

Стадия 7 — сквозная верификация плана стабилизации

Дата: 2026-07-05. Ветка: fix/stage-7-verification (на базе fix/stage-6-healthcheck, кумулятивно содержит стадии 1–6). Эта стадия ничего не меняет в поведении прод-кода — только автоматизирует проверки и фиксирует, что должен наблюдать оператор после деплоя.

Итог автотестов

Каноничный прогон (изоляция config через sys.modules): python -m pytest tests/.

260 passed

Раскладка по стадиям (все зелёные):

Стадия Файл тестов Кол-во Что покрывает
1 — анти-зависания tests/test_stage1_hangs.py 6 таймаут RPC-гейта без утечки пермита; воркер переживает Exception/FloodWait, task_done сбалансирован; отмена в spacing-ожидании не теряет пермит
2 — статика/большие видео tests/test_stage2_static.py 15 атомарный _download_atomic (publish-on-rename, чистка .part. при таймауте/zero-size/гонке); дедуп конкурентных скачиваний; FloodWait→429; touch mtime у temp_*; свипер чистит .part.+legacy .tmp.; баланс HTTP-семафора
3 — FileResponse tests/test_stage3_fileresponse.py 19 матрица Range (0-499/500-/-500/за EOF→416/мусор→400/мульти-range→206 multipart); сохранены mtime-touch, delete_after-BackgroundTask, MIME-кэш; чистый ASGI-логгер
4 — гигиена event loop tests/test_stage4_eventloop.py 19 ленивый raw_message; вынос side-effect IO из process_message (bulk upsert media id); рендер-пайплайн ушёл в поток без create_task/get_running_loop; XSS вычищен во всех 4 выводах ровно одним проходом
5 — батчинг SQLite tests/test_stage5_sqlite.py 8 кэш-хит пишет в аккумулятор, а не в SQLite; flush→DB; snapshot-then-clear не теряет поздние апдейты; re-queue при сбое; str(channel)-ключи
6 — healthcheck tests/test_stage6_healthcheck.py 11 /ping 200/503 по connected+age; ноль TG RPC; watchdog отключён→чистая проверка коннекта; watchdog_last_ok_age()
7 — интеграция tests/test_stage7_integration.py 8 сквозные сценарии (см. ниже)
— регрессия парсера tests/test_postparser_*.py 174 заголовки/флаги/автор (существовавшие до плана)

Новые интеграционные тесты стадии 7 (DoD → сквозное доказательство)

tests/test_stage7_integration.py драйвит реальные точки входа (get_media, ping, flush), чтобы поймать регрессии, которые проявляются только при взаимодействии стадий:

  1. Range на уровне маршрута /mediatest_media_route_range_prefix_0_99, _range_suffix_last_100, _range_unsatisfiable_416. Прогоняют get_media (кэш-хит) → prepare_file_responseFileResponse через реальный ASGI: bytes=0-99→206 с точным Content-Range/длиной и байтами, bytes=-100→206 (суффикс), bytes=999999999-→416 (*/size). Ловит: если кэш-хит перестанет доходить до FileResponse (ре-буферизация тела, ручные заголовки) или сломается связка digest-гейта — Range перестанет работать. (Стадия 3 пинит это на prepare_file_response напрямую; здесь — сшивка стадий 2+3.)
  2. /ping быстр и без RPC при висящей операцииtest_ping_prompt_and_rpc_free_while_slow_op_pending и _reports_degraded_promptly_while_slow_op_pending. Пока фейковая медленная корутина (модель зависшего hot-path) висит на Event, ping() возвращает 200/503 корректно и до завершения медленной операции, при нуле вызовов get_me/safe_get_messages. Ловит: рекаплинг /ping к TG RPC или к любому awaitable, который может застопориться.
  3. Дедуп + очистка при disconnect через реальный get_mediatest_get_media_concurrent_shares_one_download_and_drains_registry (2 конкурентных запроса → одна скачка, _inflight пуст) и _request_cancel_does_not_stick_registry_or_hang_sibling (отмена запроса-«клиента» не оставляет застрявший ключ и не вешает соседа). Ловит: возврат скачивания в корутину запроса (отмена убила бы download) или потерю finally-pop.
  4. str(channel)-ключ access-time end-to-endtest_media_cache_hit_flush_updates_str_keyed_db_row. Кэш-хит /media записывает str-ключ, flush обновляет ту же строку в реальной SQLite (hit→flush→DB). Сшивает две половины: ключ, который пишет hot-path, и WHERE, по которому апдейтит flush. Ловит любое расхождение ключа аккумулятора и WHERE bulk-UPDATE (напр. перестановку колонок в SQL — проверено мутацией: тест краснеет, added остаётся stale). (int/str-хазард самого канала живёт в download_media_file и закрыт стадией 5 — test_cache_hit_keys_channel_as_str; здесь покрыта связка get_media+flush.)

Соответствие DoD стадий → доказательство

  • DoD 1 «ни один путь не ждёт TG без таймаута; воркер не умирает молча» → test_stage1_hangs.py (гейт-таймаут, живучесть воркера). Живая проверка supervision под нагрузкой — наблюдение оператора (лог supervised_task_crashed/…_exited).
  • DoD 2 «клиенту никогда не отдаётся неполный файл; флуд→429; обрезки не живут >часа» → test_stage2_static.py целиком + интеграционный дедуп-тест стадии 7.
  • DoD 3 «Range-тесты зелёные; поведение эндпоинта эквивалентно (± RFC-допущения)» → test_stage3_fileresponse.py + Range на уровне /media (стадия 7). «Нет потока-на-чанк» — наблюдение оператора (нагрузочный запрос большого файла, число io-потоков).
  • DoD 4 «генерация 100-сообщ. фида не блокирует луп (параллельный /ping <100 мс); XSS зелёный; media id сохраняются» → test_stage4_eventloop.py (рендер в потоке, XSS, bulk upsert) + /ping-decoupling стадии 7. Живой замер «<100 мс на проде» — наблюдение оператора.
  • DoD 5 «на кэш-хит /media ноль обращений к SQLite; фоновая запись раз в минуту» → test_stage5_sqlite.py + str-ключ end-to-end стадии 7.
  • DoD 6 «во время зависшего TG RPC /ping отвечает мгновенно (503); контейнер не рестартится от медленного фида» → test_stage6_healthcheck.py + /ping-under-slow-op стадии 7. Отсутствие autoheal-рестартов на холодном кэше — наблюдение оператора.

Ручные curl-сценарии для оператора (пост-деплой, локально в контейнере)

Подставить реальный {channel}/{post_id}/{fid}/{digest} (валидная подпись обязательна).

# 1. Range-тройка (ожидания: 206 / 206 / 416)
curl -s -D- -o /dev/null -H "Range: bytes=0-99"      "http://127.0.0.1:80/media/{ch}/{pid}/{fid}/{digest}"
curl -s -D- -o /dev/null -H "Range: bytes=-100"       "http://127.0.0.1:80/media/{ch}/{pid}/{fid}/{digest}"
curl -s -D- -o /dev/null -H "Range: bytes=999999999-" "http://127.0.0.1:80/media/{ch}/{pid}/{fid}/{digest}"

# 2. Параллельные запросы одного БОЛЬШОГО видео (>100 MB), пока не в кэше:
#    оба должны получить ПОЛНЫЙ файл (одинаковый размер), без частичной отдачи.
URL="http://127.0.0.1:80/media/{ch}/{pid}/{bigfid}/{digest}"
curl -s -o /tmp/a "$URL" & curl -s -o /tmp/b "$URL" & wait
ls -l /tmp/a /tmp/b     # размеры совпадают и равны полному файлу; на диске нет *.part.*

# 3. Обрыв на середине стрима — нет утечки тасков/фд:
#    считать fd до/после и убедиться, что не растут монотонно.
lsof -p $(pgrep -f api_server) | wc -l          # baseline
timeout 2 curl -s -o /dev/null "$URL"; sleep 5  # оборвать скачку на середине (несколько раз)
lsof -p $(pgrep -f api_server) | wc -l          # не выросло относительно baseline

# 4. Генерация большого фида + параллельный /ping (<100 мс во время генерации):
curl -s -o /dev/null "http://127.0.0.1:80/rss/{big_channel}" &
for i in $(seq 1 20); do curl -s -o /dev/null -w "%{time_total}\n" "http://127.0.0.1:80/ping"; done
wait   # все замеры /ping должны быть заметно < 0.100 s

Сценарии 2 и 3 (реальная скачка большого видео + реальный подсчёт fd через lsof) в headless-тестах намеренно не подделаны — им нужен живой сервер, реальные загрузки и реальные файловые дескрипторы. Дедуп-инвариант и disconnect-очистка доказаны на уровне реестра/get_media (тесты стадии 7 №3), но «нет утечки fd на проде» проверяет оператор.

Diag-логи для наблюдения после деплоя

Ожидаемая динамика (сравнить с до-деплойным baseline):

  • diag_semaphore_wait — ожидание HTTP-семафора должно упасть (реже/меньше секунд).
  • diag_download_timing — время скачивания стабилизируется; нет длинных «зависаний».
  • diag_sanitize_slow — почти исчезнуть (один sanitize на выходную границу, стадия 4).
  • watchdog: heartbeatпродолжаются штатно (живость TG-сессии).
  • На момент FloodWait — нет всплеска 404 на /media; вместо этого media_flood_wait и ответы 429 с Retry-After (стадия 2.3).
  • Признаки supervision: любые supervised_task_crashed / supervised_task_exited на CRITICAL — сигнал разобраться, но задача при этом перезапускается (не тихая смерть).

План отката (rollback)

Единица отката — стадия целиком, не отдельный коммит. Каждая стадия живёт на своей ветке/PR (fix/stage-1-hangsfix/stage-7-verification) и, как правило, состоит из двух коммитов: feature-коммит + фикс review-раунда (последний нередко чинит реальный баг — напр. fail-closed XSS в стадии 4, watchdog-gate в стадии 6). Поэтому git revert одного коммита осиротит фикс review-раунда и даст несогласованный откат. Откатывать нужно на гранулярности стадии; порядок стадий = порядок деплоя.

  • Как откатывать стадию — зависит от того, как ветки влиты в fix/stability/main:
    • если стадия влита squash-merge (один коммит на стадию) — git revert <squash-commit> откатывает её целиком, однозначно;
    • если стадия влита merge-коммитомgit revert -m 1 <merge-commit> откатывает весь вклад ветки (оба коммита) разом;
    • если история линейная (rebase/fast-forward) — реверт всех коммитов стадии (git revert <feature>..<review-fix> включительно), а не одного.
  • Стадии 1 и 2 — низкорисковые, деплоятся первыми; откатываются независимо от остальных.
  • Стадия 3 (FileResponse) независима — реверт возвращает прежний ручной стриминг.
  • Стадия 4 требует, чтобы 4.2 откатывалась не позже 4.3 (иначе рендер-в-потоке остался бы без безопасного пути сохранения media id) — откатывать стадию 4 целиком.
  • Стадии 5 и 6 независимы, откатываются по отдельности.
  • Стадия 7 — только тесты и этот документ: реверт ничего не меняет в проде.

Прод-деплой, живое наблюдение diag-логов и обновление прод-compose (healthcheck→/ping, вне репозитория) — зона ответственности оператора (пункты 3–4 плана стадии 7).