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>
|
||||
Reference in New Issue
Block a user