---
work_item: ORCH-111
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---

# adr-0041: Watchdog-сигнал `proc_blocking` — алерт на долго живущий осиротевший тест-процесс

- **Статус:** proposed
- **Дата:** 2026-06-15
- **Задача:** ORCH-111 (bug → escalate full-cycle)
- **Детальный ADR:** `docs/work-items/ORCH-111/06-adr/ADR-001-watchdog-orphan-test-process-alert.md`
- **Парные ADR:** `adr-0033` (sidecar-watchdog F1b), `adr-0030` (`/metrics` — не трогаем),
  `adr-0024` (disk-watchdog — образец), `adr-0040` (timeout-kill `-9` — источник осиротения)

## Контекст
Sidecar-watchdog (ORCH-100, adr-0033) алертит `agent_hung`/`stage_stuck`/`container_down`/`orch_down`/
`host_mem`/`queue_depth`/`job_failed`/`dep_down`. `agent_hung` покрывает **только** running-агент-джобы
(по `jobs.pid` из `/metrics agents[]`). Но виновные процессы инцидента ORCH-109 — это субпроцессы
pytest, которые орк запускает своим кодом (`merge_gate.retest_branch`, `coverage_gate.measure_coverage`);
при timeout-kill агента (`-9`, adr-0040) или `TimeoutExpired` внук-pytest репарентируется на PID 1
orchestrator-контейнера (tini жнёт зомби, но **не убивает живых осиротевших**) и живёт сутками, грузя
CPU и валя merge-gate re-test. Контейнер `orchestrator-watchdog` сейчас **не видит таблицу процессов
хоста** (`network_mode: host`, но **без** `pid: host` и mount `/proc`). Между `agent_hung` (треканые
джобы) и осиротевшим процессом — слепая зона: блокирующий pytest **не порождает сигнала**.

## Решение
Новый per-entity сигнал **`proc_blocking`** **внутри наблюдателя** (`watchdog/**`): на каждом тике
sidecar **сам** сканирует `/proc` хоста (stdlib), отбирает процессы тест-класса (cmdline матчит
паттерн, дефолт `pytest`) и при возрасте > порога (заведомо > макс. легитимного бюджета тест-прогона)
поднимает алерт через **существующую** `decision.decide()`/`AlertState` в собственный Telegram-канал
sidecar. Watchdog процесс **не трогает** (только наблюдение, C-1). Изменения строго в наблюдателе;
`src/**` / `/metrics`+`schema_version` / `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` /
machine-verdict / схема БД — **не тронуты**.

- **Механизм — watchdog-side `pid: host`, НЕ orch-side `/metrics`.** Решающее: orch-side путь правит
  `src/metrics.py` → рестарт прод-`orchestrator` (запрет NFR-3); и слеп именно когда орк деградировал
  (CPU-голодание), что противоречит C-1 (наблюдатель переживает падение наблюдаемого). Watchdog-side
  читает `/proc` независимо от живости орка и не трогает контракт `/metrics`.
- **Коллектор** `watchdog/collectors/proc.py` (новый, по образцу `collectors/host.py`): stdlib-only
  (`/proc/stat` btime + `SC_CLK_TCK`; `/proc/<pid>/{cmdline,stat}`; возраст из starttime, CPU-время
  из utime+stime — информационно); **read-only** (никогда `os.kill`/`Popen`/`/proc/<pid>/environ`);
  **never-raise** (per-pid skip; top → `[]`).
- **Builder** `proc_signals` (чистый, в `signals.py`): ключ `("proc_blocking", pid)`; `active` ⇔
  `age_s > proc_age_s`; detail = усечённый cmdline-фрагмент + PID + возраст + CPU-время (BR-2).
- **RECOVERY для исчезнувшего процесса (AC-6):** в `core.tick()` синтезируется `Signal(active=False)`
  для `proc_blocking`-ключей, которые `alerting=True`, но исчезли из наблюдаемых → `decide()` даёт
  один RECOVERY (переиспользование машины, без отдельной анти-спам-логики, FR-5).
- **Анти-false-positive и отсутствие дубля с `agent_hung` — по построению:** (1) cmdline-скоуп —
  `claude`-агенты не матчат `pytest` ⇒ нулевое пересечение с `agent_hung` (NFR-4); (2) порог возраста
  > макс. бюджета (`max(merge_retest_timeout_s=600, coverage_run_timeout_s=900)=900s`) ⇒ легитимный
  in-budget прогон всегда ниже порога (BR-4). Кросс-namespace матчинг PID не нужен (ненадёжен).
- **Конфиг (новые `WATCHDOG_PROC_*`):** `WATCHDOG_PROC_ENABLED` (дефолт **false** — opt-in/kill-switch,
  зеркало `WATCHDOG_DISK_CRIT_ENABLED`), `WATCHDOG_PROC_AGE_MIN` (дефолт `60` мин; **инвариант:** >
  макс. бюджета), `WATCHDOG_PROC_PATTERNS` (CSV, дефолт `pytest`), `WATCHDOG_PROC_COOLDOWN_S`
  (дефолт `1800`). Дефолт-off ⇒ коллектор не вызывается ⇒ нулевая регрессия (AC-7).
- **Топология:** `pid: host` **только** на сервисе `orchestrator-watchdog` (НЕ volume → существующий
  `:ro`-тест compose зелёный; `/proc` отражает хост автоматически, отдельный mount не нужен).
  Привилегия — только у наблюдателя.

## Альтернативы
- **Orch-side `/metrics`-обогащение** — отвергнуто: рестарт прод-орка (NFR-3) + слепота при
  деградации орка (C-1) + новая поверхность контракта.
- **Bind-mount `/proc:ro` вместо `pid: host`** — эквивалентная видимость/привилегия; `pid: host`
  идиоматичнее (согласован с уже-`network_mode: host`). Валидная замена при предпочтении не делить
  PID-namespace.
- **Расширить `agent_hung` на нетреканые процессы** — отвергнуто: дубль/смешение классов (NFR-4).
- **Реакция (kill/reap)** — вне объёма (BR-3, жёсткое ограничение): только мониторинг.
- **Дефолт-on** — отвергнуто: привилегия + риск false-positive требуют осознанного opt-in.

## Последствия
- Закрыта слепая зона: ранний адресный алерт о CPU-голодании до того, как оно завалит merge-gate
  re-test очередной задачи; работает даже при лёгшем орке.
- Строго read-only + never-raise + дефолт-off + только наблюдатель ⇒ self-hosting-безопасно (enduro не
  затронут); конвейер byte-for-byte; deploy без рестарта прод-`orchestrator` (только sidecar).
- Анти-FP и no-dup — структурно (cmdline-скоуп + порог возраста), не хрупким PID-матчингом.
- Плата: расширение привилегии наблюдателя (`pid: host`, read-only, **меньше** уже-смонтированного
  `docker.sock`; код читает только `/stat`+`/cmdline`, никогда `/environ`; cmdline в алерте усечена);
  Linux-специфичность `/proc` (не-Linux → `[]`); новые `WATCHDOG_PROC_*` ключи в каноне тиража.
- **Топология** меняется (`pid: host`) → `07-infra-requirements.md`; **схема БД** не меняется → 08 =
  N/A. Новый компонентный сигнал + привилегия → `arch:major-change`; прод-выкат через staging-гейт
  sidecar, без рестарта прод-контейнера.
- **Откат:** `WATCHDOG_PROC_ENABLED=false` (мгновенный) или удаление коллектора/builder/врезок/ключей
  + `pid: host` — без следов в БД/схеме/контракте `/metrics`.

## Связи
adr-0033 (sidecar-watchdog F1b — рантайм/машина решения/независимый канал/never-raise — прямой
родитель), adr-0030 (контракт `/metrics`/`schema_version` — изолирован, не тронут), adr-0024
(disk-watchdog — образец pure-`decide_action`/dedup/recovery + «только читает и уведомляет»), adr-0040
(timeout-бюджеты + `-9` timeout-kill — механизм осиротения внука-pytest), adr-0037/0038
(Lite/Bundled тираж — канон `WATCHDOG_*` + compose sidecar, NFR-5).
</content>
