123 lines
11 KiB
Markdown
123 lines
11 KiB
Markdown
---
|
||
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 не затронут.
|