--- work_item: ORCH-111 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-15 model_used: claude-opus-4-8 escalate: full-cycle --- # 01 — BRD (бизнес-требования): ORCH-111 — watchdog должен алертить на долго живущие pytest/дочерние процессы, блокирующие конвейер Work Item: **ORCH-111** · Repo: **orchestrator** · Стадия: analysis · Трек: **Bug → эскалация в полный цикл** > **Эскалация (`escalate: full-cycle`).** Задача пришла как баг (`BUG:` в заголовке), но **не** > является дешёвым багфиксом: закрытие пробела требует **архитектурного решения** — у sidecar-watchdog > сейчас **нет видимости процессов хоста вообще** (`network_mode: host`, но без `pid: host` и без > bind-mount `/proc`). Выбор механизма наблюдения (расширение привилегий sidecar vs обогащение > контракта `/metrics` орком) — это развилка с последствиями для безопасности и стабильного контракта > `/metrics` (schema_version, ORCH-099). Поэтому пакет — **полный** (а не lite-bug), и задача > помечена `escalate: full-cycle`: нужен прогон стадии `architecture` + ADR (механизм видимости, > эвристика детекции, привилегии/безопасность). Оператор снимает багфикс-трек эндпоинтом > `POST /bug-fast-track/escalate?work_item=ORCH-111` (ADR-001 D5, ORCH-019). ## 1. Бизнес-контекст и проблема ### 1.1 Симптом (установленный факт) На хосте прода были обнаружены старые зависшие процессы `python3 -m pytest tests/test_install_lite_script.py`, которые жили **более 2 суток**, грузили CPU и мешали локальному merge-gate re-test. Из-за конкуренции за CPU задача **ORCH-109 несколько раз упиралась** в `re-test timeout after 600s` (`merge_gate.retest_branch`). Сами эти процессы **не были подняты как отдельный alert** watchdog'а — оператор узнал о них случайно. ### 1.2 Локализация - **Мониторинг-мозг** — sidecar-watchdog (ORCH-100, каталог `watchdog/`, сервис `orchestrator-watchdog`). Он уже алертит на `stage_stuck` (стадия задачи застряла) и `container_down` (контейнер не в норме), а также `agent_hung`, `orch_down`, `host_mem`, `queue_depth`, `job_failed`, `dep_down`. - **Сигнал `agent_hung`** (`watchdog/signals.py::eval_envelope`) покрывает **только running-агент-джобы**: он читает раздел `agents[]` из `GET /metrics`, а тот строится `src/metrics.py::_build_agents` по `db.get_running_agents()` — то есть **только по `jobs.pid` активных джобов**. - **Источник зависших процессов** — субпроцессы pytest, которые орк запускает в worktree: `merge_gate.retest_branch` (`python -m pytest `) и `coverage_gate.measure_coverage` (`pytest --cov=src`). При `subprocess.TimeoutExpired` Python убивает прямого ребёнка, но **внуки/репарентированные процессы переживают**; а если сам агент-процесс убит по таймауту (`exit_code=-9`, ORCH-109) — его дочерний pytest **репарентируется на PID 1** и продолжает жить. ### 1.3 Причина (root cause) Между двумя наблюдателями зияет **слепая зона**: `agent_hung` видит лишь *отслеживаемые* агент-джобы (по `jobs.pid`), а **осиротевшие/внебюджетные тестовые субпроцессы** (внуки pytest, репарентированные на PID 1) **не присутствуют ни в `/metrics`, ни в поле зрения sidecar** — у контейнера watchdog нет доступа к таблице процессов хоста. Поэтому долго живущий pytest, реально блокирующий конвейер через CPU-голодание merge-gate, **не порождает ни одного сигнала**, пока формально ни одна стадия задачи не «застряла». ## 2. Объём (scope) ### В объёме - Новый **отдельный класс алерта** watchdog'а: «долго живущий тестовый/дочерний процесс блокирует конвейер» — поднимается, даже если стадия задачи формально не `stuck`. - Детекция долго живущих процессов тест-класса (pytest и родственные субпроцессы гейтов), переживших свой бюджет/осиротевших, на хосте прода. - Актуализация конфиг-канона watchdog (`.env.watchdog.example` / блок `WATCHDOG_*` в `.env.example`) и наблюдаемости. ### Вне объёма - **Любая реакция на процесс** (kill/SIGTERM/cleanup/reap/перезапуск). Задача — **только мониторинг + сигнализация** (явное ограничение заказчика). Автоматический reap осиротевших процессов — **отдельная задача**. - Изменение конвейера: `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict / схема БД — **не трогаются** (watchdog — наблюдатель вне процесса орка и вне Quality Gates). - Расширение `agent_hung` на нетреканые процессы (это другой класс сигнала; дубль запрещён — см. NFR-4). - Снятие первопричины осиротения процессов в самом орке (надёжный reap внуков pytest) — ценно, но это **ремедиация**, она вне объёма этой задачи. ## 3. Заинтересованные стороны - **Заказчик/оператор прода** (Слава) — получает ранний сигнал о CPU-голодании ещё до того, как оно завалит merge-gate re-test очередной задачи. - **Self-hosting конвейер orchestrator** — страдает напрямую (инцидент ORCH-109). - **Тиражные инсталляции (Lite/Bundled, ORCH-102/103)** — sidecar входит в дефолтный комплект; новый сигнал должен укладываться в канон конфига и не ломать тираж. - **Принимает результат** — reviewer/tester + оператор (smoke на staging-эквиваленте sidecar). ## 4. Бизнес-требования (BR) - **BR-1** — Watchdog поднимает **отдельный, узнаваемый** alert, когда на хосте обнаружен долго живущий процесс тест-класса (pytest и его субпроцессы), возраст которого превышает настраиваемый порог — **независимо** от того, застряла ли формально какая-либо стадия задачи. - **BR-2** — Текст алерта **действенно идентифицирует** виновника: фрагмент командной строки, PID, возраст процесса и (при наличии) доля CPU — чтобы оператор мог сразу разобраться и вручную вмешаться. - **BR-3** — **Только мониторинг + сигнализация.** Watchdog **не убивает / не останавливает / не шлёт сигналы** процессу и не выполняет иную ремедиацию (жёсткое ограничение заказчика, рамка C-1 «наблюдатель строго read-only к наблюдаемому», ORCH-100). - **BR-4** — **Без ложных срабатываний** на легитимных in-flight прогонах: тестовый процесс, принадлежащий **активному отслеживаемому** агенту/гейту в пределах его бюджета, alert поднимать **не должен**. - **BR-5** — Анти-спам и recovery как у прочих сигналов: один alert на пересечение порога, throttled re-alert по cooldown, однократный recovery при исчезновении процесса (переиспользовать `watchdog/decision.py::decide` + `AlertState`). - **BR-6** — Сигнал под **kill-switch** и управляется конфигом (порог возраста, cooldown, область). Дефолт выбирается так, чтобы включение было **осознанным** и **self-hosting-безопасным** (см. NFR-3). ## 5. Нефункциональные требования (NFR) - **NFR-1 (надёжность)** — **never-raise** на всех новых путях (per-source / per-tick / per-send), как и весь watchdog: сбой коллектора процессов деградирует ОДИН сигнал, а не роняет тик. - **NFR-2 (read-only)** — строго наблюдение: **ни одного** управляющего действия над процессами/хостом (нет `kill`/`signal`/`Popen`/записи). Соответствует C-1 (observer separated from observed). - **NFR-3 (self-hosting безопасность)** — выкат изменения **не перезапускает** прод-контейнер `orchestrator` (встанет конвейер всех проектов): пересобирается/рестартится **только** контейнер `orchestrator-watchdog`. Если механизм требует расширения привилегий sidecar (напр. `pid: host`) — это привилегия **только наблюдателя**, обоснование и риски — задача архитектора (ADR). - **NFR-4 (без дубля)** — новый сигнал **не пересекается** с `agent_hung` (тот уже покрывает отслеживаемые агент-джобы): новый сигнал закрывает ровно пробел «нетреканый/осиротевший процесс». - **NFR-5 (канон тиража)** — при изменении compose / ключей `.env.watchdog` обновить в **том же PR**: `.env.watchdog.example`, блок `WATCHDOG_*` в `.env.example`, `docs/deployment/LITE_SETUP.md` и `docs/architecture/README.md` (норматив сопровождения ORCH-102 NFR-5; key-set-sync тест). - **NFR-6 (стек)** — sidecar остаётся **stdlib-only** (C-3, ORCH-100): без новых сторонних зависимостей. ## 6. Допущения и ограничения - **Ключевое архитектурное допущение (для архитектора):** у контейнера `orchestrator-watchdog` сейчас **нет** видимости процессов хоста (`network_mode: host`, но без `pid: host` и без mount `/proc`). Закрытие пробела требует выбора механизма — **развилка, решаемая ADR**, не аналитиком. Кандидаты (перечислены как материал для решения, **без навязывания**): (a) расширение привилегий sidecar — `pid: host` либо read-only mount хостового `/proc`, затем stdlib-скан таблицы процессов; (b) обогащение `/metrics` орком новым read-only разделом о «бесхозных» тест-субпроцессах (орк видит свой PID-namespace), который sidecar лишь читает. У каждого — свои trade-off'ы (привилегии vs контракт `/metrics`). - `/metrics` — **версионированный контракт** (`schema_version`, ORCH-099): если выбран путь (b), аддитивные изменения **не бампят** версию (sidecar обязан толерировать). - Порог возраста для детекции **должен превышать** максимальный легитимный бюджет тест-прогона (`merge_retest_timeout_s` ≈ 600s, `coverage_run_timeout_s`), чтобы нормальный прогон **никогда** не алертил, а 2-суточный осиротевший pytest — гарантированно (анти-false-positive, материал для ADR). - enduro-trails не затронут: watchdog наблюдает хост/орк self-hosting; сигнал config-gated. ## 7. Критерии успеха Watchdog при наличии долго живущего pytest/дочернего процесса, грузящего CPU, **поднимает отдельный alert** в свой Telegram-канал (с PID/cmd/возрастом), **не трогая** процесс; при отсутствии такого процесса (или выключенном флаге) — молчит; нормальный тест-прогон под активным джобом **не** триггерит ложный alert. Детальные PASS/FAIL — `03-acceptance-criteria.md`. ## 8. Риски - **Ложные срабатывания** на легитимном длинном прогоне → спам в канал (митигируется порогом > макс. бюджета + корреляцией с активным джобом). - **Расширение привилегий sidecar** (если выбран `pid: host`/`/proc`-mount) → увеличение поверхности безопасности наблюдателя (требует явного обоснования в ADR; дефолт-off). - **Дубль с `agent_hung`** при небрежной реализации (NFR-4). - Детали и владельцы рисков — `10-tech-risks.md` (заполняет архитектор).