architect(ET): auto-commit from architect run_id=565
This commit is contained in:
@@ -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`, `<repo>/.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
|
||||
|
||||
```
|
||||
|
||||
85
docs/architecture/adr/adr-0033-sidecar-watchdog.md
Normal file
85
docs/architecture/adr/adr-0033-sidecar-watchdog.md
Normal file
@@ -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 — его потребитель.
|
||||
</content>
|
||||
304
docs/work-items/ORCH-100/06-adr/ADR-001-sidecar-watchdog.md
Normal file
304
docs/work-items/ORCH-100/06-adr/ADR-001-sidecar-watchdog.md
Normal file
@@ -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/<name>/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-семантика).
|
||||
</content>
|
||||
</invoke>
|
||||
93
docs/work-items/ORCH-100/07-infra-requirements.md
Normal file
93
docs/work-items/ORCH-100/07-infra-requirements.md
Normal file
@@ -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.
|
||||
</content>
|
||||
40
docs/work-items/ORCH-100/08-data-requirements.md
Normal file
40
docs/work-items/ORCH-100/08-data-requirements.md
Normal file
@@ -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 состояние порогов. Откат не оставляет следов в БД.
|
||||
</content>
|
||||
44
docs/work-items/ORCH-100/10-tech-risks.md
Normal file
44
docs/work-items/ORCH-100/10-tech-risks.md
Normal file
@@ -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).
|
||||
</content>
|
||||
Reference in New Issue
Block a user