architect(ET): auto-commit from architect run_id=675
This commit is contained in:
@@ -108,6 +108,24 @@ F1b (рамка C-1: наблюдатель отделён от наблюдае
|
||||
`disk_watchdog` (ORCH-063, канал орка) ⇒ **нулевой дубль по построению**; sidecar покрывает провал
|
||||
«орк+disk_watchdog мертвы» через `orch_down`, плюс **opt-in** независимый критический потолок
|
||||
`host_disk_crit` (97%, `WATCHDOG_DISK_CRIT_ENABLED=false` по умолчанию) — другое событие/канал.
|
||||
- **`proc_blocking` — алерт на долго живущий осиротевший тест-процесс (ORCH-111, opt-in,
|
||||
[adr-0041](adr/adr-0041-watchdog-orphan-test-process-alert.md)):** закрывает слепую зону между
|
||||
`agent_hung` (видит только треканые джобы по `jobs.pid`) и осиротевшими субпроцессами pytest,
|
||||
которые орк запускает сам (`merge_gate.retest_branch`/`coverage_gate.measure_coverage`) и которые
|
||||
при timeout-kill агента (`-9`, ORCH-109) репарентируются на tini и живут сутками, грузя CPU и валя
|
||||
merge-gate re-test. Sidecar **сам** сканирует `/proc` хоста (новый коллектор
|
||||
`watchdog/collectors/proc.py`, stdlib-only, read-only, never-raise→`[]`); per-entity сигнал
|
||||
`("proc_blocking", pid)` active ⇔ возраст > порога **И** cmdline матчит тест-класс (дефолт `pytest`).
|
||||
Анти-false-positive и отсутствие дубля с `agent_hung` — **по построению**: cmdline-скоуп
|
||||
(`claude`-агент ≠ `pytest`) + порог возраста > макс. бюджета тест-прогона
|
||||
(`max(merge_retest_timeout_s, coverage_run_timeout_s)`), а не хрупким кросс-namespace матчингом PID.
|
||||
Алерт/recovery — через ту же `decide()`/`AlertState` (RECOVERY синтезируется для исчезнувшего
|
||||
процесса). Watchdog процесс **не трогает** (только наблюдение, C-1/BR-3). **Топология:** сервису
|
||||
`orchestrator-watchdog` добавлен `pid: host` (видимость хост-namespace; привилегия только у
|
||||
наблюдателя, read-only, меньше уже-смонтированного `docker.sock`). Ключи `WATCHDOG_PROC_*`
|
||||
(`ENABLED` дефолт **false** / `AGE_MIN`=60 / `PATTERNS`=`pytest` / `COOLDOWN_S`); дефолт-off →
|
||||
нулевая регрессия. Деплой пересобирает **только** sidecar — прод `orchestrator` не рестартится.
|
||||
Детали — `docs/work-items/ORCH-111/06-adr/ADR-001-watchdog-orphan-test-process-alert.md`.
|
||||
- **Гарантии:** never-raise (per-source/per-tick/per-send); kill-switch `WATCHDOG_ENABLED=false` →
|
||||
демон инертен (idle-loop, нулевой эффект на орк); строго read-only к наблюдаемому (нет
|
||||
start/stop/restart/exec/записи в `docker.sock`/БД/`main`) ⇒ self-hosting-безопасно (enduro не
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user