architect(ET): auto-commit from architect run_id=484
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
- **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe. **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота; декларации/циклы — leaf `src/task_deps.py`.
|
||||
- **Job-reaper** (`src/job_reaper.py`, ORCH-065 — [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md)) — фоновый daemon-поток (каркас `reconciler`), стартует/останавливается в `main.lifespan` (после `reconciler.start()` / перед `worker.stop()`). Детектирует «мёртвый» `running`-job **без рестарта** процесса (Tier-1 мёртвый `jobs.pid` после `reaper_dead_ticks` тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running`; Tier-3 backstop `reaper_max_running_s`) и приводит строку к корректному статусу через те же контракты (`_try_advance_stage`/`_finalize_job`, gate-driven; exit≠0/неизвестно → `attempts<max`→`queued`, иначе `failed`+Telegram). Атомарный reap-claim (guard `status='running'`) совместим со стартовым `requeue_running_jobs`. Тот же поток периодически делает проактивный реклейм stale/dead merge-lease (см. ниже). never-raise; kill-switch `ORCH_REAPER_ENABLED`; снимок в `GET /queue` (блок `reaper`).
|
||||
- **Reconciler** (`src/reconciler.py`, ORCH-053 — реализовано, [adr-0007](adr/adr-0007-reconciler.md)) — фоновый daemon-поток (паттерн `queue_worker`), стартует/останавливается в `main.lifespan` (после `worker.start()` / перед `worker.stop()`). Реконсилирует рассинхрон «источник истины ≠ стадия задачи» при потерянном webhook. F-1 gate-side (продвигает застрявшую стадию по локальной БД через штатный `advance_stage(..., finished_agent=None)`), F-2 plane-side (опрос Plane API → `handle_*` из `plane.py`), F-3 (БД-fallback `sha→branch` в `handle_ci_status`). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switch `ORCH_RECONCILE_ENABLED`. `analysis` F-1 не трогает (человеческий гейт). F-1 также пропускает escalated (retry≥лимита) и Blocked/Needs-Input задачи (ORCH-060). Наблюдаемость — блок `reconcile` в `GET /queue`.
|
||||
- **Disk-watchdog** (`src/disk_watchdog.py`, ORCH-063 — [adr-0024](adr/adr-0024-disk-watchdog.md)) — фоновый daemon-поток (каркас `reconciler`/`job_reaper`), стартует/останавливается в `main.lifespan` (старт последним — после `reaper.start()`; стоп первым в reverse-порядке; гард `disk_monitor_enabled`). Каждые `disk_monitor_interval_s` (дефолт 300с) меряет заполнение **хост-ФС** по смонтированным bind-путям (`/repos`, `/app/data`) через stdlib `shutil.disk_usage` (не overlay `/` контейнера, не субпроцесс `df`; дедуп путей по `st_dev`). Решение об алерте — pure-функция `decide_action(used_pct, threshold, prev_state, now, realert_s)`: алерт на пересечении порога (дефолт **85%**), cooldown-повтор `disk_monitor_realert_s` (анти-спам, не на каждом тике), однократный recovery при возврате ниже порога. Алерт — `send_telegram` (notifying, best-effort). Состояние анти-спама — in-memory (без миграции БД). never-raise (per-path/per-tick/per-send); только читает и уведомляет — не трогает диск/контейнер, не рестартит прод (self-hosting безопасность). Kill-switch `ORCH_DISK_MONITOR_ENABLED`; снимок — блок `disk_monitor` в `GET /queue` (`enabled`/`threshold_pct`/`interval_s`/`realert_s`/`paths`[`used_pct`/`free_gb`/`alerting`/`last_alert_at`]). `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — не тронуты. Детали — `docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md`.
|
||||
- **Notifications / Live-tracker** (`src/notifications.py`, ORCH-042/ORCH-067) — ОДНА live-карточка на задачу (`update_task_tracker`), обновляется на каждом переходе. Режим `ORCH_TRACKER_MODE` (дефолт `bump` с ORCH-067: delete+silent send+repoint внизу чата; `edit` — правка на месте). Карточка несёт строку Plane-статуса `📍 …` (оффлайн-ядро `plane_status_label` + best-effort live-overlay `_live_plane_branch_override`, kill-switch `ORCH_TRACKER_LIVE_STATUS`) и кликабельный номер задачи (`plane_issue_link`/`link_for` → ссылка в Plane, fail-safe на сырой номер). **ORCH-080:** оба низкоуровневых примитива (`send_telegram`/`edit_telegram`) шлют payload с `disable_web_page_preview: True` — Telegram больше не разворачивает баннер link-preview Plane под карточкой/уведомлениями; `parse_mode: HTML` сохранён (ссылка остаётся кликабельной), безусловно без kill-switch. Все алерты, упоминающие `work_item_id`, делают номер кликабельным. **ORCH-087:** bump ведёт авторитетный леджер всех созданных карточек (`tracker_messages`, `deleted_at IS NULL` = жива) и на каждом обновлении зачищает ВСЕ незакрытые mid (а не только скаляр `tracker_message_id`) → класс «замёрзшая сирота» устранён; строка стадии несёт фактический эффорт рядом с моделью (`· {model} · {effort}`, колонка `agent_runs.effort`, стамп в `launcher._spawn`); done-строка времени переписана на три подписанных метрики `⏱️ Агенты · твоё{~cap} · общее с ожиданием` (кап `ORCH_TRACKER_BRD_REVIEW_CAP_S`); deploy-цикл дополнен overlay-ключом `confirm_deploy`. Контракт всего компонента — never raises; карточка всегда silent. Детали — [internals.md](internals.md) §7 и [ADR-001](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md).
|
||||
- **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту.
|
||||
- **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` неизменна (обратная совместимость).
|
||||
|
||||
59
docs/architecture/adr/adr-0024-disk-watchdog.md
Normal file
59
docs/architecture/adr/adr-0024-disk-watchdog.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
work_item: ORCH-063
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-09
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# adr-0024: Disk-watchdog — фоновый heartbeat-демон мониторинга заполнения хост-ФС
|
||||
|
||||
> Сквозной (cross-cutting) ADR: вводит **новый фоновый компонент** оркестратора в ряду
|
||||
> `reconciler` (adr-0007) и `job_reaper` (adr-0011). Детальное решение задачи —
|
||||
> `docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md`.
|
||||
|
||||
## Статус
|
||||
Proposed (ORCH-063)
|
||||
|
||||
## Контекст
|
||||
07.06.2026 диск хоста mva154 тихо дорос до 100% и положил **весь self-hosting-конвейер** (один
|
||||
прод-инстанс `orchestrator` обслуживает все прод-проекты из общей БД/очереди). Проактивного сигнала
|
||||
о заполнении диска у системы не было. Оркестратор уже имеет два проверенных фоновых daemon-потока с
|
||||
единым каркасом (`threading.Thread(daemon=True)` + `threading.Event`, `start/stop/status`,
|
||||
never-raise, снимок в `GET /queue`): `reconciler` (ORCH-053) и `job_reaper` (ORCH-065). Новый
|
||||
эксплуатационный watchdog логично встроить тем же паттерном.
|
||||
|
||||
## Решение
|
||||
Вводится третий фоновый компонент **disk-watchdog** (`src/disk_watchdog.py`):
|
||||
- **Калька каркаса** `reconciler`/`reaper`: daemon-поток, чистый stop через `_stop.wait(interval)`,
|
||||
контракт `start()`/`stop(timeout)`/`status()`, старт/стоп в `main.lifespan` (старт последним —
|
||||
после `reaper.start()`; стоп первым в reverse-порядке), наблюдаемость — аддитивный блок
|
||||
`disk_monitor` в `GET /queue`.
|
||||
- **Замер** заполнения **хост-ФС** через смонтированные bind-пути (`/repos`, `/app/data`) stdlib
|
||||
`shutil.disk_usage` (не overlay `/` контейнера, не субпроцесс `df`); дедуп путей по `st_dev`.
|
||||
- **Решение об алерте** — pure-функция от `(used_pct, threshold, prev_state, now, realert_s)`:
|
||||
алерт на пересечении порога (дефолт 85%), ограниченный cooldown-повтор, recovery при возврате
|
||||
ниже порога. Состояние анти-спама — in-memory (без миграции БД).
|
||||
- **Алерт** — `send_telegram` (notifying), best-effort. Kill-switch `disk_monitor_enabled`.
|
||||
- **Только сигнал, не лечение:** watchdog читает и уведомляет, не трогает диск/контейнер, не
|
||||
рестартит прод (self-hosting безопасность). Авто-очистка диска — отдельная задача.
|
||||
|
||||
**Инварианты:** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, схема БД — **не меняются**
|
||||
(watchdog — эксплуатационный демон, не Quality Gate, как `reconciler`/`reaper`). never-raise на
|
||||
уровнях per-path / per-tick / per-send. При выключенном kill-switch — поведение 1:1 как сейчас
|
||||
(нулевая регрессия для enduro-trails).
|
||||
|
||||
## Последствия
|
||||
- **+** Ранний сигнал предотвращает групповой простой всех проектов; дёшево, без внешних
|
||||
зависимостей (принцип «всё в Docker на одном сервере, минимум зависимостей»).
|
||||
- **+** Знакомый паттерн фонового демона → низкий риск, простое сопровождение.
|
||||
- **−** In-memory состояние / best-effort Telegram — допустимы для раннего сигнала (не SLA).
|
||||
- **Откат:** `ORCH_DISK_MONITOR_ENABLED=false`; миграций БД нет.
|
||||
|
||||
## Ссылки
|
||||
- Задачный ADR: `docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md`
|
||||
- Родственные компоненты: [adr-0007-reconciler.md](adr-0007-reconciler.md),
|
||||
[adr-0011-job-reaper-lease-reclaim.md](adr-0011-job-reaper-lease-reclaim.md)
|
||||
- Топология host-разделов: `docs/operations/INFRA.md`
|
||||
</content>
|
||||
196
docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md
Normal file
196
docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md
Normal file
@@ -0,0 +1,196 @@
|
||||
---
|
||||
work_item: ORCH-063
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-09
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# ADR-001: Disk-watchdog — heartbeat-демон мониторинга заполнения хост-ФС + Telegram-алерт при ≥85%
|
||||
|
||||
Work Item: **ORCH-063** — INFRA: мониторинг диска mva154 + алерт при >85%
|
||||
Стадия: **architecture**
|
||||
Сквозная регистрация: **`docs/architecture/adr/adr-0024-disk-watchdog.md`** (новый фоновый
|
||||
компонент-демон в ряду `reconciler`/`job_reaper` — кросс-каттинговое решение).
|
||||
|
||||
## Статус
|
||||
Proposed
|
||||
|
||||
## Контекст
|
||||
07.06.2026 диск хоста **mva154** (`slin@82.22.50.71`) тихо дорос до 100% и положил **весь
|
||||
конвейер всех проектов** (CI красный, очередь Gitea застряла). Корневая боль: у оркестратора
|
||||
**нет проактивного сигнала** о заполнении диска — оператор узнаёт о проблеме постфактум, когда
|
||||
self-hosting-инстанс `orchestrator` (8500, один на все прод-проекты, общая БД/очередь) уже встал
|
||||
(BRD §1).
|
||||
|
||||
Факты, сверенные с кодом:
|
||||
- В оркестраторе уже есть **каркас фонового daemon-потока**, повторённый дважды:
|
||||
`src/reconciler.py::Reconciler` (ORCH-053) и `src/job_reaper.py` (ORCH-065) — оба
|
||||
`threading.Thread(daemon=True)` + `threading.Event`, чистый stop через `self._stop.wait(interval)`,
|
||||
контракт `start()`/`stop(timeout)`/`status()`, **never-raise** на тик, наблюдаемость через
|
||||
`GET /queue`. Старт/стоп — в `src/main.py::lifespan` (старт после `reaper.start()`, стоп в
|
||||
reverse-порядке), снимок — в `@app.get("/queue")` (`"reaper": reaper.status()` и др.).
|
||||
- Контейнер бежит `network_mode: host` с bind-mount'ами host-разделов: `/home/slin/repos → /repos`,
|
||||
`./data → /app/data` (`docs/operations/INFRA.md` §«Тома»). Именно эта ФС переполнилась 07.06.
|
||||
Замер по overlay `/` контейнера нерепрезентативен (BRD §6, NFR-3).
|
||||
- Алерты шлются через `src/notifications.py::send_telegram` (notifying по умолчанию; silent —
|
||||
только при явном `disable_notification`).
|
||||
- Образец «чистая leaf-логика + тонкая обёртка» уже принят: `src/task_deps.py`, `src/serial_gate.py`,
|
||||
`src/staging_verdict.py` — pure-функции (never-raise) + точечные врезки.
|
||||
|
||||
«Как есть» не годится: единственный сигнал о диске — падение всего конвейера. Нужен дешёвый ранний
|
||||
heartbeat-watchdog. ТЗ (02-trz) фиксирует требования; данный ADR фиксирует **как** (§OQ-1..OQ-5).
|
||||
|
||||
## Решение
|
||||
|
||||
### Сводка
|
||||
Вводим **disk-watchdog** — новый фоновый daemon-поток `src/disk_watchdog.py`, точная калька
|
||||
архитектуры `reconciler`/`job_reaper`. Демон каждые `disk_monitor_interval_s` (дефолт 300с) меряет
|
||||
заполнение **смонтированных хост-путей** через stdlib `shutil.disk_usage(path)`, дедуплицирует пути
|
||||
по физическому устройству (`os.stat(path).st_dev`), и через **чистую функцию решения** от
|
||||
`(used_pct, threshold, prev_state, now)` решает: послать алерт (пересечение порога вверх), повторить
|
||||
(cooldown `disk_monitor_realert_s`), послать recovery (возврат вниз) или молчать. Состояние
|
||||
анти-спама — **in-memory** (без миграции БД). Наблюдаемость — аддитивный блок `disk_monitor` в
|
||||
`GET /queue`. Kill-switch `disk_monitor_enabled`. **never-raise** на каждом уровне.
|
||||
`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схема БД — **не трогаются**.
|
||||
|
||||
### D1 — Способ замера: stdlib `shutil.disk_usage` (OQ-1, FR-2/NFR-2)
|
||||
Замер каждого пути — `shutil.disk_usage(path)` (`total`/`used`/`free` в байтах), `used_pct =
|
||||
round(used / total * 100, 1)`. Чистый системный вызов `statvfs`, без порождения субпроцесса `df` на
|
||||
каждом тике (NFR-2: heartbeat порядка минут, дёшево). `df` отвергнут (см. Альтернативы).
|
||||
- **Почему репрезентативно для хост-ФС (NFR-3, AC-8):** `shutil.disk_usage(path)` возвращает
|
||||
статистику ФС, которой принадлежит `path`. На bind-mount'е `/repos`/`/app/data` это **хост-раздел**
|
||||
(тот, что переполнился 07.06), а не overlay контейнера. Дефолтный набор путей —
|
||||
`/repos,/app/data`; `shutil.disk_usage("/")` **не** используется как источник истины.
|
||||
- Недоступный/несуществующий путь (`FileNotFoundError`/`PermissionError`/`OSError`) → пропуск
|
||||
**этого** пути с `logger.warning`, остальные пути меряются дальше (FR-2, AC-6: один битый путь не
|
||||
роняет весь тик).
|
||||
|
||||
### D2 — Дедуп путей по физическому устройству (OQ-2, FR-2)
|
||||
Перед замером пути резолвим `os.stat(path).st_dev` и схлопываем пути с одинаковым `st_dev` в один
|
||||
логический раздел (ключ дедупа — `st_dev`; для отображения берём первый успешно резолвнутый путь).
|
||||
На mva154 `/repos` и `/app/data` с высокой вероятностью лежат на одном host-разделе (BRD §6) → один
|
||||
алерт, а не два дубля. Дедуп — **желательное** требование (BRD §6), реализуемое, never-raise: ошибка
|
||||
`os.stat` → путь обрабатывается как отдельный (fail-open, без потери замера).
|
||||
|
||||
### D3 — Чистая функция решения + модель состояния (OQ-3, FR-4, AC-3/AC-4)
|
||||
Решение об отправке вынесено в **pure-функцию** (юнит-тестируема без потока и реального таймера,
|
||||
AC-3):
|
||||
|
||||
```
|
||||
decide_action(used_pct, threshold, prev: PathAlertState, now, realert_s) -> Action
|
||||
# Action ∈ {NONE, ALERT, REALERT, RECOVERY}
|
||||
```
|
||||
|
||||
- `prev.alerting == False` и `used_pct >= threshold` → **ALERT** (пересечение «ниже→на/выше»);
|
||||
- `prev.alerting == True` и `used_pct >= threshold` и `now - prev.last_alert_at >= realert_s` →
|
||||
**REALERT** (cooldown истёк); иначе при `alerting && >=threshold` → **NONE** (анти-спам: не на
|
||||
каждом тике, BR-3/AC-3);
|
||||
- `prev.alerting == True` и `used_pct < threshold` → **RECOVERY** (переход «выше→ниже», ровно одно
|
||||
сообщение, сброс `alerting`, BR-4/AC-4);
|
||||
- `prev.alerting == False` и `used_pct < threshold` → **NONE** (норма).
|
||||
|
||||
**Модель состояния (in-memory, per device/path):** `PathAlertState{alerting: bool, last_alert_at:
|
||||
float|None}`, словарь `{dedup_key -> PathAlertState}` в демоне. Durable-хранение **отвергнуто**
|
||||
(OQ-3): TRZ §5/NFR-5 допускает in-memory, состояние best-effort. После рестарта `alerting`
|
||||
сбрасывается → при всё ещё полном диске придёт повторный алерт — это **безопасно** (ранний сигнал,
|
||||
не SLA). **Время инъецируется** `now`-провайдером (дефолт — обёртка над часами; в тестах — фейк),
|
||||
чтобы cooldown/recovery тестировались детерминированно (AC-3).
|
||||
|
||||
### D4 — Отправка алерта: формат в leaf + `send_telegram` напрямую (OQ-5, FR-3)
|
||||
Форматирование текста — pure-функция в `disk_watchdog.py` (`format_alert_message` /
|
||||
`format_recovery_message`, тестируема). Отправка — **прямой** `send_telegram(text)`
|
||||
(**notifying**, не silent — это алерт, как `notify_error`); отдельный helper в `notifications.py`
|
||||
**не** вводим (минимизация поверхности; OQ-5 оставляет выбор за архитектором). Вызов `send_telegram`
|
||||
обёрнут `try/except` → ошибка доставки логируется и не роняет тик (BR-6/AC-6; доставка best-effort,
|
||||
BRD §6).
|
||||
- **Содержимое (действенное, FR-3/AC-2):** точка монтирования/путь, занято %, свободно (ГБ и %),
|
||||
порог; текст на русском в стиле существующих `notify_*`. Пример:
|
||||
`🔴 Диск mva154: /repos заполнен на 87.3% (порог 85%). Свободно 6.2 ГБ (12.7%). Освободите
|
||||
место — риск остановки конвейера всех проектов.`
|
||||
Recovery: `🟢 Диск mva154: /repos вернулся ниже порога — 78.1% (свободно 11.0 ГБ).`
|
||||
|
||||
### D5 — Один порог 85% (OQ-4, BR-2)
|
||||
Один настраиваемый порог `disk_monitor_threshold_pct` (дефолт 85, зафиксирован Владельцем).
|
||||
Второй «критический» порог (напр. 95%) с усиленным алертом — **вне объёма** (OQ-4, BRD §8 R-3),
|
||||
кандидат на follow-up. Конфигурируемость порога (BR-5) оставляет рычаг тюнинга.
|
||||
|
||||
### D6 — Lifecycle и точки врезки (FR-1/FR-5/FR-6, AC-1/AC-5/AC-7)
|
||||
- **`src/disk_watchdog.py`** (новый leaf) — pure-логика (`measure_paths`, `decide_action`,
|
||||
`format_*`) + класс `DiskWatchdog(threading.Thread(daemon=True) + threading.Event)` с
|
||||
`start()`/`stop(timeout=5.0)`/`status()`; цикл `while not self._stop.is_set(): try: tick();
|
||||
except: log; self._stop.wait(interval)`. Модуль-синглтон `disk_watchdog = DiskWatchdog()`.
|
||||
- **`src/config.py`** — флаги §«Конфигурация» (D7); defensive-валидация значений (порог 1..100,
|
||||
интервалы > 0) → невалидное к дефолту + warning (паттерн `reconcile_grace_*`).
|
||||
- **`src/main.py::lifespan`** — `disk_watchdog.start()` **последним** (после `reaper.start()`,
|
||||
гард `if settings.disk_monitor_enabled`), `disk_watchdog.stop()` **первым** в `finally`
|
||||
(reverse-порядок). Демон независим (не трогает очередь/БД) → порядок не критичен, но
|
||||
следуем конвенции.
|
||||
- **`@app.get("/queue")`** — аддитивный ключ `"disk_monitor": disk_watchdog.status()`; существующие
|
||||
ключи не меняются; `status()` never-raise (при ошибке — `{"enabled": ...}` минимум, FR-6/AC-7).
|
||||
Снимок: `enabled`, `threshold_pct`, `interval_s`, `realert_s`, `paths` (по каждому
|
||||
устройству/пути: `path`, `used_pct`, `free_gb`, `alerting`, `last_alert_at`).
|
||||
- **`.env.example`** — дескрипторы `ORCH_DISK_*` (AC-5).
|
||||
|
||||
### D7 — Конфигурация (`src/config.py`, FR-5/AC-5)
|
||||
| Поле (env) | Тип / дефолт | Назначение |
|
||||
|------------|--------------|------------|
|
||||
| `disk_monitor_enabled` (`ORCH_DISK_MONITOR_ENABLED`) | `bool = True` | kill-switch; `False` → демон не стартует (нулевая регрессия, NFR-4). |
|
||||
| `disk_monitor_interval_s` (`ORCH_DISK_MONITOR_INTERVAL_S`) | `int = 300` | период heartbeat (порядок минут, NFR-2). |
|
||||
| `disk_monitor_threshold_pct` (`ORCH_DISK_MONITOR_THRESHOLD_PCT`) | `int = 85` | порог алерта (дефолт Владельца). |
|
||||
| `disk_monitor_realert_s` (`ORCH_DISK_MONITOR_REALERT_S`) | `int = 21600` | cooldown повторного алерта выше порога (~6 ч, анти-спам). |
|
||||
| `disk_monitor_paths` (`ORCH_DISK_MONITOR_PATHS`) | `str = "/repos,/app/data"` (CSV) | отслеживаемые host-пути (NFR-3); пусто → дефолтный набор. |
|
||||
|
||||
### D8 — Инварианты (NFR-5/NFR-6, AC-6)
|
||||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, схема существующих таблиц БД — **без изменений**
|
||||
(watchdog — эксплуатационный демон, не QG; как `reconciler`/`reaper`). Новой миграции нет (D3).
|
||||
- **never-raise** на трёх уровнях: per-path (D1), per-tick (внешний `try/except` в `_run`),
|
||||
per-send (D4). Сбой watchdog не блокирует и не роняет конвейер (BR-6/NFR-1).
|
||||
- **Self-hosting безопасность (NFR-6):** watchdog только **читает** заполнение и **шлёт** Telegram —
|
||||
не трогает диск/контейнер, не рестартит прод. Безопасен для enduro-trails в общем инстансе.
|
||||
|
||||
## Альтернативы
|
||||
- **Субпроцесс `df -P` на каждом тике** — отвергнут: лишнее порождение процесса при heartbeat
|
||||
порядка минут (NFR-2), парсинг вывода, зависимость от формата `df`. `shutil.disk_usage` — stdlib,
|
||||
без субпроцесса, кроссплатформенно.
|
||||
- **Замер по `/` (overlay контейнера)** — отвергнут: нерепрезентативен для хост-раздела (NFR-3/AC-8),
|
||||
прямой путь к повтору инцидента 07.06 (ложно-низкое заполнение).
|
||||
- **Durable-состояние анти-спама (доп. таблица)** — отвергнуто: TRZ §5/NFR-5 допускает in-memory;
|
||||
повторный алерт после рестарта при полном диске безопасен; миграция = лишняя поверхность и
|
||||
усложнение отката.
|
||||
- **Внешний мониторинг (Prometheus/Grafana/node_exporter)** — вне объёма (BRD §2.2): тяжёлая
|
||||
инфра-зависимость против принципа «минимум зависимостей, всё в Docker на одном сервере». Дешёвый
|
||||
встроенный heartbeat закрывает боль.
|
||||
- **Новый endpoint `GET /disk`** — не вводим (TRZ §4 рекомендация): снимок отдаётся блоком в
|
||||
`/queue`, меньше API-поверхности.
|
||||
|
||||
## Последствия
|
||||
- **+** Ранний сигнал о заполнении диска до остановки конвейера всех проектов; дешёвая страховка от
|
||||
дорогого группового self-hosting-простоя.
|
||||
- **+** Полная архитектурная калька проверенных `reconciler`/`reaper` → низкий риск, знакомый паттерн
|
||||
для ревью/сопровождения.
|
||||
- **+** Чистая pure-логика (`decide_action`, `format_*`, `measure_paths`) юнит-тестируема без потока
|
||||
и таймера (AC-3/AC-6).
|
||||
- **−** In-memory состояние → повторный алерт после рестарта при всё ещё полном диске. Митигейшн:
|
||||
это безопасно (ранний сигнал, не SLA; NFR-5) и редко (рестарт прода — событие).
|
||||
- **−** Best-effort доставка Telegram (та же `send_telegram`): алерт может не дойти при сбое сети.
|
||||
Митигейшн: watchdog — ранний сигнал, не гарантия; cooldown-повтор повышает шанс доставки.
|
||||
- **−** Дедуп по `st_dev` не покрывает редкий случай разных устройств для `/repos` и `/app/data`
|
||||
(тогда — два независимых алерта, что корректно). Без ущерба.
|
||||
- **Откат:** `ORCH_DISK_MONITOR_ENABLED=false` (демон не стартует, блок `/queue` → `{"enabled":
|
||||
false}`, поведение 1:1 как сейчас). Полное удаление — снять врезки в `main.py`/`config.py` +
|
||||
удалить leaf; миграций БД нет → откат тривиален (TRZ §7).
|
||||
|
||||
## Ссылки
|
||||
- BRD: `docs/work-items/ORCH-063/01-brd.md`
|
||||
- TRZ: `docs/work-items/ORCH-063/02-trz.md`
|
||||
- Acceptance: `docs/work-items/ORCH-063/03-acceptance-criteria.md`
|
||||
- Инфра: `docs/work-items/ORCH-063/07-infra-requirements.md`
|
||||
- Риски: `docs/work-items/ORCH-063/10-tech-risks.md`
|
||||
- Сквозной ADR: `docs/architecture/adr/adr-0024-disk-watchdog.md`
|
||||
- Сверено по коду: `src/reconciler.py` (каркас демона), `src/job_reaper.py` (lifecycle/status),
|
||||
`src/main.py` (lifespan §94-118, `/queue` §142-173), `src/notifications.py::send_telegram`,
|
||||
`docs/operations/INFRA.md` (bind-mount'ы `/repos`, `/app/data`).
|
||||
</content>
|
||||
</invoke>
|
||||
63
docs/work-items/ORCH-063/07-infra-requirements.md
Normal file
63
docs/work-items/ORCH-063/07-infra-requirements.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
work_item: ORCH-063
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-09
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 07 — Инфра-требования: ORCH-063 — мониторинг диска mva154 + алерт при ≥85%
|
||||
|
||||
Work Item: **ORCH-063** · Repo: **orchestrator** (self-hosting) · Стадия: architecture
|
||||
|
||||
## I-1. Топология / окружения
|
||||
Топология **не меняется**. Watchdog работает внутри существующего контейнера `orchestrator`
|
||||
(8500, `network_mode: host`) и опирается на уже существующие bind-mount'ы host-разделов:
|
||||
- `/home/slin/repos → /repos` (рабочие репозитории, git-worktree `/repos/_wt/...`);
|
||||
- `./data → /app/data` (SQLite БД).
|
||||
|
||||
Именно эта host-ФС переполнилась 07.06. Замер ведётся по смонтированным путям `/repos`, `/app/data`
|
||||
(`shutil.disk_usage`), что отражает **хост-раздел**, а не overlay `/` контейнера (NFR-3/AC-8). Новых
|
||||
контейнеров/портов/томов/сетей не требуется. Тот же демон автоматически работает и в staging-инстансе
|
||||
(8501) — на собственной Ф С/путях, без отдельной настройки.
|
||||
|
||||
## I-2. Переменные окружения / секреты
|
||||
Новые env (дескрипторы — в `.env.example`; **без секретов**):
|
||||
|
||||
| Env | Дефолт | Назначение |
|
||||
|-----|--------|------------|
|
||||
| `ORCH_DISK_MONITOR_ENABLED` | `true` | kill-switch (false → демон не стартует, нулевая регрессия). |
|
||||
| `ORCH_DISK_MONITOR_INTERVAL_S` | `300` | период heartbeat-замера, сек. |
|
||||
| `ORCH_DISK_MONITOR_THRESHOLD_PCT` | `85` | порог заполнения для алерта. |
|
||||
| `ORCH_DISK_MONITOR_REALERT_S` | `21600` | cooldown повторного алерта выше порога (~6 ч). |
|
||||
| `ORCH_DISK_MONITOR_PATHS` | `/repos,/app/data` | CSV отслеживаемых host-путей. |
|
||||
|
||||
Telegram-доставка использует **существующие** секреты `send_telegram` (`ORCH_TELEGRAM_*` /
|
||||
`.env`) — новых секретов не вводится. Дефолты пригодны для прода без обязательной правки `.env`
|
||||
(env опциональны — все имеют значения по умолчанию в `config.py`).
|
||||
|
||||
## I-3. Деплой / рестарт
|
||||
- Изменение **не требует** специальной инфра-процедуры сверх штатного self-hosting-деплоя
|
||||
(staging 8501 → прод 8500 через `Confirm Deploy`, ORCH-059/036).
|
||||
- **Self-hosting инвариант соблюдён:** watchdog только читает заполнение и шлёт уведомление — не
|
||||
рестартит/не роняет прод-контейнер, не выполняет действий над диском (NFR-6). Безопасен для
|
||||
enduro-trails в общем инстансе.
|
||||
- Демон стартует/останавливается автоматически в `main.lifespan` (ручной запуск не нужен, AC-1/AC-8).
|
||||
|
||||
### Реакция оператора на алерт (runbook-минимум)
|
||||
При получении Telegram-алерта «Диск mva154 ≥ порога»:
|
||||
1. Зайти на хост (`slin@82.22.50.71`), проверить `df -h /home/slin/repos`.
|
||||
2. Освободить место (кандидаты — порядок ручной очистки): прунинг старых git-worktree
|
||||
`/home/slin/repos/_wt/*` завершённых задач; `docker image prune` / `docker builder prune`;
|
||||
ротация/удаление старых логов. **Авто-очистка — вне объёма ORCH-063** (отдельная задача).
|
||||
3. Дождаться recovery-сообщения «диск ниже порога» (приходит однократно при возврате под порог).
|
||||
|
||||
> Развёрнутый раздел про disk-watchdog (что мониторится, порог, как отключить
|
||||
> `ORCH_DISK_MONITOR_ENABLED`, реакция на алерт) добавляется в `docs/operations/INFRA.md` на стадии
|
||||
> development (TRZ §9, AC-9).
|
||||
|
||||
## I-4. CI/CD
|
||||
Без изменений `.gitea/workflows/`. Новый код покрывается существующим `pytest tests/` (юнит-тесты
|
||||
pure-логики `decide_action`/`measure_paths`/`format_*` + изоляция never-raise — TRZ/AC-3/AC-6).
|
||||
</content>
|
||||
39
docs/work-items/ORCH-063/10-tech-risks.md
Normal file
39
docs/work-items/ORCH-063/10-tech-risks.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
work_item: ORCH-063
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-09
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 10 — Технические риски: ORCH-063 — мониторинг диска mva154 + алерт при ≥85%
|
||||
|
||||
Work Item: **ORCH-063** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
> Информационный (гейтом не парсится). Риски реализации и их митигейшн.
|
||||
|
||||
## Реестр рисков
|
||||
|
||||
| ID | Риск | Вер. | Влия. | Митигейшн |
|
||||
|----|------|------|-------|-----------|
|
||||
| TR-1 | **Замер по неверной ФС** (overlay `/` контейнера вместо host-раздела) → ложно-низкое заполнение → watchdog молчит при реально полном хосте (повтор 07.06). | Сред. | Выс. | ADR D1: замер `shutil.disk_usage` по bind-mount-путям `/repos`/`/app/data` (host-разделы); `/` запрещён как источник (NFR-3/AC-8). Тест AC-8. |
|
||||
| TR-2 | **Спам-алерты на каждом тике** при длительном превышении → шум, оператор глохнет. | Сред. | Сред. | ADR D3: pure `decide_action` — алерт на пересечении + cooldown `disk_monitor_realert_s` (~6 ч); юнит-тест AC-3. |
|
||||
| TR-3 | **Залипший cooldown** — после спада ниже порога состояние не сброшено → новое превышение молчит. | Низ. | Сред. | ADR D3: переход «выше→ниже» сбрасывает `alerting` + однократный recovery; цикл повторяем. Тест AC-4. |
|
||||
| TR-4 | **Исключение в тике/отправке роняет поток или конвейер.** | Низ. | Выс. | ADR D8: never-raise на 3 уровнях (per-path, per-tick, per-send), как `reconciler`/`reaper`. Тест AC-6 (битый путь / падение `send_telegram`). |
|
||||
| TR-5 | **Порог 85% близок к 100% при быстром росте** (один большой build/worktree) → оператор не успевает. | Низ. | Сред. | Дефолт зафиксирован Владельцем; конфигурируем (BR-5). Второй «критический» порог (95%) — кандидат follow-up (OQ-4, вне объёма). |
|
||||
| TR-6 | **Исчерпание inode** (не байтов) валит ФС, но не ловится замером по %-байтам. | Низ. | Сред. | Вне объёма ORCH-063 (BRD §8 R-4); кандидат на расширение замера (`os.statvfs` f_files/f_favail). Задокументировать как known-limitation. |
|
||||
| TR-7 | **Потеря анти-спам-состояния при рестарте** (in-memory) → повторный алерт при всё ещё полном диске. | Сред. | Низ. | Осознанный компромисс (ADR D3, NFR-5): повторный ранний сигнал безопасен; durable-хранение отвергнуто (лишняя миграция). |
|
||||
| TR-8 | **Best-effort Telegram** — алерт не доставлен при сбое сети. | Низ. | Сред. | Та же `send_telegram` (never-raise); cooldown-повтор повышает шанс доставки. Watchdog — ранний сигнал, не SLA (BRD §6). |
|
||||
| TR-9 | **Дедуп по `st_dev` ошибочно схлопнет разные разделы** или `os.stat` упадёт. | Низ. | Низ. | ADR D2: ключ дедупа — фактический `st_dev`; ошибка `os.stat` → fail-open (путь как отдельный, замер не теряется). |
|
||||
|
||||
## Сводный вывод
|
||||
Доминирующий класс — **риски ложного молчания/шума** (TR-1, TR-2, TR-3), полностью закрытые
|
||||
конструктивно: корректный источник замера (host-ФС) + pure-функция анти-спама с юнит-покрытием.
|
||||
Изоляция от конвейера обеспечена never-raise-каркасом проверенных `reconciler`/`reaper`. Эскалация
|
||||
`arch:major-change` **не требуется**: изменение аддитивное, под kill-switch, без правки
|
||||
`STAGE_TRANSITIONS`/`QG_CHECKS`/схемы БД, тривиально откатывается. Возврат в анализ **не требуется** —
|
||||
ТЗ реализуемо без нарушения принципов. Остаточный риск для прод-конвейера (self-hosting) — **низкий**:
|
||||
watchdog только читает и уведомляет, не трогает прод. TR-6 (inode) — осознанная known-limitation вне
|
||||
объёма.
|
||||
</content>
|
||||
Reference in New Issue
Block a user