187 lines
15 KiB
Markdown
187 lines
15 KiB
Markdown
---
|
||
work_item: ORCH-063
|
||
stage: analysis
|
||
author_agent: analyst
|
||
status: ready-for-review
|
||
created_at: 2026-06-09
|
||
model_used: claude-opus-4-8
|
||
---
|
||
|
||
# 02 — ТЗ (TRZ): ORCH-063 — INFRA: мониторинг диска mva154 + алерт при >85%
|
||
|
||
Work Item: **ORCH-063** · Repo: **orchestrator** · Стадия: analysis
|
||
|
||
> ТЗ описывает **что** и **где** должно измениться (модули/контракты/артефакты), выведенное из BRD и
|
||
> фактического кода. **Как** (точная структура демона, способ замера, хранение состояния анти-спама,
|
||
> точки врезки) — решает архитектор в `06-adr/`. ТЗ фиксирует требования и границы.
|
||
|
||
---
|
||
|
||
## 1. Сводка изменения
|
||
|
||
Ввести **disk-watchdog** — фоновый daemon-поток (по образцу `reconciler`/`job_reaper`), который
|
||
периодически (heartbeat) измеряет заполнение **хост-файловой системы** через смонтированные в
|
||
контейнер bind-пути и при пересечении настраиваемого порога (дефолт **85%**) шлёт **Telegram-алерт**
|
||
оператору. Анти-спам (алерт на пересечение + ограниченное повторение + recovery при возврате ниже
|
||
порога), наблюдаемость в `GET /queue`, kill-switch, never-raise. **Машина стадий, реестр QG и схема
|
||
БД-контрактов не меняются; новой миграции не требуется.**
|
||
|
||
---
|
||
|
||
## 2. Задействованные модули / пути
|
||
|
||
| Путь | Действие |
|
||
|------|----------|
|
||
| `src/disk_watchdog.py` *(новый leaf-модуль; имя — на усмотрение архитектора)* | **создать** — чистая логика замера + решение об алерте (pure, тестируемо) + daemon-обёртка (`threading.Thread(daemon=True)` + `threading.Event`, `start`/`stop`/`status`), never-raise. Образец: `src/reconciler.py`, `src/job_reaper.py`. |
|
||
| `src/config.py` | **изменить** — добавить флаги фичи (см. §8). |
|
||
| `src/main.py` | **изменить** — `start()`/`stop()` watchdog в `lifespan` (после `reaper.start()` / в reverse-порядке на shutdown); добавить read-only блок `disk_monitor` в `GET /queue`. |
|
||
| `src/notifications.py` | **изменить (опц.)** — переиспользовать `send_telegram(text)` (notifying) напрямую из watchdog **или** добавить тонкий helper `notify_disk_alert(...)`/`notify_disk_recovery(...)` (never-raise). Выбор — архитектор. |
|
||
| `.env.example` | **изменить** — задокументировать новые `ORCH_DISK_*` переменные (дескрипторы, без значений-секретов). |
|
||
|
||
> Чистую логику (замер по путям, дедуп по устройству, решение «алертить / повторить / recovery» как
|
||
> функция от текущего %, порога и предыдущего состояния) держать в **leaf-модуле**, never-raise, по
|
||
> образцу `src/task_deps.py` / `src/post_deploy.py` — для юнит-тестируемости без фонового потока.
|
||
|
||
---
|
||
|
||
## 3. Функциональные требования
|
||
|
||
### FR-1 — Heartbeat-демон (BR-1, BR-8)
|
||
- Фоновый daemon-поток измеряет заполнение диска каждые `disk_monitor_interval_s` секунд.
|
||
- Стартует/останавливается в `main.lifespan` (паттерн `reconciler.start()`/`reaper.start()` и reverse
|
||
на shutdown). Период — `threading.Event().wait(interval)` (чистый stop, как `reconciler._run`).
|
||
- Контракт демона: `start()`, `stop(timeout)`, `status() -> dict` (для `/queue`).
|
||
|
||
### FR-2 — Замер заполнения хост-ФС (BR-1, NFR-3)
|
||
- Для каждого пути из `disk_monitor_paths` измерить заполнение (`used/total`, %), свободно (байты/%).
|
||
- **Источник — смонтированные хост-пути**, а не overlay `/` контейнера (NFR-3): дефолтный набор
|
||
путей должен покрывать раздел(ы), на которых растут рабочие данные оркестратора — `/repos`
|
||
(host `/home/slin/repos`) и `/app/data` (host `./data`). Способ замера — предпочтительно stdlib
|
||
`shutil.disk_usage(path)` (без субпроцесса `df` на каждом тике, NFR-2); финальный выбор — архитектор.
|
||
- При совпадении физического устройства у нескольких путей — желательно не дублировать алерт (дедуп
|
||
по устройству `st_dev`/mount); требование «желательно», не блокирующее.
|
||
- Недоступный/несуществующий путь → пропуск этого пути с лог-warning, без падения тика.
|
||
|
||
### FR-3 — Алерт при превышении порога (BR-2)
|
||
- Если заполнение пути **≥ `disk_monitor_threshold_pct`** (дефолт `85`) — сформировать и отправить
|
||
Telegram-алерт через `send_telegram` (notifying, **не** silent — это alert, как `notify_error`).
|
||
- **Содержимое алерта (действенное):** идентификатор хоста/пути (точка монтирования), занято %,
|
||
свободно (ГБ и/или %), порог. Текст — на русском, по стилю существующих `notify_*`-алертов.
|
||
|
||
### FR-4 — Анти-спам, повтор и recovery (BR-3, BR-4)
|
||
- Решение об отправке — функция от `(current_pct, threshold, previous_state, now)`:
|
||
- **переход «ниже→на/выше порога»** → отправить алерт (первое пересечение);
|
||
- **остаётся выше порога** → повторно слать **не чаще**, чем раз в `disk_monitor_realert_s`
|
||
(cooldown), а не на каждом тике;
|
||
- **переход «выше→ниже порога»** → сбросить состояние алерта и отправить однократное
|
||
**recovery-сообщение** «диск ниже порога» (notifying).
|
||
- Состояние анти-спама может быть **in-memory** (best-effort; после рестарта допустим повторный
|
||
алерт, если всё ещё выше порога — безопасно, NFR-5). Время — через инъецируемый `now`-провайдер,
|
||
чтобы решение было тестируемо без реального таймера.
|
||
|
||
### FR-5 — Конфигурируемость и kill-switch (BR-5, NFR-4)
|
||
- Поведение управляется флагами `config.py` (см. §8). При `disk_monitor_enabled=False` watchdog
|
||
**не запускается** (демон не стартует в `lifespan`) — нулевая регрессия.
|
||
|
||
### FR-6 — Наблюдаемость (BR-7)
|
||
- `GET /queue` получает аддитивный read-only блок `disk_monitor` (по образцу блоков `reconcile`/
|
||
`reaper`/`serial_gate`): `enabled`, `threshold_pct`, `interval_s`, `paths` с последним замером
|
||
(`used_pct`, `free_bytes`/`free_gb`), `alerting` (bool на путь/глобально), `last_alert_at`.
|
||
never-raise: при ошибке — минимальный словарь с флагами.
|
||
|
||
---
|
||
|
||
## 4. Изменения API
|
||
|
||
- **Новых обязательных endpoint'ов нет.** Снимок состояния отдаётся через существующий `GET /queue`
|
||
(аддитивный блок `disk_monitor`, §3/FR-6); существующие ключи ответа не меняются.
|
||
- Опционально (на усмотрение архитектора, **не обязательно**): отдельный `GET /disk` для on-demand
|
||
замера. Если вводится — задокументировать в README. Рекомендация: ограничиться блоком в `/queue`.
|
||
|
||
---
|
||
|
||
## 5. Изменения схемы БД
|
||
|
||
**Нет.** Состояние watchdog — best-effort, держится в памяти демона (NFR-5). Новых таблиц/колонок/
|
||
миграций не вводится. `STAGE_TRANSITIONS`/`QG_CHECKS`/`tasks`/`jobs`/`agent_runs` — без изменений.
|
||
|
||
> Если архитектор решит сделать состояние last-alert durable (переживающим рестарт) — допустима
|
||
> только **аддитивная, идемпотентная** миграция (`CREATE TABLE IF NOT EXISTS`), но это **не**
|
||
> требование ТЗ (по умолчанию — in-memory).
|
||
|
||
---
|
||
|
||
## 6. Требования к новым/изменённым QG checks
|
||
|
||
**Нет.** Watchdog — фоновый эксплуатационный демон, **не** Quality Gate стадии. Реестр `QG_CHECKS` и
|
||
`check_*` не трогаются (аналогично `reconciler`/`job_reaper`, которые тоже не являются QG).
|
||
|
||
---
|
||
|
||
## 7. Совместимость / регресс
|
||
|
||
- **Аддитивно:** новый leaf-модуль + точечные врезки в `main.lifespan` и `GET /queue` + флаги config.
|
||
Существующий код не переписывается.
|
||
- **Kill-switch** `disk_monitor_enabled` (дефолт `True`): `False` → демон не стартует, `/queue`-блок
|
||
отдаёт `{"enabled": false}` — поведение приложения 1:1 как сейчас (NFR-4).
|
||
- **never-raise:** изоляция фонового потока (паттерн `reconciler`/`reaper`); сбой замера/отправки/
|
||
тика не влияет на конвейер (BR-6/NFR-1). Демон бежит в общем self-hosting-инстансе — обязан быть
|
||
безопасным для enduro-trails.
|
||
- **Обратимость:** удаление эффекта = выключение флага; миграций БД нет, откат тривиален.
|
||
- **Self-hosting:** watchdog только читает заполнение и шлёт уведомление — не трогает диск/контейнер,
|
||
не рестартит прод (NFR-6).
|
||
|
||
---
|
||
|
||
## 8. Конфигурация (`src/config.py`)
|
||
|
||
По образцу `reconcile_*` / `merge_gate_*`:
|
||
|
||
| Поле (env) | Тип / дефолт | Назначение |
|
||
|------------|--------------|------------|
|
||
| `disk_monitor_enabled` (`ORCH_DISK_MONITOR_ENABLED`) | `bool = True` | kill-switch; `False` → демон не стартует (нулевая регрессия). |
|
||
| `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` | минимальный интервал между повторными алертами, пока выше порога (анти-спам; ~6 ч). |
|
||
| `disk_monitor_paths` (`ORCH_DISK_MONITOR_PATHS`) | `str = "/repos,/app/data"` (CSV) | отслеживаемые пути (смонтированные хост-разделы, NFR-3); пусто → дефолтный набор. |
|
||
|
||
Финальный набор/имена флагов и дефолты уточняет архитектор; диапазон/валидация значений (порог в
|
||
1..100, интервалы > 0) — defensive, невалидное → дефолт + лог-warning (паттерн `reconcile_grace_*`).
|
||
|
||
---
|
||
|
||
## 9. Артефакты pipeline (создать/обновить в ТОМ ЖЕ PR)
|
||
|
||
Документация — golden source (CLAUDE.md §2). По итогам разработки обновить:
|
||
- `docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md` — решение (способ замера хост-ФС,
|
||
набор путей/дедуп, хранение состояния анти-спама, точки врезки, дефолты порога/периода).
|
||
- `docs/architecture/README.md` — новый компонент «Disk-watchdog (ORCH-063)» в списке компонентов +
|
||
описание блока `disk_monitor` в `GET /queue`.
|
||
- `docs/operations/INFRA.md` — раздел/строки про disk-watchdog: что мониторится, порог, как
|
||
отключить (`ORCH_DISK_MONITOR_ENABLED`), что делать при алерте (ручная очистка — ссылка/руководство).
|
||
- `.env.example` — новые `ORCH_DISK_*` дескрипторы.
|
||
- `CHANGELOG.md` — запись `feat:`.
|
||
- При новом endpoint `/disk` (если архитектор введёт) — обновить таблицу API в README.
|
||
|
||
---
|
||
|
||
## 10. Инварианты (не нарушать)
|
||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, схема существующих таблиц БД — **без изменений**.
|
||
- never-raise на тик демона; сбой watchdog не блокирует и не роняет конвейер (NFR-1).
|
||
- Замер — по **хост-разделам** (bind-mount-пути), не по overlay `/` контейнера (NFR-3).
|
||
- Не рестартить/не ронять прод-контейнер; watchdog только читает и уведомляет (NFR-6, self-hosting).
|
||
- При выключенном флаге — поведение 1:1 как сейчас; enduro-trails не затрагивается.
|
||
|
||
---
|
||
|
||
## 11. Открытые вопросы для архитектора (не блокируют анализ)
|
||
- OQ-1: Способ замера — stdlib `shutil.disk_usage(path)` vs субпроцесс `df` (рекомендация — stdlib,
|
||
NFR-2).
|
||
- OQ-2: Дедуп путей по физическому устройству (`os.stat().st_dev`), чтобы единый host-раздел не
|
||
алертил дважды.
|
||
- OQ-3: Состояние анти-спама — in-memory (рекомендация) vs durable (доп. таблица); влияет на
|
||
поведение после рестарта.
|
||
- OQ-4: Нужен ли второй «критический» порог (напр. 95%) с усиленным/более частым алертом — кандидат,
|
||
по умолчанию **нет** (один порог 85%).
|
||
- OQ-5: Helper в `notifications.py` (`notify_disk_alert`) vs прямой вызов `send_telegram` из watchdog.
|