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>
|
||||
@@ -0,0 +1,245 @@
|
||||
---
|
||||
work_item: ORCH-111
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-15
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# ADR-001: Watchdog-сигнал `proc_blocking` — алерт на долго живущий блокирующий тест-процесс
|
||||
|
||||
Work Item: **ORCH-111** — watchdog должен алертить на долго живущие pytest/дочерние процессы, блокирующие конвейер
|
||||
Стадия: **architecture**
|
||||
Сквозная регистрация: **`docs/architecture/adr/adr-0041-watchdog-orphan-test-process-alert.md`** (кросс-каттинг: новый компонентный сигнал sidecar + изменение топологии контейнера-наблюдателя).
|
||||
|
||||
## Статус
|
||||
Proposed
|
||||
|
||||
## Контекст
|
||||
|
||||
**Установленный инцидент (BRD §1).** На прод-хосте жили >2 суток осиротевшие процессы
|
||||
`python3 -m pytest tests/test_install_lite_script.py`, грузили CPU и через конкуренцию за CPU
|
||||
несколько раз валили локальный merge-gate re-test (`merge_gate.retest_branch` → `re-test timeout
|
||||
after 600s`) на ORCH-109. **Ни одного алерта** не поднялось — оператор узнал случайно.
|
||||
|
||||
**Слепая зона (сверено по коду).**
|
||||
- Sidecar-watchdog (ORCH-100, `watchdog/`, adr-0033) уже алертит `agent_hung`/`stage_stuck`/
|
||||
`container_down`/`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, которые орк запускает **своим** кодом:
|
||||
`merge_gate.retest_branch` (`subprocess.run(["python","-m","pytest", target, "-q"], …)`,
|
||||
таймаут `settings.merge_retest_timeout_s=600`) и `coverage_gate.measure_coverage`
|
||||
(`pytest --cov=src`, таймаут `settings.coverage_run_timeout_s=900`). При
|
||||
`subprocess.TimeoutExpired` / timeout-kill агента (`exit_code=-9`, ORCH-109) внук-pytest
|
||||
**репарентируется на PID 1** orchestrator-контейнера (tini под `init: true` — жнёт зомби, но
|
||||
**не убивает живых** осиротевших) и продолжает жить.
|
||||
- **Контейнер `orchestrator-watchdog` сейчас не видит таблицу процессов хоста вообще**
|
||||
(`docker-compose.yml`: `network_mode: host` + read-only `docker.sock` + `/repos:ro` + `./data:ro`,
|
||||
**без** `pid: host` и **без** mount `/proc`). Между `agent_hung` (видит только треканые джобы по
|
||||
`jobs.pid`) и реальностью (осиротевший pytest вне `/metrics` и вне поля зрения sidecar) зияет дыра:
|
||||
долго живущий pytest, реально блокирующий конвейер через CPU-голодание, **не порождает сигнала**,
|
||||
пока формально ни одна стадия не `stuck`.
|
||||
|
||||
**Linux-семантика, на которую опирается решение.** Хост-namespace процессов — предок всех
|
||||
контейнерных: из корневого PID-namespace видны **все** процессы всех контейнеров (с хост-глобальными
|
||||
PID). Контейнер с `pid: host` присоединяется к этому namespace, и его `/proc` отражает хост → видит
|
||||
осиротевший pytest независимо от того, в каком контейнере он формально запущен. Это подтверждает
|
||||
факт BRD «обнаружены на хосте» (оператор делал `ps aux` в корневом namespace).
|
||||
|
||||
**Почему нужен ADR, а не lite-bug.** Закрытие дыры — развилка с последствиями для безопасности
|
||||
(привилегии наблюдателя) и для стабильного контракта `/metrics` (`schema_version`, ORCH-099). Задача
|
||||
эскалирована `escalate: full-cycle` (BRD).
|
||||
|
||||
## Решение
|
||||
|
||||
### Сводка
|
||||
Добавить в sidecar-watchdog **новый per-entity сигнал** `proc_blocking`: на каждом тике sidecar
|
||||
**сам** сканирует хостовую таблицу процессов (stdlib `/proc`), отбирает процессы тест-класса (cmdline
|
||||
матчит паттерн, дефолт `pytest`), и если возраст процесса превысил порог (заведомо больший
|
||||
максимального легитимного бюджета тест-прогона) — поднимает алерт через **существующую** машину
|
||||
`decision.decide()` / `AlertState` в собственный Telegram-канал sidecar. Watchdog процесс **не
|
||||
трогает** (только наблюдение). Механизм видимости — **watchdog-side** (`pid: host` на сервисе
|
||||
`orchestrator-watchdog`), а **не** обогащение `/metrics` орком. Изменения — строго внутри наблюдателя
|
||||
(`watchdog/**` + сервис watchdog в compose); `src/**` / `/metrics` / `STAGE_TRANSITIONS` / `QG_CHECKS`
|
||||
/ `check_*` / machine-verdict / схема БД — **не тронуты**.
|
||||
|
||||
### D1 — Механизм видимости: watchdog-side `/proc`-скан под `pid: host` (НЕ orch-side `/metrics`)
|
||||
**Решение:** sidecar получает видимость процессов хоста через `pid: host` на сервисе
|
||||
`orchestrator-watchdog` и читает `/proc` напрямую (stdlib). Отвергнут orch-side путь (обогащение
|
||||
`/metrics` разделом о бесхозных процессах).
|
||||
|
||||
Обоснование (по приоритету):
|
||||
1. **NFR-3 (self-hosting deploy-инвариант) — решающий.** Orch-side путь правит `src/metrics.py` →
|
||||
требует пересборки/**рестарта прод-контейнера `orchestrator`** → прямо запрещён NFR-3 («выкат
|
||||
пересобирает/рестартит только `orchestrator-watchdog`»). Watchdog-side путь правит только
|
||||
`watchdog/**` + сервис watchdog → удовлетворяет NFR-3.
|
||||
2. **Резилентность / покрытие именно дыры.** Осиротевший pytest опаснее всего, когда орк уже
|
||||
деградировал (CPU-голодание) или лёг. Orch-side путь в этот момент **слеп** (`/metrics`
|
||||
недоступен/тормозит → раздел не доставлен), что противоречит самой причине существования sidecar
|
||||
(C-1: наблюдатель переживает падение наблюдаемого). Watchdog-side читает `/proc` независимо от
|
||||
живости орка.
|
||||
3. **Изоляция контракта.** Watchdog-side не трогает контракт `/metrics` и `schema_version` (ORCH-099)
|
||||
вовсе.
|
||||
|
||||
Привязка: NFR-3, BR-1, C-1.
|
||||
|
||||
### D2 — Анти-false-positive и отсутствие дубля с `agent_hung` — **по построению**, без матчинга PID
|
||||
Два структурных свойства, не требующих ненадёжного кросс-namespace сопоставления PID (при `pid: host`
|
||||
sidecar видит хост-глобальные PID, а `/metrics agents[].pid` — PID в namespace орка; они не совпадают):
|
||||
|
||||
- **Скоуп по cmdline.** `proc_blocking` рассматривает **только** процессы, чья cmdline матчит
|
||||
тест-класс (дефолт-паттерн `pytest`). Агент-процессы `claude` (которые покрывает `agent_hung`)
|
||||
**никогда** не матчат `pytest` → нулевое пересечение с `agent_hung` (NFR-4 / AC-5 **по
|
||||
построению**). Сами pytest-субпроцессы орк запускает своим кодом (merge/coverage-гейты), они
|
||||
**никогда** не треканы как джобы → `agent_hung` их и не покрывает. Два сигнала разбивают
|
||||
пространство процессов по cmdline.
|
||||
- **Порог возраста > макс. легитимного бюджета.** Легитимный in-flight прогон ограничен
|
||||
`merge_retest_timeout_s=600s` и `coverage_run_timeout_s=900s`. При
|
||||
`WATCHDOG_PROC_AGE_MIN*60 > max(600,900)=900s` легитимный прогон **всегда** ниже порога (BR-4 /
|
||||
AC-4 **по построению**). Это и есть «атрибуция активному джобу» из FR-1/FR-6: процесс в пределах
|
||||
бюджета физически не может перерасти порог. Кросс-namespace матчинг PID не нужен (и ненадёжен) —
|
||||
возраст namespace-агностичен.
|
||||
|
||||
**Кросс-инвариант (фиксируется ADR):** дефолтный порог **обязан** превышать максимум
|
||||
`max(merge_retest_timeout_s, coverage_run_timeout_s)`. Дефолт `WATCHDOG_PROC_AGE_MIN=60` (мин) =
|
||||
3600s — 4× запас над 900s; меняя `merge_retest_timeout_s`/`coverage_run_timeout_s` вверх,
|
||||
поднимай порог. Привязка: BR-4, NFR-4, AC-4, AC-5.
|
||||
|
||||
### D3 — Коллектор `watchdog/collectors/proc.py` (stdlib `/proc`-скан)
|
||||
**Создать** leaf-коллектор по образцу `collectors/host.py`. Контракт **фиксирован** (NFR-1/2/6):
|
||||
- **stdlib-only:** читает `/proc/stat` (`btime` — момент загрузки) + `os.sysconf("SC_CLK_TCK")`;
|
||||
итерирует числовые `/proc/<pid>`; `/proc/<pid>/cmdline` (NUL-разделённое → join пробелом) →
|
||||
матч паттерна; `/proc/<pid>/stat` поле 22 (`starttime` в тиках) → `age_s = now - (btime +
|
||||
starttime/clk_tck)`; поля 14+15 (`utime+stime`) → `cpu_s = (utime+stime)/clk_tck` (накопленное
|
||||
CPU-время, **информационно** для BR-2; в активацию НЕ входит).
|
||||
- **read-only:** только открытие файлов на чтение. **Запрещены** `os.kill`, отправка сигналов,
|
||||
`subprocess.Popen/run`, любые мутации ФС/процессов. **Никогда не читает `/proc/<pid>/environ`**
|
||||
(там секреты).
|
||||
- **never-raise:** per-pid guard (один нечитаемый/исчезнувший `/proc/<pid>` пропускается, не роняя
|
||||
список — гонка «процесс умер между listdir и read» нормальна); top-level guard → `[]`.
|
||||
- Выход — список записей `{pid, cmdline, age_s, cpu_s?, start_ticks}`; чистый разбор отделён от I/O
|
||||
(тестируемо на фикстурах `/proc`-текста, без реального хоста).
|
||||
|
||||
Привязка: FR-2, NFR-1, NFR-2, NFR-6, AC-8.
|
||||
|
||||
### D4 — Чистый builder `proc_signals` + синтез RECOVERY для исчезнувшего процесса
|
||||
**Builder** в `watchdog/signals.py` (по образцу `container_signals`/`host_signals`): по списку
|
||||
кандидатов и конфигу возвращает `Signal`-объекты.
|
||||
- **Ключ** — per-entity `("proc_blocking", pid)` (зеркало `("container_down", name)`).
|
||||
- **active=True** ⇔ `age_s > cfg.proc_age_s` (cmdline уже отфильтрована коллектором по паттерну).
|
||||
- **title/detail** — действенные (RU, в стиле существующих): фрагмент cmdline + PID + возраст (сек) +
|
||||
(при наличии) CPU-время (BR-2). Cmdline-фрагмент **усечь** до ограниченной длины (анти-утечка
|
||||
случайных аргументов в канал; см. риск R-2).
|
||||
|
||||
**RECOVERY для исчезнувшего процесса (новый аспект, AC-6).** `container_down` авто-recovery'ится
|
||||
лишь потому, что набор имён статичен (Signal строится на каждый сконфигурированный контейнер). А
|
||||
`agent_hung`/`stage_stuck` emit'ят сигнал **только** когда active=True → при исчезновении сущности
|
||||
**не** recovery'ятся (известное ограничение). AC-6 **требует однократный RECOVERY при исчезновении
|
||||
процесса**. Решение, **переиспользующее** `decide()`/`AlertState` (FR-5 — никакой отдельной
|
||||
анти-спам-логики): в `core.tick()` после построения сигналов по текущим кандидатам **синтезировать**
|
||||
`Signal(active=False)` для каждого ключа в `self._states` с префиксом `"proc_blocking"`, который
|
||||
`alerting=True`, но **отсутствует** в множестве текущих наблюдаемых ключей → `decide()` даёт
|
||||
`RECOVERY` один раз и чистит состояние. Это per-family bookkeeping, не новая throttle-логика.
|
||||
PID-recycling — редкий край: естественный цикл vanish→recovery→new-alert корректен; опциональное
|
||||
усиление ключа — добавить `start_ticks` (`("proc_blocking", pid, start_ticks)`).
|
||||
|
||||
Привязка: FR-1, FR-5, BR-2, BR-5, AC-6.
|
||||
|
||||
### D5 — Конфиг + kill-switch (дефолт-off)
|
||||
В `watchdog/config.py` добавить ключи (never-raise парсеры `_bool`/`_float`/`_csv`):
|
||||
|
||||
| Ключ | Тип | Дефолт | Смысл |
|
||||
|------|-----|--------|-------|
|
||||
| `WATCHDOG_PROC_ENABLED` | bool | **`false`** | kill-switch / осознанный opt-in (зеркало `WATCHDOG_DISK_CRIT_ENABLED`) |
|
||||
| `WATCHDOG_PROC_AGE_MIN` | float (мин) | `60` | порог возраста; **обязан** > `max(merge_retest_timeout_s, coverage_run_timeout_s)/60` (D2) |
|
||||
| `WATCHDOG_PROC_PATTERNS` | CSV | `pytest` | паттерны cmdline тест-класса (substring-матч) |
|
||||
| `WATCHDOG_PROC_COOLDOWN_S` | float | `1800` | per-signal cooldown (через `Signal.cooldown_s`, уже есть) |
|
||||
|
||||
Derived `proc_age_s = proc_age_min*60` (как `agent_hung_s`/`stage_stuck_s`). **Дефолт-off** — потому
|
||||
что `proc_blocking` требует привилегии `pid: host` (D6) и осознанного включения (BR-6/NFR-3). При
|
||||
`WATCHDOG_PROC_ENABLED=false` коллектор в `core.tick()` **не вызывается** (гейт как у `_collect_disk`
|
||||
на `disk_crit_enabled`) → нулевой оверхед и нулевая регрессия (AC-7).
|
||||
|
||||
Привязка: FR-3, BR-6, NFR-1, AC-7.
|
||||
|
||||
### D6 — Топология: `pid: host` **только** на `orchestrator-watchdog`
|
||||
Аддитивная однострочная правка сервиса `orchestrator-watchdog` в `docker-compose.yml`: `pid: host`.
|
||||
Под `pid: host` контейнерный `/proc` автоматически отражает хостовый namespace — **отдельный
|
||||
mount `/proc` не нужен**. `pid: host` — НЕ volume, поэтому существующий тест
|
||||
`tests/watchdog/test_compose_service.py::test_host_paths_mounted_read_only` (требует `:ro` на каждом
|
||||
**volume**) остаётся зелёным; разработчик добавляет позитивный тест на наличие `pid: host`.
|
||||
**Привилегия — только у наблюдателя**, обоснование/риски — `07-infra-requirements.md` + R-2 ниже.
|
||||
Деплой: пересобрать/рестартить **только** `orchestrator-watchdog`; прод `orchestrator` **не**
|
||||
трогается (NFR-3). Для Lite/Bundled (ORCH-102/103) `pid: host` становится частью канонического
|
||||
compose sidecar; сигнал дефолт-off → нулевое изменение поведения тиражных инсталляций.
|
||||
|
||||
Привязка: NFR-3, BR-1, BR-6, AC-9.
|
||||
|
||||
### D7 — Диспетч и инварианты конвейера
|
||||
Новый сигнал диспетчеризуется в `core.tick()` тем же путём `decision.decide(...)` + `self._states[key]`
|
||||
+ `self._send(...)`: ALERT на пересечении порога, REALERT по cooldown, RECOVERY при исчезновении
|
||||
(D4). Никакой отдельной анти-спам-логики (FR-5). Инварианты (AC-9, byte-for-byte):
|
||||
`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict (`verdict:`/`result:`/`deploy_status:`/
|
||||
`staging_status:`/`security_status:`/`coverage_status:`) / схема БД / `/metrics`+`schema_version`
|
||||
— **не тронуты**. Watchdog — наблюдатель вне процесса орка и вне Quality Gates (как
|
||||
`disk_watchdog`/`reaper`/`reconciler`).
|
||||
|
||||
## Альтернативы
|
||||
- **(b) Orch-side обогащение `/metrics` разделом о бесхозных процессах** — отвергнуто: правит
|
||||
`src/**` → рестарт прод-`orchestrator` (нарушение NFR-3); слеп именно когда орк деградировал
|
||||
(нарушение C-1); новая поверхность контракта `/metrics`. (D1.)
|
||||
- **Bind-mount хостового `/proc:ro` вместо `pid: host`** — даёт ту же видимость через read-only
|
||||
volume (паттерн node_exporter `--path.procfs`), но привилегия **эквивалентна** (`/proc/<pid>/environ`
|
||||
читаем при обоих) и `/proc`-mount имеет краевые особенности; `pid: host` идиоматичнее и согласован
|
||||
с уже-host-network-наблюдателем. Остаётся валидной заменой, если оператор предпочитает не делить
|
||||
PID-namespace. (D6.)
|
||||
- **Расширить `agent_hung` на нетреканые процессы** — отвергнуто: дубль/смешение классов (NFR-4); у
|
||||
sidecar нет `jobs.pid` для нетреканых, а кросс-namespace PID не совпадают.
|
||||
- **Реакция (kill/reap осиротевших)** — вне объёма (жёсткое ограничение заказчика BR-3): задача —
|
||||
только мониторинг; reap — отдельная задача (ремедиация).
|
||||
- **Дефолт-on сигнала** — отвергнуто: привилегия `pid: host` + риск false-positive требуют осознанного
|
||||
включения; зеркало `WATCHDOG_DISK_CRIT_ENABLED=false`.
|
||||
|
||||
## Последствия
|
||||
- **+** Закрыта дыра наблюдаемости: долго живущий блокирующий pytest даёт ранний адресный алерт
|
||||
(PID/cmdline/возраст), даже если орк лёг и ни одна стадия формально не `stuck`.
|
||||
- **+** Строго read-only + never-raise + дефолт-off + изменения только в наблюдателе ⇒
|
||||
self-hosting-безопасно; enduro не затронут; конвейер byte-for-byte; deploy без рестарта прод-орка
|
||||
(NFR-3).
|
||||
- **+** Анти-false-positive и отсутствие дубля с `agent_hung` — структурно (cmdline-скоуп + порог
|
||||
возраста), а не хрупким матчингом PID.
|
||||
- **−** Расширение привилегии наблюдателя (`pid: host`): sidecar видит таблицу процессов хоста.
|
||||
Митигейшн: привилегия read-only и **меньше**, чем уже смонтированный `docker.sock` (полная
|
||||
интроспекция контейнеров); код читает **только** `/stat`+`/cmdline`, никогда `/environ`; сигнал
|
||||
дефолт-off; cmdline в алерте усечена. (R-1/R-2.)
|
||||
- **−** Новая поверхность совместимости с `/proc`-форматом (Linux-специфично); на не-Linux/битом
|
||||
`/proc` коллектор → `[]` (один сигнал тих). Митигейшн: чистый разбор + фикстуры.
|
||||
- **−** Канон тиража: при добавлении ключей/правке compose — синхронизировать в **том же PR**
|
||||
`.env.watchdog.example` ↔ блок `WATCHDOG_*` `.env.example`, `docs/deployment/LITE_SETUP.md`,
|
||||
`docs/architecture/README.md` (NFR-5; key-sync `tests/test_lite_setup_doc.py`).
|
||||
- **Масштаб:** новый компонентный сигнал + изменение топологии/привилегий наблюдателя →
|
||||
рекомендуется лейбл **`arch:major-change`**; прод-выкат — через staging-эквивалент sidecar
|
||||
(smoke на staging-хосте), без рестарта прод-`orchestrator`.
|
||||
- **Откат:** `WATCHDOG_PROC_ENABLED=false` (мгновенный, привилегия дремлет) или удаление
|
||||
`watchdog/collectors/proc.py` + builder/врезки + ключей + `pid: host` — без следов в БД/схеме/
|
||||
контракте `/metrics`.
|
||||
|
||||
## Ссылки
|
||||
- BRD: `docs/work-items/ORCH-111/01-brd.md`
|
||||
- TRZ: `docs/work-items/ORCH-111/02-trz.md`
|
||||
- Acceptance: `docs/work-items/ORCH-111/03-acceptance-criteria.md`
|
||||
- Инфра: `docs/work-items/ORCH-111/07-infra-requirements.md`
|
||||
- Риски: `docs/work-items/ORCH-111/10-tech-risks.md`
|
||||
- Сквозной ADR: `docs/architecture/adr/adr-0041-watchdog-orphan-test-process-alert.md`
|
||||
- Связанные ADR: `adr-0033` (sidecar-watchdog F1b — машина `decide`/`AlertState`/never-raise),
|
||||
`adr-0030` (контракт `/metrics`/`schema_version` — НЕ трогаем), `adr-0024` (disk-watchdog — образец
|
||||
pure-решающей функции + dedup), `adr-0040` (ORCH-109 timeout-kill `-9` — источник осиротения).
|
||||
- Сверено по коду: `watchdog/{core,signals,config,decision,notify}.py`,
|
||||
`watchdog/collectors/{host,orch}.py`, `docker-compose.yml` (сервис `orchestrator-watchdog`),
|
||||
`src/merge_gate.py::retest_branch`, `src/coverage_gate.py::measure_coverage`,
|
||||
`src/config.py` (`merge_retest_timeout_s=600`, `coverage_run_timeout_s=900`),
|
||||
`tests/watchdog/test_compose_service.py`, `tests/test_lite_setup_doc.py`.
|
||||
</content>
|
||||
62
docs/work-items/ORCH-111/07-infra-requirements.md
Normal file
62
docs/work-items/ORCH-111/07-infra-requirements.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
work_item: ORCH-111
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-15
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 07 — Инфра-требования: ORCH-111 — watchdog-алерт на долго живущий тест-процесс
|
||||
|
||||
Work Item: **ORCH-111** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
> Применимо: меняется топология контейнера-наблюдателя (`pid: host`) и добавляются env-ключи
|
||||
> watchdog. Решение — `06-adr/ADR-001` + сквозной `adr-0041`.
|
||||
|
||||
## I-1. Топология / окружения
|
||||
- **Изменение:** сервису `orchestrator-watchdog` в `docker-compose.yml` добавляется **`pid: host`**
|
||||
(ADR-001 D6). Под `pid: host` контейнерный `/proc` отражает корневой PID-namespace хоста →
|
||||
watchdog видит таблицу процессов хоста (включая осиротевшие pytest, репарентированные на tini
|
||||
orchestrator-контейнера). **Отдельный mount `/proc` не требуется** (procfs зависит от namespace
|
||||
читателя).
|
||||
- `pid: host` — **НЕ volume** → существующий тест `tests/watchdog/test_compose_service.py::
|
||||
test_host_paths_mounted_read_only` (требует `:ro` на каждом volume) остаётся зелёным; разработчик
|
||||
добавляет позитивный тест на наличие `pid: host`.
|
||||
- Привилегия — **только у наблюдателя** (`orchestrator-watchdog`). Прод `orchestrator` и
|
||||
`orchestrator-staging` **не** меняются. Прочие тома/порты/сеть watchdog — без изменений
|
||||
(`network_mode: host`, `docker.sock:ro`, `/repos:ro`, `./data:ro`, `mem_limit: 128m`).
|
||||
- **Тираж (Lite/Bundled, ORCH-102/103):** `pid: host` входит в канонический compose sidecar; сигнал
|
||||
дефолт-off → нулевое изменение поведения у тиражных инсталляций.
|
||||
|
||||
## I-2. Переменные окружения / секреты
|
||||
- **Новые ключи** (ADR-001 D5), парсеры never-raise:
|
||||
| Ключ | Дефолт | Смысл |
|
||||
|------|--------|-------|
|
||||
| `WATCHDOG_PROC_ENABLED` | `false` | kill-switch / осознанный opt-in |
|
||||
| `WATCHDOG_PROC_AGE_MIN` | `60` | порог возраста (мин); **обязан** > `max(merge_retest_timeout_s=600, coverage_run_timeout_s=900)/60` |
|
||||
| `WATCHDOG_PROC_PATTERNS` | `pytest` | CSV паттернов cmdline (substring-матч) |
|
||||
| `WATCHDOG_PROC_COOLDOWN_S` | `1800` | per-signal cooldown |
|
||||
- **Канон тиража (NFR-5 / AC-10):** ключи добавить в **том же PR** в `.env.watchdog.example` **И** в
|
||||
блок `WATCHDOG_*` `.env.example` (равенство множеств держит `tests/test_lite_setup_doc.py`
|
||||
TC-02b) + описать в `docs/deployment/LITE_SETUP.md` и `docs/architecture/README.md`.
|
||||
- **Секреты:** новых нет. Канал алертов — существующий `WATCHDOG_TG_*` (свой бот sidecar, C-1).
|
||||
- **На прод-хосте (разово, человек):** в `.env.watchdog` выставить `WATCHDOG_PROC_ENABLED=true`
|
||||
(включение сигнала; без этого — дремлет).
|
||||
|
||||
## I-3. Деплой / рестарт
|
||||
- **Self-hosting инвариант (NFR-3):** выкат пересобирает/рестартит **только** контейнер
|
||||
`orchestrator-watchdog` (`docker compose up -d --build orchestrator-watchdog`). Прод-контейнер
|
||||
`orchestrator` **НЕ** перезапускается (иначе встанет конвейер всех проектов, включая enduro).
|
||||
- Прод-выкат sidecar — через staging-эквивалент (smoke на staging-хосте перед прод): проверить, что
|
||||
под `pid: host` коллектор видит процессы, сигнал поднимается на синтетическом старом pytest и молчит
|
||||
при дефолт-off.
|
||||
- **Откат:** `WATCHDOG_PROC_ENABLED=false` (мгновенный; привилегия дремлет) либо снятие `pid: host` +
|
||||
рестарт только sidecar.
|
||||
|
||||
## I-4. CI/CD
|
||||
- Без изменений `.gitea/workflows/`. Новые тесты — `tests/watchdog/test_proc_blocking_signal.py`,
|
||||
`tests/watchdog/test_tick_proc_blocking_integration.py` (TRZ §2) — исполняются существующим
|
||||
`pytest tests/`. Key-sync (`test_lite_setup_doc.py`) и compose-тесты watchdog должны остаться
|
||||
зелёными.
|
||||
</content>
|
||||
38
docs/work-items/ORCH-111/10-tech-risks.md
Normal file
38
docs/work-items/ORCH-111/10-tech-risks.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
work_item: ORCH-111
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-15
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 10 — Технические риски: ORCH-111 — watchdog-алерт на долго живущий тест-процесс
|
||||
|
||||
Work Item: **ORCH-111** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
> Информационный (гейтом не парсится). Риски реализации и их митигейшн.
|
||||
|
||||
## Реестр рисков
|
||||
|
||||
| ID | Риск | Вер. | Влия. | Митигейшн |
|
||||
|----|------|------|-------|-----------|
|
||||
| TR-1 | **Расширение привилегии наблюдателя** (`pid: host`): sidecar видит таблицу процессов всего хоста (все контейнеры). | Сред. | Сред. | Привилегия **read-only** и **меньше**, чем уже-смонтированный `docker.sock` (полная интроспекция контейнеров); код читает **только** `/proc/<pid>/{stat,cmdline}`, **никогда** `/proc/<pid>/environ`; сигнал дефолт-off; обоснование в ADR-001 D1/D6 + adr-0041. (NFR-2/AC-3) |
|
||||
| TR-2 | **Утечка секретов через cmdline** в Telegram-алерт (если тест-команда содержит чувствительный аргумент). | Низ. | Сред. | Скоуп паттерна — `pytest` (не принимает секретов в аргументах); cmdline в detail **усекать** до ограниченного фрагмента; канал — приватный бот оператора (C-1). |
|
||||
| TR-3 | **Ложные срабатывания** на легитимном длинном прогоне → спам. | Низ. | Сред. | Порог возраста **обязан** > `max(merge_retest_timeout_s=600, coverage_run_timeout_s=900)`; дефолт `WATCHDOG_PROC_AGE_MIN=60` мин (4× запас); cmdline-скоуп; дедуп/cooldown через `decision.decide`/`AlertState` (BR-4/BR-5, по построению D2). |
|
||||
| TR-4 | **Кросс-namespace PID не совпадают** (`pid: host` даёт хост-глобальные PID; `/metrics agents[].pid` — namespace орка) → ненадёжная атрибуция «процесс активного джоба». | Сред. | Низ. | Атрибуция **не** через PID, а через порог возраста (namespace-агностичен) + cmdline-скоуп; дубль с `agent_hung` исключён по построению (claude ≠ pytest). ADR-001 D2. |
|
||||
| TR-5 | **Отсутствие RECOVERY** для исчезнувшего процесса (динамический per-entity ключ — как у `agent_hung`/`stage_stuck`, которые не recovery'ятся при пропадании сущности). | Сред. | Низ. | Синтез `Signal(active=False)` для `proc_blocking`-ключей, alerting=True но исчезнувших из наблюдаемых → один RECOVERY через `decide()` (ADR-001 D4, AC-6). Покрыть интеграционным тестом tick→recovery. |
|
||||
| TR-6 | **PID-recycling**: PID переиспользован после смерти орфана → ложная пара recovery+new-alert. | Низ. | Низ. | Естественный цикл vanish→recovery→new-alert корректен; опционально усилить ключ `("proc_blocking", pid, start_ticks)`. |
|
||||
| TR-7 | **never-raise регресс**: гонка «процесс умер между `listdir(/proc)` и `read`» или битый `/proc` роняет тик. | Низ. | Выс. | Per-pid guard (skip), top-level guard → `[]`; чистый разбор отделён от I/O и покрыт фикстурами; AC-8. |
|
||||
| TR-8 | **Дрейф канона тиража**: ключи добавлены в код, но не синхронизированы в `.env.example`/`.env.watchdog.example`/`LITE_SETUP.md`. | Сред. | Низ. | Key-sync `tests/test_lite_setup_doc.py` (TC-02b) красит PR; норматив NFR-5 «обновить в том же PR»; reviewer-ось ORCH-079 (доки). |
|
||||
| TR-9 | **Не-Linux / нет `/proc`** (тиражная инсталляция на не-Linux) → сигнал не работает. | Низ. | Низ. | Коллектор → `[]` (один сигнал тих, тик жив); сигнал дефолт-off; Linux-специфичность задокументирована (как CPU-liveness `agent_hung`). |
|
||||
|
||||
## Сводный вывод
|
||||
Доминирующий класс — **безопасность наблюдателя** (TR-1/TR-2: привилегия `pid: host` + видимость
|
||||
cmdline). Остаточный риск для прод-конвейера **низкий**: все изменения строго в наблюдателе
|
||||
(`watchdog/**` + сервис watchdog), read-only, never-raise, дефолт-off; `src/**` / `/metrics` /
|
||||
`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / схема БД — не тронуты; выкат не рестартит прод-`orchestrator`
|
||||
(NFR-3). Эскалация: рекомендуется лейбл **`arch:major-change`** (изменение топологии/привилегий
|
||||
наблюдателя + новый компонентный сигнал). Возврат в анализ **не требуется** — ТЗ удовлетворяется без
|
||||
нарушения принципов архитектуры (всё в Docker на одном сервере, stdlib-only, без новых зависимостей).
|
||||
</content>
|
||||
Reference in New Issue
Block a user