--- work_item: ORCH-111 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-15 model_used: claude-opus-4-8 --- # 02 — ТЗ (TRZ): ORCH-111 — alert на долго живущие pytest/дочерние процессы в watchdog Work Item: **ORCH-111** · Repo: **orchestrator** · Стадия: analysis > ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода. > Архитектурное обоснование/решения — задача архитектора (06-adr). В частности, **выбор механизма > видимости процессов хоста** (см. §2 «развилка») — за ADR; ниже зафиксированы **требования и > ограничения**, а не способ реализации. ## 1. Сводка изменения Добавить в sidecar-watchdog (`watchdog/`) **новый сигнал** класса «долго живущий блокирующий тест/дочерний процесс» (рабочее имя ключа — `proc_blocking`; финальное имя утверждает разработчик/ADR). Сигнал активен, когда на хосте есть процесс тест-класса (pytest и его субпроцессы), чей возраст превысил настраиваемый порог и который **не атрибутируется активному отслеживаемому джобу**. Сигнал проходит через **существующую** машину `decide()`/`AlertState` (анти-спам/recovery) и публикуется в собственный Telegram-канал sidecar. Watchdog при этом **не трогает** процесс (BR-3/NFR-2). Это изменение **внутри наблюдателя**: машина стадий орка и Quality Gates не затрагиваются. ## 2. Задействованные модули / пути > **Развилка механизма (решает архитектор, ADR).** Часть путей **условна** и зависит от выбранного > механизма видимости процессов. Ниже помечено явно. | Путь | Действие | |------|----------| | `watchdog/signals.py` | изменить — чистый builder нового сигнала `proc_blocking` (по образцу `host_signals`/`container_signals`) | | `watchdog/config.py` | изменить — новые ключи `WATCHDOG_PROC_*` (enable/порог возраста/паттерны/cooldown); never-raise парсеры | | `watchdog/core.py` | изменить — врезка коллектора процессов + диспетч нового сигнала в `tick()` (per-source guard) | | `watchdog/collectors/proc.py` | **создать** — коллектор списка процессов-кандидатов (механизм — по ADR); never-raise → `[]` | | `.env.watchdog.example` | изменить — задокументировать новые `WATCHDOG_PROC_*` ключи (канон) | | `.env.example` (блок `WATCHDOG_*`) | изменить — key-set-sync с `.env.watchdog.example` (тест `test_lite_setup_doc`/key-sync) | | `tests/watchdog/test_proc_blocking_signal.py` | **создать** — unit + регресс на новый сигнал | | `tests/watchdog/test_tick_proc_blocking_integration.py` | **создать** — интеграция tick→dispatch | | `docs/architecture/README.md`, `docs/deployment/LITE_SETUP.md` | изменить — описать сигнал/ключи (NFR-5) | | `docker-compose.yml` (сервис `orchestrator-watchdog`) | **условно** изменить — привилегия/mount (`pid: host` или `/proc:ro`) **только если** ADR выберет watchdog-side host-скан | | `src/metrics.py` (`_build_*`, аддитивный раздел) | **условно** изменить — **только если** ADR выберет orch-side обогащение `/metrics`; **аддитивно**, без бампа `schema_version` | ## 3. Функциональные требования ### FR-1 — Новый сигнал `proc_blocking` (чистый builder) В `watchdog/signals.py` добавить чистую функцию-builder (без I/O), которая по списку записей о процессах-кандидатах и конфигу возвращает `Signal`-объекты: - **Ключ** — per-entity, со **стабильной идентичностью** процесса (напр. `("proc_blocking", pid)` или хеш `cmdline`), чтобы `AlertState`/cooldown работали по каждому процессу отдельно (как `("container_down", name)`). - **active=True** ⇔ возраст процесса `> cfg.proc_age_s` **И** командная строка матчит класс «тест/дочерний» (паттерн pytest и родственные, конфигурируемо) **И** процесс **не** принадлежит активному отслеживаемому джобу (анти-false-positive, BR-4). - **title/detail** — действенные: фрагмент cmdline, PID, возраст (сек), доля/время CPU при наличии (BR-2). Текст на русском, в стиле существующих сигналов. - Привязка: **BR-1, BR-2, BR-4**. ### FR-2 — Коллектор процессов-кандидатов Создать `watchdog/collectors/proc.py` — собирает «сырьё» (список записей `{pid, cmdline, age_s, cpu?}`) тем механизмом, который утвердит ADR. Контракт коллектора **фиксирован независимо от механизма**: **stdlib-only** (NFR-6), **read-only** (NFR-2), **never-raise** → при любой ошибке/ недоступности источника возвращает `[]` (один сигнал пропущен, тик жив). Привязка: **NFR-1, NFR-2, NFR-6**. ### FR-3 — Конфиг + kill-switch В `watchdog/config.py` добавить ключи (имена финализирует разработчик/ADR; предложение): `WATCHDOG_PROC_ENABLED` (kill-switch), `WATCHDOG_PROC_AGE_MIN` (порог возраста в минутах; дефолт **должен превышать** макс. легитимный бюджет тест-прогона — см. §7), `WATCHDOG_PROC_PATTERNS` (CSV паттернов cmdline, дефолт включает `pytest`), при необходимости отдельный `WATCHDOG_PROC_COOLDOWN_S`. Все парсеры never-raise с безопасными дефолтами (как существующие `_int`/`_bool`/`_csv`). Выключенный флаг → коллектор/сигнал инертны (нулевой эффект). Привязка: **BR-6, NFR-1**. ### FR-4 — Инвариант «только наблюдение» На всём новом пути запрещены `os.kill`, отправка сигналов, `subprocess.Popen`/`run`, любые мутации процессов/ФС/БД. Watchdog **только читает и уведомляет**. Привязка: **BR-3, NFR-2**. ### FR-5 — Диспетч через существующую машину решения Новый сигнал диспетчеризуется в `core.tick()` через тот же путь `decision.decide(...)` + `self._states[key]` + `self._send(...)`: ALERT на пересечении порога, REALERT по cooldown, RECOVERY при исчезновении процесса. Никакой отдельной логики анти-спама не вводить. Привязка: **BR-5**. ### FR-6 — Без дубля с `agent_hung` Новый сигнал покрывает **только** процессы, **не** представленные в `/metrics agents[]` (нетреканые/ осиротевшие). Атрибуция «процесс принадлежит активному джобу» исключает такие процессы из `proc_blocking` (предотвращает двойной алерт и ложные срабатывания на живом агенте). Привязка: **NFR-4, BR-4**. ## 4. Изменения API - **Орк HTTP API:** новых эндпоинтов **не требуется**. **Условно** (если ADR выберет orch-side путь): **аддитивный** раздел в ответе `GET /metrics` (`src/metrics.py`) о бесхозных тест-субпроцессах — строго read-only, **без бампа** `schema_version` (ORCH-099 NFR-6), sidecar толерирует отсутствие. - **Watchdog:** внутренний сигнал, внешнего API не имеет. ## 5. Изменения схемы БД Нет. ## 6. Требования к новым/изменённым QG checks Нет. Watchdog — наблюдатель **вне** конвейера и вне Quality Gates: `QG_CHECKS` / `check_*` / machine-verdict ключи / `STAGE_TRANSITIONS` — **байт-в-байт не трогаются** (как `disk_watchdog`/ `reaper`/`reconciler`). ## 7. Совместимость / регресс - **Kill-switch + дефолты:** при выключенном `WATCHDOG_PROC_ENABLED` (или при дефолте, выбранном безопасно) — **нулевая регрессия**: ни одного нового алерта, тик 1:1 как до ORCH-111. Дефолтный порог возраста **обязан превышать** максимальный легитимный бюджет тест-прогона (`merge_retest_timeout_s` ≈ 600s, `coverage_run_timeout_s`) — иначе нормальный прогон даст ложный alert (анти-false-positive, BR-4). - **never-raise / read-only:** новый код не может уронить тик и не выполняет управляющих действий (NFR-1/NFR-2). - **Контракт `/metrics`:** при orch-side варианте — только аддитивно, без бампа версии; при watchdog-side варианте — `/metrics` не трогается вовсе. - **Self-hosting (NFR-3):** выкат — пересборка/рестарт **только** `orchestrator-watchdog`; прод `orchestrator` **не** перезапускается. Если механизм требует привилегии/mount в compose — это правка **только** сервиса watchdog. - **Канон тиража (NFR-5):** новые ключи синхронизировать в `.env.watchdog.example` ↔ блок `WATCHDOG_*` в `.env.example` и описать в `LITE_SETUP.md` в том же PR (key-set-sync тест должен остаться зелёным). - **Область:** сигнал config-gated; enduro-trails не затронут.