Стадия 4 — гигиена event loop: рендер фидов в поток, lazy raw_message, один sanitize на выход #4

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

Цель

Event loop не должен выполнять CPU-работу > ~50 мс: во время генерации фида сейчас замирает всё приложение, включая стриминг медиа.

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

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

  • copy.deepcopy сотен Message-объектов на лупе (rss_generator.py:38, активен при TIME_BASED_MERGE=True — включено в проде).
  • process_message синхронно на лупе для каждого сообщения (rss_generator.py:211-230), внутри — 4+ вызова bleach на сообщение (post_parser.py:672, 752, 833, 920; есть диагностика diag_sanitize_slow >50мс/вызов) и str(message) — полная сериализация каждого поста (post_parser.py:523).
  • На 100 сообщениях — секунды блокировки лупа на каждый poll фида.

Задачи (строгий порядок: 4.2 → 4.3!)

  • 4.1 process_message(..., include_raw: bool = False): str(message) только для JSON-выдачи и debug-HTML, в фидах — никогда
  • 4.2 Убрать side-effect IO из process_message: _save_media_file_ids (post_parser.py:977-1028) больше не делает get_running_loop()/create_task, а копит (channel, post_id, fid, ts) в self._pending_media_ids; после рендера вызывающий код один раз делает await asyncio.to_thread(upsert_media_file_ids_bulk_sync, ...) — новая функция в file_io.py с executemany. Счётчик _persist_pending_count удалить
  • 4.3 _create_time_based_media_groups / _create_messages_groups / _trim_messages_groups / _render_messages_groups — внутри нет ни одного await → превратить в sync-функции и выполнять одним to_thread из generate_channel_rss/generate_channel_html (deepcopy уезжает туда же)
  • 4.4 Один sanitize на выходную границу: фиды — финальный проход уже есть (rss_generator.py:443, 643) → внутренние per-message проходы убрать; /html/{ch}/{id} — добавить единственный проход в _format_html; /json — один проход по body+footer в process_message
  • debug-HTML: raw_message в <pre> прогнать через html.escape
  • Тесты: XSS (<script>, onerror=, javascript: → вычищено во всех трёх выводах); media id сохраняются после рендера (bulk upsert); deepcopy Message из pickle-кэша не падает в потоке

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

  • Главная мина: если сделать 4.3 без 4.2 — asyncio.get_running_loop() внутри потока бросит RuntimeError, ветка тихо упадёт в except-лог, записи media id молча перестанут сохраняться и фоновый прогрев кэша умрёт. Строго 4.2 до 4.3.
  • GIL: перенос в поток не делает работу бесплатной — луп перестаёт стоять колом, но замедляется. Поэтому 4.1/4.4 (сокращение объёма работы) обязательны, а не опциональны.
  • XSS: перед удалением внутренних sanitize-проходов построить карту «выходная точка → где чистится»; ни один выход не должен остаться без ровно одного прохода.
  • Внутри потока не должно остаться ни create_task, ни get_running_loop, ни asyncio-примитивов.
  • deepcopy живых Message сегодня работает — код только переместить, не менять. Отказ от deepcopy (не-мутирующая группировка через map message_id→group_id) — отдельный опциональный рефакторинг, сюда не тащить.

DoD

Во время генерации фида на 100 сообщений параллельный /ping (стадия 6) отвечает < 100 мс; XSS-тесты зелёные; media id сохраняются.

Зависимости: нет жёстких; /ping для DoD-проверки появляется в стадии 6 (можно проверить любым лёгким эндпоинтом).

## Цель Event loop не должен выполнять CPU-работу > ~50 мс: во время генерации фида сейчас замирает всё приложение, включая стриминг медиа. Полный план: [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) ## Контекст (что чиним) - `copy.deepcopy` сотен Message-объектов на лупе (rss_generator.py:38, активен при `TIME_BASED_MERGE=True` — включено в проде). - `process_message` синхронно на лупе для каждого сообщения (rss_generator.py:211-230), внутри — **4+ вызова bleach** на сообщение (post_parser.py:672, 752, 833, 920; есть диагностика `diag_sanitize_slow` >50мс/вызов) и `str(message)` — полная сериализация каждого поста (post_parser.py:523). - На 100 сообщениях — секунды блокировки лупа на каждый poll фида. ## Задачи (строгий порядок: 4.2 → 4.3!) - [ ] **4.1** `process_message(..., include_raw: bool = False)`: `str(message)` только для JSON-выдачи и debug-HTML, в фидах — никогда - [ ] **4.2** Убрать side-effect IO из `process_message`: `_save_media_file_ids` (post_parser.py:977-1028) больше не делает `get_running_loop()`/`create_task`, а копит `(channel, post_id, fid, ts)` в `self._pending_media_ids`; после рендера вызывающий код один раз делает `await asyncio.to_thread(upsert_media_file_ids_bulk_sync, ...)` — новая функция в file_io.py с `executemany`. Счётчик `_persist_pending_count` удалить - [ ] **4.3** `_create_time_based_media_groups` / `_create_messages_groups` / `_trim_messages_groups` / `_render_messages_groups` — внутри нет ни одного await → превратить в sync-функции и выполнять одним `to_thread` из `generate_channel_rss`/`generate_channel_html` (deepcopy уезжает туда же) - [ ] **4.4** Один sanitize на выходную границу: фиды — финальный проход уже есть (rss_generator.py:443, 643) → внутренние per-message проходы убрать; `/html/{ch}/{id}` — добавить единственный проход в `_format_html`; `/json` — один проход по body+footer в `process_message` - [ ] debug-HTML: `raw_message` в `<pre>` прогнать через `html.escape` - [ ] Тесты: XSS (`<script>`, `onerror=`, `javascript:` → вычищено во всех трёх выводах); media id сохраняются после рендера (bulk upsert); deepcopy Message из pickle-кэша не падает в потоке ## Где не наебаться - **Главная мина**: если сделать 4.3 без 4.2 — `asyncio.get_running_loop()` внутри потока бросит RuntimeError, ветка тихо упадёт в except-лог, **записи media id молча перестанут сохраняться** и фоновый прогрев кэша умрёт. Строго 4.2 до 4.3. - GIL: перенос в поток не делает работу бесплатной — луп перестаёт стоять колом, но замедляется. Поэтому 4.1/4.4 (сокращение объёма работы) обязательны, а не опциональны. - XSS: перед удалением внутренних sanitize-проходов построить карту «выходная точка → где чистится»; ни один выход не должен остаться без ровно одного прохода. - Внутри потока не должно остаться ни `create_task`, ни `get_running_loop`, ни asyncio-примитивов. - deepcopy живых Message сегодня работает — код только переместить, не менять. Отказ от deepcopy (не-мутирующая группировка через map message_id→group_id) — отдельный опциональный рефакторинг, сюда не тащить. ## DoD Во время генерации фида на 100 сообщений параллельный `/ping` (стадия 6) отвечает < 100 мс; XSS-тесты зелёные; media id сохраняются. **Зависимости**: нет жёстких; `/ping` для DoD-проверки появляется в стадии 6 (можно проверить любым лёгким эндпоинтом).
Sign in to join this conversation.