From 1c08b3f62ad07ca1bff2e5132ee4da9d1a31d12e Mon Sep 17 00:00:00 2001 From: claude-bot Date: Wed, 10 Jun 2026 08:49:58 +0300 Subject: [PATCH] architect(ET): auto-commit from architect run_id=565 --- docs/architecture/README.md | 48 +++ .../adr/adr-0033-sidecar-watchdog.md | 85 +++++ .../06-adr/ADR-001-sidecar-watchdog.md | 304 ++++++++++++++++++ .../ORCH-100/07-infra-requirements.md | 93 ++++++ .../ORCH-100/08-data-requirements.md | 40 +++ docs/work-items/ORCH-100/10-tech-risks.md | 44 +++ 6 files changed, 614 insertions(+) create mode 100644 docs/architecture/adr/adr-0033-sidecar-watchdog.md create mode 100644 docs/work-items/ORCH-100/06-adr/ADR-001-sidecar-watchdog.md create mode 100644 docs/work-items/ORCH-100/07-infra-requirements.md create mode 100644 docs/work-items/ORCH-100/08-data-requirements.md create mode 100644 docs/work-items/ORCH-100/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 3ef112d..a54c24c 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -20,6 +20,7 @@ - **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane. Резолв статусов проекта `get_project_states` (ORCH-10) кэширует `{logical_key→uuid}` per-project; **ORCH-068** добавляет в кэш-запись `{uuid→group}` (для терминал-исключения F-2) и **TTL** `ORCH_PLANE_STATES_TTL_S` (дефолт 300с; `0` → прежний lifetime-кэш) — устаревший набор статусов самозалечивается без рестарта процесса через существующий `reload_project_states()` (баг кэша после появления нового Plane-статуса). Форма возврата `get_project_states` неизменна (обратная совместимость). - **FS ownership detect** (`src/fs_normalize.py`, ORCH-057 — [adr-0031](adr/adr-0031-legacy-ownership-normalization.md)) — чистый **never-raise** leaf (паттерн `serial_gate`/`preflight`), закрывает пробел ORCH-040: при миграции на `user: "1000:1000"` legacy `root:root` файлы в `/repos` ломали создание worktree под uid 1000 (`ensure_worktree` → сырой `fatal: … Permission denied`, агент не стартовал). Три слоя: (1) **D1** — `src/git_worktree.py::ensure_worktree` классифицирует класс «нет прав» (`Permission denied`/`could not create leading directories`/`insufficient permission`/`EACCES`/`EPERM`) и поднимает actionable `RuntimeError` с причиной + лечащей командой (не-прав-ошибки сохраняют прежний контракт — меняется только формулировка, не факт сбоя); (2) **D2** — `scan_ownership(roots, target_uid=os.getuid())` обходит `/repos/_wt`, `/.git/{objects,worktrees}`, `data/runs` с ранним выходом при первом `st_uid != target_uid` + TTL-кэш; (3) **D3** — best-effort вызов на старте `main.lifespan` → WARNING + Telegram при mismatch (claim **НЕ** блокируется — внятный ранний отказ даёт D1 в точке launch, знающей repo; preflight-блок отвергнут как repo-слепой → регресс enduro). Опц. `normalize()` chown'ит только при `CAP_CHOWN` (под uid 1000 — no-op; init-контейнер/root-entrypoint отвергнуты — реинтродукция root-контекста + self-deploy compose). Фактическая нормализация = **операторская процедура** под root на хосте (`INFRA.md` «Миграция uid»). Условность `applies(repo)` first: `fs_normalize_enabled` (kill-switch) + `fs_normalize_repos` (CSV, пусто → self-hosting only). Наблюдаемость — блок `fs_ownership` в `GET /queue`; опц. `POST /fs-normalize/check`. `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД — не тронуты. Детали — `docs/work-items/ORCH-057/06-adr/ADR-001-legacy-ownership-normalization.md`. - **Metrics endpoint** (`src/metrics.py` + `GET /metrics`, ORCH-099 — [adr-0030](adr/adr-0030-metrics-endpoint.md)) — лёгкий **read-only** leaf-сборщик (`build_metrics() -> dict`, never-raise по разделам, паттерн `serial_gate.snapshot()`) + тонкий эндпоинт (стиль `GET /queue`). Отдаёт JSON-«сырьё» о самом орке (стадии задач / очередь jobs / agent-liveness / стоимость-токены) как **стабильный машинный контракт для sidecar F1b** (`watchdog/`, отдельная задача — наблюдатель отделён от наблюдаемого). Только чтение существующих `tasks`/`jobs`/`agent_runs` + in-memory-снапшотов (`worker.breaker`); два read-only helper'а в `db.py` (`get_running_agents`/`agent_cost_totals`). Логику мониторинга (пороги/алерты/история/Telegram) НЕ несёт — это F1b. Контракт ниже (§ «Сырьё-эндпоинт `/metrics`»). Kill-switch `metrics_endpoint_enabled` (дефолт `True`). `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — не тронуты. +- **Sidecar-watchdog F1b** (`watchdog/` + сервис `orchestrator-watchdog`, ORCH-100 — [adr-0033](adr/adr-0033-sidecar-watchdog.md)) — **мозг мониторинга в ОТДЕЛЬНОМ контейнере** (наблюдатель отделён от наблюдаемого, C-1): код в репо орка (`watchdog/`), рантайм — свой образ (`watchdog/Dockerfile`, `python:3.12-slim`, **stdlib-only**) + сервис в `docker-compose.yml` (`network_mode: host`, read-only `docker.sock`, `mem_limit: 128m`). На каждом тике собирает 4 источника: `GET /metrics` орка (F1a/ORCH-099), хост (диск/inode/память/CPU, stdlib), статусы контейнеров через read-only `docker.sock` (GET-only, без `docker` SDK), пинг Plane/Gitea/Anthropic. Каждый сигнал → **обобщённая чистая** `decide(signal_active, prev, now, cooldown)` (генерализация `disk_watchdog.decide_action`, per-signal in-memory `AlertState`) → алерт в **собственный** Telegram-канал sidecar (`WATCHDOG_TG_*`, **НЕ** импорт `src/notifications.py`). Особый сигнал `orch_down` — `/metrics` не отвечает (наблюдатель жив, наблюдаемый лёг). Диск: штатные 85% остаются за `disk_watchdog` (ORCH-063, нулевой дубль), sidecar — `orch_down` + opt-in потолок 97% (default off). never-raise, kill-switch `WATCHDOG_ENABLED`, строго read-only к наблюдаемому; `src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД орка — не тронуты. Подробнее ниже (§ «Sidecar-watchdog F1b»). Детали — `docs/work-items/ORCH-100/06-adr/ADR-001-sidecar-watchdog.md`. ## Сырьё-эндпоинт `/metrics` для sidecar (ORCH-099 — design) @@ -73,6 +74,53 @@ F1b (рамка C-1: наблюдатель отделён от наблюдае Подробнее: [adr-0030](adr/adr-0030-metrics-endpoint.md), детально — `docs/work-items/ORCH-099/06-adr/ADR-001-metrics-endpoint.md`. +## Sidecar-watchdog F1b (ORCH-100 — design) + +**Вторая половина пары наблюдаемости.** F1a (ORCH-099) отдаёт сырьё через `GET /metrics`; F1b — мозг, +который это сырьё читает, дополняет внешними сигналами и превращает в алерты. Ключевая рамка +заказчика — **наблюдатель отделён от наблюдаемого** (C-1): частичные стражи (`disk_watchdog`/`reaper`/ +`reconciler`) живут ВНУТРИ процесса орка и лягут вместе с ним; sidecar в отдельном контейнере +переживает падение орка и делает наблюдателя **громче** в инцидент. + +- **Рантайм:** код в `watchdog/` (репо орка), но **отдельный контейнер** `orchestrator-watchdog` + (свой `watchdog/Dockerfile`, `python:3.12-slim`, **stdlib-only** — без сторонних зависимостей, + C-3 «тонкий стек, НЕ Grafana/Prometheus»). `network_mode: host` → `/metrics` достижим как + `http://127.0.0.1:8500/metrics`; `docker.sock` смонтирован **read-only**; `mem_limit: 128m`; + `restart: unless-stopped`. +- **4 коллектора на тик:** (a) `GET /metrics` орка (толерантный парсинг конверта F1a — неизвестные + ключи игнор, рост `schema_version` → warning); (b) хост — диск (`shutil.disk_usage`)/inode/память + (`/proc/meminfo`)/CPU; (c) контейнеры через read-only `docker.sock` — **только** GET list/inspect + (Up/healthy/restarting/exited/unhealthy), без `docker` SDK; (d) пинг Plane/Gitea/Anthropic. +- **Решение — обобщённая чистая функция** `decide(signal_active, prev, now, cooldown) -> alert | + realert | recovery | none` (строгая генерализация `src/disk_watchdog.py::decide_action`; + per-signal in-memory `AlertState`, рестарт → корректный повторный алерт стоящей проблемы). Реестр + сигналов: `orch_down` (K подряд неудачных опросов), `host_mem`, `host_disk_crit` (opt-in потолок), + `agent_hung` (доля CPU из Δ`cpu_ticks`/`clk_tck`/Δ`generated_at` < floor при растущем `runtime_s` — + sidecar stateful-арбитр), `stage_stuck` (`age_in_stage_s`), `job_failed` (edge), `queue_depth`, + `container_down` (per name), `dep_down` (per name). Пороги/интервалы/URL — из env (`WATCHDOG_*`). +- **`orch_down` — главный сигнал:** `/metrics` не отвечает (таймаут/refused/5xx/нечитаемо) → алерт + «орк не отвечает» через ту же машину порога/дедупа/recovery. Наблюдатель жив, наблюдаемый лёг. +- **Независимый Telegram-канал:** свои `WATCHDOG_TG_BOT_TOKEN`/`WATCHDOG_TG_CHAT_ID`; **запрещено** + импортировать `src/notifications.py` или использовать токен орка (иначе падение орка утянуло бы и + алерт-канал — нарушение C-1). +- **Владелец диск-алерта (BR-10, ADR-001 D6):** штатные 85% — ЕДИНСТВЕННО за внутренним + `disk_watchdog` (ORCH-063, канал орка) ⇒ **нулевой дубль по построению**; sidecar покрывает провал + «орк+disk_watchdog мертвы» через `orch_down`, плюс **opt-in** независимый критический потолок + `host_disk_crit` (97%, `WATCHDOG_DISK_CRIT_ENABLED=false` по умолчанию) — другое событие/канал. +- **Гарантии:** 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 не + затронут). `src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схема БД орка — **не тронуты** + (F1b вне процесса орка и вне конвейера QG — как `disk_watchdog`/`reaper`/`reconciler`). Деплой + sidecar НЕ рестартит прод-контейнер `orchestrator`; прод-выкат — через staging-гейт (8501). +- **Инфра-предусловие (разовое, человек):** добавить сервис в compose, создать bot/chat watchdog, + смонтировать `docker.sock` `:ro` + хост-пути, первый запуск на хосте — + `docs/work-items/ORCH-100/07-infra-requirements.md`. + +Подробнее: [adr-0033](adr/adr-0033-sidecar-watchdog.md), детально — +`docs/work-items/ORCH-100/06-adr/ADR-001-sidecar-watchdog.md`, +`docs/work-items/ORCH-100/07-infra-requirements.md`. + ## Конвейер и Quality Gates ``` diff --git a/docs/architecture/adr/adr-0033-sidecar-watchdog.md b/docs/architecture/adr/adr-0033-sidecar-watchdog.md new file mode 100644 index 0000000..d2fd36d --- /dev/null +++ b/docs/architecture/adr/adr-0033-sidecar-watchdog.md @@ -0,0 +1,85 @@ +--- +work_item: ORCH-100 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# adr-0033: Sidecar-watchdog F1b — мозг мониторинга в отдельном контейнере + +- **Статус:** proposed +- **Дата:** 2026-06-10 +- **Задача:** ORCH-100 (FND/F1b) +- **Детальный ADR:** `docs/work-items/ORCH-100/06-adr/ADR-001-sidecar-watchdog.md` +- **Парный ADR:** `adr-0030` (F1a `/metrics` — источник сырья) + +## Контекст +Домен 0 «Фундамент» эпика автономного саморазвития, рамка наблюдаемости заказчика: **наблюдатель +отделён от наблюдаемого**. F1a (adr-0030) отдаёт read-only `GET /metrics` — **только сырьё**. F1b — +**мозг**: читает сырьё, дополняет внешними сигналами (хост/контейнеры/зависимости), решает по порогам, +алертит. Частичные стражи (`disk_watchdog`/`reaper`/`reconciler`) живут ВНУТРИ процесса орка — орк +завис/упал ⇒ они мертвы, платформа слепа в критический момент. Рамки: C-1 (отдельный контейнер, код в +`watchdog/`), C-2 (без внешнего плеча — принятый риск), C-3 (тонкий стек, НЕ Grafana/Prometheus; хост +впритык). Критический инвариант: орк лёг ⇒ `/metrics` недоступен = **сам сигнал тревоги**. + +## Решение +Новая папка `watchdog/` — **тонкий Python-3.12-stdlib демон** (без сторонних зависимостей), отдельный +образ `watchdog/Dockerfile` + сервис `orchestrator-watchdog` в `docker-compose.yml` (`network_mode: +host`, read-only `docker.sock`, `mem_limit: 128m`, `restart: unless-stopped`). Тик: (1) `GET /metrics`; +(2) хост (диск/inode/память/CPU, stdlib); (3) статусы контейнеров через read-only `docker.sock` +(GET-only — без `docker` SDK); (4) пинг Plane/Gitea/Anthropic. Сигналы проходят через **обобщённую +чистую** `decide(signal_active, prev, now, cooldown) -> alert|realert|recovery|none` (генерализация +`disk_watchdog.decide_action`; per-signal in-memory `AlertState`). Алерт — в **собственный** Telegram- +канал sidecar (свои `WATCHDOG_TG_*`; **НЕ** импорт `src/notifications.py`). Особый сигнал — `/metrics` +не отвечает → `orch_down`. Всё never-raise (per-source/per-tick/per-send), под kill-switch +`WATCHDOG_ENABLED`, строго read-only к наблюдаемому. **`src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/ +`check_*`/схема БД орка — не тронуты** (F1b вне процесса орка и вне конвейера QG). + +- **Стек** — Python stdlib (`urllib`, `socket`+`http.client` для docker.sock, `shutil.disk_usage`, + `/proc/meminfo`); pytest на чистые функции. Отвергнуты Go / `docker` SDK / Prometheus (C-3). +- **Реестр сигналов** — `orch_down` (K подряд неудачных опросов), `host_mem`/`host_disk_crit`, + `agent_hung` (Δ`cpu_ticks`/`clk_tck`/Δ`generated_at` < floor при растущем `runtime_s`; нужно 2 + опроса — sidecar stateful-арбитр), `stage_stuck` (`age_in_stage_s`), `job_failed` (edge), + `queue_depth`, `container_down` (per name), `dep_down` (per name). Пороги/интервалы/URL — из env. +- **Владелец диск-алерта (BR-10)** — штатные 85% остаются за внутренним `disk_watchdog` (ORCH-063, + канал орка) ⇒ **нулевой дубль по построению**; sidecar покрывает провал «орк+disk_watchdog мертвы» + через `orch_down`, плюс **opt-in** (default off) независимый критический потолок `host_disk_crit` + (97%) — другое событие/канал, не повтор 85%. +- **Толерантность контракта** — неизвестные ключи `/metrics` игнорируются, отсутствие опционального не + ошибка, рост `schema_version` → warning (зеркало аддитивной политики adr-0030). +- **Kill-switch** `WATCHDOG_ENABLED=false` → демон инертен (idle-loop, не exit) ⇒ нулевой эффект. + +## Альтернативы +- **Go / `docker` SDK / `requests`** — отклонено: вес/вторая цепочка против C-3 и консистентности с + `disk_watchdog`. +- **Prometheus/Grafana/TSDB** — отклонено: прямой запрет C-3. +- **Sidecar — единственный владелец диска** — отклонено: потеря покрытия, когда сам sidecar/Docker + недоступен; выбрана связка primary `disk_watchdog` + opt-in ceiling. +- **Push из орка в sidecar** — отклонено: зависший орк не пушит; pull падает = сам сигнал `orch_down`. +- **bridge + `host.docker.internal`** — отклонено: на Linux ненадёжно; `network_mode: host` проще. +- **Своя БД/файл порогов** — отклонено: C-3; in-memory best-effort достаточно (как `disk_watchdog`). + +## Последствия +- Внешний мозг мониторинга переживает падение орка; `orch_down` делает наблюдателя громче в инцидент. +- Строго read-only + независимый канал + never-raise ⇒ self-hosting-безопасно (enduro не затронут); + падение sidecar не влияет на конвейер. +- Аддитивно/обратимо: `src/**`/гейты/схема байт-в-байт; kill-switch → нулевая регрессия; дубль диска + исключён структурно. +- Плата: новый контейнер на впритык-хосте (`mem_limit: 128m` + замер RSS на staging обязательны); + C-2 (падёт хост → молчит и sidecar); новая поверхность совместимости `/metrics`↔F1b (толерантный + парсинг + единый репо контракта); CPU-liveness Linux-специфичен. +- **Топология** меняется (новый контейнер) → `07-infra-requirements.md`; **схема БД** не меняется → + 08 = N/A. Новый компонент + контейнер + канал → `arch:major-change`; прод-выкат через staging-гейт + (8501), деплой sidecar НЕ рестартит прод-контейнер. +- **Откат:** не запускать сервис / `WATCHDOG_ENABLED=false` (мгновенный) или удаление `watchdog/` + + сервиса + env — без следов в БД/схеме. + +## Связи +adr-0030 (F1a `/metrics` — парный источник сырья; контракт `cpu_ticks`/`clk_tck`/`generated_at`/ +`schema_version`), adr-0024 (`disk_watchdog` — образец решающей функции/never-raise + владелец +диск-алерта), adr-0025 (build-cache-pruner — паттерн «вторая половина»), adr-0017 (serial_gate — +leaf `snapshot()`/never-raise), adr-0011 (job-reaper — pid/liveness-семантика). Прямой источник — +**F1a** (`GET /metrics`); F1b — его потребитель. + diff --git a/docs/work-items/ORCH-100/06-adr/ADR-001-sidecar-watchdog.md b/docs/work-items/ORCH-100/06-adr/ADR-001-sidecar-watchdog.md new file mode 100644 index 0000000..6f9b368 --- /dev/null +++ b/docs/work-items/ORCH-100/06-adr/ADR-001-sidecar-watchdog.md @@ -0,0 +1,304 @@ +--- +work_item: ORCH-100 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# ADR-001: Sidecar-watchdog F1b — мозг мониторинга в отдельном контейнере + +Work Item: **ORCH-100** — FND/F1b: sidecar-watchdog (мозг мониторинга, отдельный контейнер) +Стадия: **architecture** +Сквозная регистрация: **`docs/architecture/adr/adr-0033-sidecar-watchdog.md`** (решение +кросс-каттинговое — новый компонент наблюдаемости + новый рантайм-контейнер + новый независимый +алерт-канал; парный к adr-0030 F1a). + +## Статус +Proposed + +## Контекст + +F1b — вторая половина пары наблюдаемости домена 0 «Фундамент» эпика автономного саморазвития. **F1a +(ORCH-099, adr-0030)** уже отдаёт лёгкий read-only `GET /metrics` — **только сырьё** (стадии, +очередь, agent-liveness, cost) в версионированном конверте. F1b — **мозг**, который это сырьё читает, +дополняет внешними сигналами (хост, контейнеры, зависимости) и превращает в **алерты**. + +Рамка заказчика (Слава, 09.06) — **установленный факт, не предмет переизобретения** (BRD §1): +- **C-1 / C-1б:** наблюдатель ОТДЕЛЁН от наблюдаемого. Код sidecar — в репо орка (`watchdog/`), + рантайм — **ОТДЕЛЬНЫЙ контейнер** (`orchestrator-watchdog`). Изоляция на уровне контейнера. +- **C-2:** без внешнего плеча (один хост; принятый риск — падёт весь хост → молчит и наблюдатель). +- **C-3:** тонкий стек — **НЕ Grafana/Prometheus/TSDB**. Хост впритык (RAM 171Mi free / 7.7Gi, диск 92%). +- **Критический инвариант:** падение/зависание орка делает sidecar **громче**, а не тише — орк лёг ⇒ + `/metrics` недоступен = **сам сигнал тревоги** «орк не отвечает». + +Факты, сверенные с кодом: +- Орк работает `network_mode: host`, порт 8500 (`docker-compose.yml:14`) ⇒ из host-network sidecar + `/metrics` достижим как `http://127.0.0.1:8500/metrics`. +- `docker.sock` на хосте `/var/run/docker.sock`, уже монтируется в орк (`docker-compose.yml:18`). +- `src/disk_watchdog.py::decide_action(used_pct, threshold, prev, now, realert_s)` — эталонная + чистая решающая функция `alert | realert | recovery | none` + `PathAlertState` (in-memory + анти-спам) + трёхуровневый never-raise (per-path / per-tick / per-send). BRD §BR-9 прямо предписывает + её как образец. +- Диск уже алертит `disk_watchdog` (ORCH-063) на 85% **через Telegram орка** — потенциальный дубль + (BR-10), требует явного выбора владельца. +- `/metrics`-конверт (adr-0030 D2): `schema_version`/`generated_at`/`clk_tck`/`stages`/`queue`/ + `agents`/`cost`/`enabled`; CPU-сырьё — `cpu_ticks` (utime+stime из `/proc`), орк **дельту не считает** + (stateless) — арбитр «жив/завис» это **F1b** (sidecar считает долю CPU по двум опросам). + +«Как есть» не годится: частичные стражи (`disk_watchdog`/`reaper`/`reconciler`) живут **ВНУТРИ +процесса орка** — зависнет/упадёт орк, лягут и они, и платформа слепа именно в критический момент. + +## Решение + +### Сводка + +Новая папка `watchdog/` в репо орка — **тонкий Python-3.12-stdlib демон** (никаких сторонних +зависимостей), собираемый в отдельный образ (`watchdog/Dockerfile`) и поднимаемый сервисом +`orchestrator-watchdog` в `docker-compose.yml` (свой процесс/память/рестарт, `network_mode: host`, +read-only `docker.sock`). На каждом тике: (1) `GET /metrics` орка; (2) хост (диск/inode/память/CPU); +(3) статусы контейнеров через read-only `docker.sock`; (4) пинг Plane/Gitea/Anthropic. Каждый сигнал +проходит через **обобщённую чистую решающую функцию** (генерализация `disk_watchdog.decide_action`) с +per-signal in-memory дедупом/throttle/recovery и шлёт алерт в **собственный** Telegram-канал sidecar. +Особый сигнал — `/metrics` не отвечает → `orchestrator_down`. Всё never-raise, под kill-switch, +строго read-only к наблюдаемому. **`src/**` не меняется** — F1b потребитель `/metrics`; +`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схема БД орка — **не тронуты**. + +### D1 — Стек: Python 3.12 stdlib-only, отдельный тонкий образ (BR-1, NFR-2, C-3) + +**Решение: Python 3.12 + только стандартная библиотека** на базе `python:3.12-slim`. +- `urllib.request` — HTTP к `/metrics` и пинги зависимостей (короткие таймауты). +- `docker.sock` — **сырой HTTP-over-unix-socket** через stdlib (`socket.AF_UNIX` + + `http.client.HTTPConnection`-подкласс), БЕЗ pip-пакета `docker`. Только `GET /containers/json` и + `GET /containers//json` ⇒ read-only **по построению** (нет ни одного мутирующего вызова). +- Хост-метрики — `shutil.disk_usage` (как `disk_watchdog`), `/proc/meminfo`, `/proc/loadavg` / + `os.getloadavg` — stdlib, без тяжёлых агентов. +- Telegram — `urllib` POST на `api.telegram.org`. +- Тесты — `pytest` на чистые функции (решение/парсинг конверта/детект down), как `disk_watchdog.decide`. + +Обоснование: BRD §BR-9 фиксирует `disk_watchdog.decide` как образец — Python даёт почти дословный +перенос паттерна, переиспользует экспертизу команды и pytest, держит образ тонким (stdlib-only ⇒ нет +дерева зависимостей). **Отвергнуто:** Go (вторая цепочка инструментов/языка ради ~десятков МБ RSS — +не оправдано на фоне C-1-консистентности с `disk_watchdog`); `docker` SDK / `requests` / `httpx` +(вес и поверхность зависимостей против C-3); Prometheus/Grafana/TSDB (прямой запрет C-3). + +Привязка: BR-1, NFR-2, FR-1, AC-1, AC-4. + +### D2 — Топология контейнера: `network_mode: host` + read-only docker.sock + `mem_limit` (BR-1/3/4, NFR-2/4) + +Сервис `orchestrator-watchdog` (`docker-compose.yml`): +- `build: ./watchdog`, `container_name: orchestrator-watchdog`, `restart: unless-stopped` + (самовосстановление, FR-1). +- **`network_mode: host`** — как орк ⇒ `/metrics` достижим как `http://127.0.0.1:8500/metrics` + (дефолт, конфигурируем), и доступны хост-интерфейсы. Отвергнут bridge + `host.docker.internal` + (на Linux ненадёжно, лишняя сложность). +- **`/var/run/docker.sock:/var/run/docker.sock:ro`** — read-only mount (NFR-4, AC-6); даже при + read-only mount код делает **только** GET-запросы (двойная гарантия). +- **Хост-пути для дисковых метрик** — read-only bind тех же путей, что меряет `disk_watchdog` + (`/repos`, `/app/data`/`./data`), `:ro` ⇒ `shutil.disk_usage` видит хост-ФС, но не может писать. +- **`mem_limit: 128m`** (+ `mem_reservation: 32m`) — тонкость измерима и принудительна (NFR-2). + Ожидаемый базовый RSS однопоточного stdlib-демона ~40–60 МБ; 128 МБ — потолок с запасом, но далеко + от Grafana-класса. OOM при превышении = ранний сигнал «sidecar растолстел» (см. 10-tech-risks TR-4). +- `env_file: .env.watchdog` (или общий `.env` с префиксом `WATCHDOG_`; точный файл — деталь + инфра-предусловия 07). Свои токен/chat — **только** у sidecar. +- **Self-hosting:** добавление нового сервиса и `docker compose up -d orchestrator-watchdog` + поднимает ТОЛЬКО watchdog — прод-контейнер `orchestrator` НЕ пересобирается и НЕ рестартится + (отдельный сервис). Это снимает риск «деплой наблюдателя уронил наблюдаемого». + +Привязка: BR-1, BR-3, BR-4, NFR-2, NFR-4, FR-1, FR-4, FR-5, AC-1, AC-4, AC-6. + +### D3 — Структура кода `watchdog/` (NFR-3, NFR-7) + +``` +watchdog/ + Dockerfile # python:3.12-slim, COPY watchdog/, ENTRYPOINT демон + __main__.py # цикл: tick loop, kill-switch, per-tick never-raise, лог старта/тика + config.py # чтение WATCHDOG_* env (пороги/интервалы/токены/URL/kill-switch), дефолты + collectors/ + orch.py # GET /metrics -> распарсенный конверт | сигнал orchestrator_down + host.py # диск (shutil.disk_usage) / inode / память (/proc/meminfo) / CPU (loadavg) + containers.py # docker.sock (ro) GET list/inspect -> статусы Up/healthy/restarting/exited/unhealthy + deps.py # пинг Plane/Gitea/Anthropic (urllib, короткий таймаут) + decision.py # ЧИСТАЯ decide(...) + AlertState (генерализация disk_watchdog) + notify.py # независимый Telegram-транспорт (свой токен/chat; НЕ импорт src/notifications) + tests/ # pytest на чистые функции (или tests/watchdog/ — на усмотрение developer) +``` + +Никакого импорта из `src/**` (иначе падение/рефактор орка утянул бы sidecar — нарушение C-1). +Логирование старта/тика/каждого вердикта в stdout контейнера (NFR-7) — по логам видно, что sidecar +жив и почему (не)сработал алерт. + +Привязка: BR-8, NFR-1, NFR-3, NFR-7, FR-8, FR-11, AC-3. + +### D4 — Обобщённая чистая решающая функция (BR-6, BR-9, FR-7) — образец `disk_watchdog.decide_action` + +`disk_watchdog.decide_action` зашит на `used_pct >= threshold`. Для F1b сигналов много и они +разнотипны (булевы — «орк down», «контейнер unhealthy»; счётчики — «job-failed delta»; пороговые — +«память %», «agent завис N мин»). Поэтому **сравнение выносится наружу**, а функция работает с уже +вычисленным булевым `signal_active`: + +``` +def decide(signal_active: bool, prev: AlertState, now: float, cooldown_s: float) -> str: + # not alerting & active -> ALERT (пересечение порога) + # alerting & active & cooldown ок -> REALERT (повтор) + # alerting & active & в cooldown -> NONE (анти-спам) + # alerting & не active -> RECOVERY (возврат в норму) + # not alerting & не active -> NONE (норма) + +@dataclass +class AlertState: # 1:1 семантика PathAlertState + alerting: bool = False + last_alert_at: float | None = None +``` + +Это **строгая генерализация** disk-варианта (тот же набор исходов, та же cooldown/recovery-семантика, +тот же in-memory best-effort, инъецируемые `now`/`cooldown` для детерминированных тестов). Состояние — +карта `{signal_key -> AlertState}`, где `signal_key` идентифицирует сигнал: скаляр (`"orch_down"`, +`"host_mem"`) или кортеж для пер-сущностных (`("agent_hung", run_id)`, `("container_down", name)`, +`("stage_stuck", work_item)`, `("dep_down", dep_name)`). Рестарт sidecar сбрасывает карту → +корректно повторно алертит ещё стоящую проблему (как `disk_watchdog`; FR-7). + +Привязка: BR-6, BR-9, FR-7, AC-2, TC-01…TC-04. + +### D5 — Реестр сигналов и их пороги (BR-2/3/4/5/6/7, FR-2…FR-7) + +| signal_key | Источник | `signal_active` когда | Порог (env, дефолт) | +|------------|----------|------------------------|----------------------| +| `orch_down` | collectors/orch | K подряд неудачных `/metrics` (таймаут/refused/5xx/нечитаемо) | `WATCHDOG_ORCH_DOWN_TICKS=3` | +| `host_mem` | host | `mem_used_pct >= порог` | `WATCHDOG_MEM_PCT=90` | +| `host_disk_crit` | host | `disk_used_pct >= ceiling` (**opt-in, см. D6**) | `WATCHDOG_DISK_CRIT_PCT=97`, `WATCHDOG_DISK_CRIT_ENABLED=false` | +| `agent_hung` (per run_id) | orch.agents | `runtime_s > N` И доля CPU (Δ`cpu_ticks`/`clk_tck`/Δ`generated_at`) `< floor` | `WATCHDOG_AGENT_HUNG_MIN=20`, `WATCHDOG_AGENT_CPU_FLOOR=0.01` | +| `stage_stuck` (per work_item) | orch.stages | `age_in_stage_s > порог` | `WATCHDOG_STAGE_STUCK_MIN=120` | +| `job_failed` | orch.queue | `counts.failed` вырос с прошлого тика (edge) | — (дельта; алерт на рост) | +| `queue_depth` | orch.queue | `depth >= порог` | `WATCHDOG_QUEUE_DEPTH=20` | +| `container_down` (per name) | containers | статус ∉ {running, healthy} (restarting/exited/unhealthy) | список `WATCHDOG_CONTAINERS=orchestrator` | +| `dep_down` (per name) | deps | пинг неуспешен/таймаут | URL'ы/таймаут из env | + +- **`agent_hung`** требует **двух** опросов (stateful у sidecar) — sidecar хранит предыдущие + `(cpu_ticks, generated_at)` per run_id и считает долю CPU; `cpu_ticks: null` (pid мёртв/не-Linux — + adr-0030 D5) ⇒ сигнал не вычисляется (none), не ложная тревога. +- **`job_failed`** — edge-сигнал (рост счётчика), а не sustained-порог: при росте `failed` → ALERT + один раз; recovery как такового нет (это событие), поэтому состояние сбрасывается сразу после + отправки (alerting=False), чтобы следующий новый фейл снова алертил. +- Все пороги/интервалы/URL/таймауты/cooldown — из env (FR-10), канон в `.env.example`. + +Привязка: BR-2…BR-7, FR-2…FR-7, AC-1, AC-2. + +### D6 — Владелец диск-алерта: disk_watchdog остаётся основным; sidecar — opt-in критический потолок (BR-10, FR-9) — **ключевое решение** + +BRD §BR-10 / FR-9 / AC-5 явно делегируют выбор владельца архитектору. **Решение:** + +1. **Штатный диск-алерт на 85% остаётся ЕДИНСТВЕННО за внутренним `disk_watchdog` (ORCH-063), через + Telegram орка.** Sidecar **НЕ** запускает независимый диск-алерт на том же пороге ⇒ **нулевой дубль + по построению** (AC-5 удовлетворён структурно, а не throttle-эвристикой). +2. **Вклад sidecar в дисковую безопасность — покрытие именно того провала, который F1b и создаётся + закрывать:** когда орк (а с ним и in-process `disk_watchdog`) **завис/упал**, штатный диск-алерт + физически невозможен. Тогда срабатывает **`orch_down`** — мастер-сигнал sidecar с независимого + канала; его текст явно подсказывает «in-process стражи (диск/reaper/reconciler) тоже мертвы → + проверьте хост, включая диск». +3. **Крайний edge — орк жив, но его Telegram сломан** (диск растёт, `disk_watchdog` не может + доставить): sidecar несёт **opt-in** независимый алерт `host_disk_crit` на **более высоком** + пороге-потолке (дефолт 97%, **выключен по умолчанию** `WATCHDOG_DISK_CRIT_ENABLED=false`). Это + **другое событие** (критический потолок, независимый канал), а не повтор 85%-события ⇒ инвариант + «не более одного алерта на одно событие переполнения» сохранён. Включается оператором осознанно, + когда нужна избыточность канала. + +Итог: из коробки — ровно один владелец диска (`disk_watchdog`); резервирование канала — обратимый +opt-in. Решение и обоснование зафиксированы здесь (AC-5). + +Привязка: BR-10, FR-9, AC-5. + +### D7 — Независимый Telegram-транспорт (BR-8, NFR-4, FR-8) + +`watchdog/notify.py` читает **свои** `WATCHDOG_TG_BOT_TOKEN` / `WATCHDOG_TG_CHAT_ID` из env и шлёт +через `urllib` POST на `api.telegram.org`. **Запрещено** импортировать `src/notifications.py` или +использовать токен/функции/чат орка — иначе падение/рефактор орка утянул бы алерт-канал (нарушение +C-1, прямой смысл BR-8). Отсутствие токена/chat → sidecar логирует и не шлёт (fail-safe), но **не +падает** (NFR-3). Сообщение несёт суть: сигнал, значение, порог, хост/контейнер. + +Привязка: BR-8, NFR-4, FR-8, AC-2, AC-6. + +### D8 — Three-level never-raise + kill-switch (NFR-3, NFR-5, FR-10, FR-11) + +- **per-source:** битый коллектор (орк down / docker.sock недоступен / пинг таймаут) деградирует + ОДИН сигнал, прочие собираются (`orch_down` сам по себе — нормальный сигнал, а не крах тика). +- **per-tick:** внешний `try/except` цикла — ошибка тика логируется, не валит демон. +- **per-send:** обёрнутый `notify` — сбой Telegram логируется и проглатывается (best-effort). +- **Kill-switch** `WATCHDOG_ENABLED` (env): `false` → демон **инертен** (idle-loop с логом «disabled», + НЕ `exit`, чтобы `restart: unless-stopped` не крутил рестарт-петлю) ⇒ нулевой эффект на орк и + конвейер. Полная обратимость: не запускать сервис вовсе / `WATCHDOG_ENABLED=false`. + +Привязка: NFR-1, NFR-3, NFR-5, FR-10, FR-11, AC-3, AC-4. + +### D9 — Толерантность к версии `/metrics` (NFR-6, FR-2) + +`collectors/orch.py` парсит конверт защитно: неизвестные ключи игнорируются, отсутствие +опционального — не ошибка (дефолт `None`/`[]`/`{}`), `enabled:false` трактуется явно (орк сам +выключил `/metrics` — не `orch_down`). Рост `schema_version` выше известного → `logger.warning` +(«новая версия контракта, читаю совместимое подмножество»), **не** крэш. Это зеркалит аддитивно- +толерантную политику F1a (adr-0030 D2): sidecar обязан пережить расширение `/metrics` без правок. + +Привязка: NFR-6, FR-2, AC-1. + +## Альтернативы + +- **Go-стек / `docker` SDK / `requests`** — отвергнуто: вес/вторая цепочка инструментов против C-3 и + C-1-консистентности с `disk_watchdog` (D1). +- **Prometheus/Grafana/TSDB/дашборд** — отвергнуто: прямой запрет C-3 (тонкий стек, хост впритык). +- **Sidecar — единственный владелец диска (внутренний `disk_watchdog` выключить)** — отвергнуто: + потеря покрытия диска, когда сам sidecar/хост-Docker недоступен; `disk_watchdog` дешёв и уже в + проде. Выбрана связка «disk_watchdog primary + sidecar opt-in ceiling» (D6). +- **Sidecar дублирует диск на 85% с дедупом по времени** — отвергнуто: хрупкая координация двух + каналов на одном событии; структурное «один владелец на порог» надёжнее (D6). +- **Push метрик из орка в sidecar** — отвергнуто: при зависшем орке push не уходит; pull-опрос + падает = **сам сигнал** `orch_down` (C-1). +- **bridge-сеть + `host.docker.internal`** — отвергнуто: на Linux ненадёжно; `network_mode: host` + проще и достигает и `/metrics`, и хост-интерфейсов (D2). +- **Своя БД/файл состояния порогов** — отвергнуто: тонкий стек (C-3); in-memory best-effort + достаточно (рестарт → корректный повторный алерт стоящей проблемы), как `disk_watchdog` (D4). + +## Последствия + +- **+** Появляется внешний мозг мониторинга, переживающий падение орка — закрыт корневой пробел + «in-process стражи лягут вместе с орком»; `orch_down` делает наблюдателя **громче** в инцидент. +- **+** Строго read-only к наблюдаемому (docker.sock `:ro` + GET-only, нет записи в БД/диск/`main`, + нет start/stop/restart/exec) + независимый канал ⇒ self-hosting-безопасно (enduro-trails не + затронут); падение sidecar не влияет на конвейер (NFR-1/4, AC-6). +- **+** Аддитивно и обратимо: новая папка `watchdog/`, новый сервис compose, новые `WATCHDOG_*` env. + `src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схема БД орка — байт-в-байт. Kill-switch → + нулевая регрессия. +- **+** Дубль диск-алерта исключён структурно (D6): один владелец на порог; резерв канала — opt-in. +- **−** Новый рантайм-контейнер на впритык-хосте: бюджет памяти `mem_limit: 128m` (D2) + измерение + фактического RSS на staging — обязательны (10-tech-risks TR-4). +- **−** C-2: падёт весь хост/Docker → молчит и sidecar (принятый заказчиком риск; внешнее плечо L2 + отложено). +- **−** Новая поверхность совместимости `/metrics`↔F1b — митигируется толерантным парсингом (D9) + + единым репо контракта (adr-0030). CPU-liveness Linux-специфичен (`/proc`); не-Linux → сигнал + `agent_hung` деградирует в none, не ошибка. +- **Топология:** меняется (новый контейнер) → см. `07-infra-requirements.md` (разовое действие: + добавить сервис в compose, создать bot/chat watchdog, смонтировать docker.sock `:ro` + хост-пути, + первый запуск). **Схема БД:** не меняется → `08-data-requirements.md` = N/A. +- **Эскалация:** новый компонент наблюдаемости + новый рантайм-контейнер + новый алерт-канал → лейбл + **`arch:major-change`** (консервативно, хоть изменение аддитивно/read-only/обратимо). Прод-выкат — + строго через staging-гейт (8501); деплой sidecar НЕ рестартит прод-контейнер `orchestrator`. +- **Откат:** не запускать сервис / `WATCHDOG_ENABLED=false` (мгновенный); удаление папки `watchdog/` + + сервиса из compose + `WATCHDOG_*` env — полный откат без следов (нет БД/схемы/изменений `src`). + +## Ссылки +- BRD: `docs/work-items/ORCH-100/01-brd.md` +- TRZ: `docs/work-items/ORCH-100/02-trz.md` +- Acceptance: `docs/work-items/ORCH-100/03-acceptance-criteria.md` +- Инфра-требования: `docs/work-items/ORCH-100/07-infra-requirements.md` +- Данные: `docs/work-items/ORCH-100/08-data-requirements.md` (N/A) +- Тех-риски: `docs/work-items/ORCH-100/10-tech-risks.md` +- Сквозной ADR: `docs/architecture/adr/adr-0033-sidecar-watchdog.md` +- Сверено по коду: `src/disk_watchdog.py` (`decide_action`/`PathAlertState`/трёхуровневый never-raise + — эталон D4/D8), `docker-compose.yml` (`network_mode: host`, `docker.sock` mount — база D2), + `src/metrics.py`/adr-0030 (контракт `/metrics`, `cpu_ticks`/`clk_tck`/`generated_at` — D5/D9). +- Связанные ADR: adr-0030 (F1a `/metrics` — источник сырья, парный контракт), adr-0024 + (`disk_watchdog` — образец решающей функции/never-raise/владелец диск-алерта), adr-0025 + (build-cache-pruner — «вторая половина» паттерн), adr-0017 (serial_gate — leaf never-raise), + adr-0011 (job-reaper — pid/liveness-семантика). + + diff --git a/docs/work-items/ORCH-100/07-infra-requirements.md b/docs/work-items/ORCH-100/07-infra-requirements.md new file mode 100644 index 0000000..e90f1dd --- /dev/null +++ b/docs/work-items/ORCH-100/07-infra-requirements.md @@ -0,0 +1,93 @@ +--- +work_item: ORCH-100 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 07 — Инфра-требования: ORCH-100 — FND/F1b: sidecar-watchdog + +Work Item: **ORCH-100** · Repo: **orchestrator** · Стадия: architecture + +> When-applicable: топология **меняется** (новый рантайм-контейнер). Разовое инфра-действие выполняет +> человек (Слава/Стрим) на хосте mva154; дальше код `watchdog/` катится через конвейер (self-hosting). + +## I-1. Топология / окружения + +Новый сервис `orchestrator-watchdog` в `docker-compose.yml` — **отдельный контейнер** рядом с +`orchestrator` (8500) и `orchestrator-staging` (8501, profile staging). +- **Образ:** `build: ./watchdog` (`watchdog/Dockerfile`, `python:3.12-slim`, stdlib-only). +- **Сеть:** `network_mode: host` — достаёт `/metrics` орка как `http://127.0.0.1:8500/metrics` и + хост-интерфейсы (ADR-001 D2). +- **Тома (все read-only к наблюдаемому, NFR-4):** + - `/var/run/docker.sock:/var/run/docker.sock:ro` — статусы контейнеров (GET-only). + - `/home/slin/repos:/repos:ro` и `./data:/app/data:ro` (или эквивалент) — дисковые метрики хоста + через `shutil.disk_usage` (те же пути, что у `disk_watchdog`). +- **Лимиты:** `mem_limit: 128m` + `mem_reservation: 32m` (тонкость измерима/принудительна, NFR-2); + `restart: unless-stopped` (самовосстановление, FR-1). +- **Kill-switch:** `WATCHDOG_ENABLED` (env). `false` → демон инертен (idle-loop, не exit — чтобы + `restart` не крутил петлю), нулевой эффект на орк. +- **Контейнеры под наблюдением (BR-4):** минимум `orchestrator`; список `WATCHDOG_CONTAINERS` (CSV). +- **Образец сервиса (ориентир для developer; точные пути сверить с актуальным `docker-compose.yml`):** + ```yaml + orchestrator-watchdog: + build: ./watchdog + container_name: orchestrator-watchdog + restart: unless-stopped + network_mode: host + mem_limit: 128m + mem_reservation: 32m + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /home/slin/repos:/repos:ro + - ./data:/app/data:ro + env_file: .env.watchdog # ЛИБО общий .env с префиксом WATCHDOG_ (деталь — developer/оператор) + group_add: ["999"] # docker-группа для чтения docker.sock (как у орка) + ``` + +## I-2. Переменные окружения / секреты + +Канон (без секретов) — в `.env.example` (TRZ §2). Префикс `WATCHDOG_` (изоляция от `ORCH_`): +- **Секреты (только на хосте, в гит НЕ коммитятся):** `WATCHDOG_TG_BOT_TOKEN`, `WATCHDOG_TG_CHAT_ID` + — **собственные** bot/chat sidecar, независимые от Telegram орка (BR-8). Отсутствие → sidecar + логирует и не шлёт (fail-safe), но не падает. +- **Управление:** `WATCHDOG_ENABLED` (kill-switch), `WATCHDOG_INTERVAL_S` (дефолт 60), + `WATCHDOG_ORCH_METRICS_URL` (дефолт `http://127.0.0.1:8500/metrics`). +- **Пороги/таймауты (дефолты — ADR-001 D5):** `WATCHDOG_ORCH_DOWN_TICKS=3`, `WATCHDOG_MEM_PCT=90`, + `WATCHDOG_DISK_CRIT_ENABLED=false`, `WATCHDOG_DISK_CRIT_PCT=97`, `WATCHDOG_AGENT_HUNG_MIN=20`, + `WATCHDOG_AGENT_CPU_FLOOR=0.01`, `WATCHDOG_STAGE_STUCK_MIN=120`, `WATCHDOG_QUEUE_DEPTH=20`, + `WATCHDOG_COOLDOWN_S` (анти-спам realert), `WATCHDOG_HTTP_TIMEOUT_S`. +- **Цели:** `WATCHDOG_CONTAINERS` (CSV, дефолт `orchestrator`), `WATCHDOG_DEP_PLANE_URL`/ + `WATCHDOG_DEP_GITEA_URL`/`WATCHDOG_DEP_ANTHROPIC_URL` (health/ping). + +> Анти-дубль диск-алерта (ADR-001 D6): штатный 85%-алерт остаётся за внутренним `disk_watchdog` +> (ORCH-063). `WATCHDOG_DISK_CRIT_ENABLED` по умолчанию `false` — sidecar НЕ дублирует диск, пока +> оператор осознанно не включит независимый критический потолок. + +## I-3. Деплой / рестарт + +- **Разовое действие человеком на хосте (Слава/Стрим):** + 1. Создать **отдельного** Telegram-бота watchdog + получить chat-id; положить `WATCHDOG_TG_*` в + `.env.watchdog` (или `.env`) на хосте. + 2. Заполнить пороги/интервалы (дефолты годятся), включить `WATCHDOG_ENABLED=true`. + 3. Добавить сервис в `docker-compose.yml` (приходит с PR) и поднять **только его:** + `docker compose up -d --build orchestrator-watchdog`. +- **Self-hosting инвариант (критично):** поднятие/пересборка `orchestrator-watchdog` **НЕ** трогает + прод-контейнер `orchestrator` (отдельный сервис) — конвейер всех проектов не прерывается. **НЕ** + выполнять `docker compose up -d` без явного имени сервиса, если это спровоцирует рекреейт орка. +- **Прод-выкат кода watchdog** — через штатный self-hosting-конвейер и **обязательный staging-гейт + (8501)** перед прод-деплоем; деплой sidecar не рестартит прод-контейнер орка. +- **Проверка после старта (NFR-7):** `docker logs orchestrator-watchdog` показывает старт + тики; + тестовый алерт приходит в канал watchdog; остановка орка (на staging) → приходит `orch_down`. + +## I-4. CI/CD + +- Без изменений `.gitea/workflows/` по существу: новые тесты sidecar (`watchdog/tests/` или + `tests/watchdog/`) подхватываются существующим `pytest tests/`/прогоном (изолированы, чистые + функции — без контейнера/таймера). Если выбран отдельный путь `watchdog/tests/`, developer + обеспечивает его включение в существующий тест-ран (без нового workflow-файла). +- Docker-сборка нового образа — стандартным `docker compose build` (отдельный `watchdog/Dockerfile`), + без правок пайплайна CI. + diff --git a/docs/work-items/ORCH-100/08-data-requirements.md b/docs/work-items/ORCH-100/08-data-requirements.md new file mode 100644 index 0000000..65d6deb --- /dev/null +++ b/docs/work-items/ORCH-100/08-data-requirements.md @@ -0,0 +1,40 @@ +--- +work_item: ORCH-100 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 08 — Требования к данным: ORCH-100 — FND/F1b: sidecar-watchdog + +Work Item: **ORCH-100** · Repo: **orchestrator** · Стадия: architecture + +> When-applicable. Создан для аудитопригодности: фиксирует, что схема БД **не меняется** — это +> архитектурное утверждение (sidecar вне процесса орка, без своей БД), а не пропуск. + +## Изменения схемы БД орка + +**N/A.** Sidecar **не пишет** в БД орка (NFR-4: строго read-only к наблюдаемому — нет +`INSERT/UPDATE/DELETE/CREATE/ALTER`) и **не читает** её напрямую: всё орк-сырьё идёт через +`GET /metrics` (F1a, adr-0030). `tasks`/`jobs`/`agent_runs`/`STAGE_TRANSITIONS`/`QG_CHECKS` — +не тронуты. + +## Собственное хранилище sidecar + +**Нет (по решению C-3 / ADR-001 D4).** Состояние порогов (`AlertState`: `alerting`/`last_alert_at` +per signal_key) — **in-memory best-effort** в процессе демона: ни таблицы, ни файла, ни миграции. +Рестарт sidecar сбрасывает карту состояний → ещё стоящая проблема корректно повторно алертится один +раз (ранний сигнал, не SLA) — 1:1 семантика `disk_watchdog.PathAlertState` (ORCH-063). + +## Журнал уроков (F2) + +**Вне объёма.** Долговременное хранение инцидентов/уроков (потенциально БД орка) — отдельная задача +домена F2; F1b ничего не персистит (BRD §«Вне объёма»). + +## Вывод + +Изменений данных/схемы нет. Контракт данных F1b — **потребление** версионированного JSON `/metrics` +(adr-0030) + эфемерное in-memory состояние порогов. Откат не оставляет следов в БД. + diff --git a/docs/work-items/ORCH-100/10-tech-risks.md b/docs/work-items/ORCH-100/10-tech-risks.md new file mode 100644 index 0000000..1d44080 --- /dev/null +++ b/docs/work-items/ORCH-100/10-tech-risks.md @@ -0,0 +1,44 @@ +--- +work_item: ORCH-100 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 10 — Технические риски: ORCH-100 — FND/F1b: sidecar-watchdog + +Work Item: **ORCH-100** · Repo: **orchestrator** · Стадия: architecture + +> Информационный (гейтом не парсится). Реестр рисков реализации F1b и митигейшн. + +## Реестр рисков + +| ID | Риск | Вер. | Влия. | Митигейшн | +|----|------|------|-------|-----------| +| TR-1 | **Дубль диск-алерта** с `disk_watchdog` (ORCH-063) на одно событие переполнения. | Сред. | Низ. | ADR-001 D6: 85% остаётся ЕДИНСТВЕННО за `disk_watchdog` (канал орка); sidecar НЕ дублирует порог — `host_disk_crit` opt-in (default off) и на другом пороге-потолке (97%, другой канал = другое событие). Структурно один владелец на порог. | +| TR-2 | **Ложный `orch_down`** на одиночной сетевой икоте `/metrics` (флапп). | Сред. | Сред. | Порог `WATCHDOG_ORCH_DOWN_TICKS` (K подряд неудачных опросов, дефолт 3) + cooldown/recovery decide() (FR-3). Единичный transient → none. | +| TR-3 | **Sidecar толстеет** (память на впритык-хосте, 171Mi free) и сам становится проблемой. | Низ. | Сред. | Stdlib-only Python, один поток (D1); `mem_limit: 128m` + `mem_reservation: 32m` принудительно (D2); **обязательный замер фактического RSS на staging** перед прод-выкатом; OOM = ранний сигнал, не тихий рост. | +| TR-4 | **Привилегии docker.sock** — доступ к Docker API = потенциально мощно. | Низ. | Выс. | Mount `:ro` (NFR-4) + код делает ТОЛЬКО GET (list/inspect), без `docker` SDK — мутаций нет по построению; ревью + статпроверка (AC-6/TC-09). | +| TR-5 | **Дрейф контракта `/metrics`** (F1a расширили/сломали) роняет/искажает sidecar. | Низ. | Сред. | Толерантный парсинг (D9): неизвестные ключи игнор, отсутствие опционального не ошибка, рост `schema_version` → warning не крэш; единый репо контракта (adr-0030); ломающее изменение `/metrics` — отдельная задача-расширение F1a, не F1b. | +| TR-6 | **Шум алертов** (флапп на границе порога agent_hung/stage_stuck/mem). | Сред. | Низ. | Чистая decide() с cooldown/realert/recovery (D4, образец disk_watchdog); пороги/cooldown из env (тюнинг без релиза); `agent_hung` требует 2 опросов + CPU-floor (не дёргается на коротких паузах). | +| TR-7 | **Self-hosting: деплой sidecar задел прод-контейнер** `orchestrator`. | Низ. | Выс. | Отдельный сервис; `docker compose up -d orchestrator-watchdog` поднимает только его (07 I-3); прод-выкат через staging-гейт (8501); деплой sidecar не рестартит орк. | +| TR-8 | **`network_mode: host`** у sidecar — разделяет сетевой namespace хоста. | Низ. | Низ. | Sidecar read-only, не слушает входящих портов (опц. liveness вне обязательного объёма); host-network нужен для достижимости `/metrics` и хост-интерфейсов (D2); поверхность минимальна. | +| TR-9 | **Утечка/отсутствие** `WATCHDOG_TG_*` (свой бот) → алерты не доходят/секрет в гит. | Низ. | Сред. | Секреты только в `.env*` на хосте, канон без значений в `.env.example` (правило 8); отсутствие токена → fail-safe (лог, не падение, не шлёт); префикс `WATCHDOG_` изолирует от `ORCH_`. | +| TR-10 | **C-2: падёт весь хост/Docker** → молчит и sidecar (нет внешнего плеча). | Низ. | Выс. | Принятый заказчиком риск (одна площадка); внешнее плечо L2 сознательно отложено (BRD §«Вне объёма»). Документируется, не закрывается в F1b. | + +## Сводный вывод + +Доминирующий класс — **операционно-инфраструктурный** (привилегии docker.sock, память впритык, +self-hosting-безопасность), а не алгоритмический: ядро (decide/парсинг) — чистые тестируемые функции, +перенос зрелого паттерна `disk_watchdog`. Все мутирующие пути закрыты по построению (read-only mount + +GET-only, нет записи в БД/`main`), независимый алерт-канал и kill-switch дают полную обратимость. +Остаточный риск для прод-конвейера (enduro-trails и пр.) — **near-zero**: F1b физически вне процесса +орка и вне конвейера QG, при выключенном флаге — нулевой эффект. + +**Эскалация:** новый компонент наблюдаемости + новый рантайм-контейнер + новый алерт-канал → лейбл +**`arch:major-change`** (консервативно). Возврат в анализ **не требуется** — ТЗ выполнимо в рамках +принципов (всё в Docker на одном сервере, тонкий стек, минимум зависимостей). Обязательное +предусловие приёмки developer/tester: **замер фактического RSS sidecar на staging** (TR-3). +