From 4d9251c69807510bf34db5a5bff267de5c6ddf08 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 9 Jun 2026 18:43:16 +0300 Subject: [PATCH] architect(ET): auto-commit from architect run_id=484 --- docs/architecture/README.md | 1 + .../adr/adr-0024-disk-watchdog.md | 59 ++++++ .../ORCH-063/06-adr/ADR-001-disk-watchdog.md | 196 ++++++++++++++++++ .../ORCH-063/07-infra-requirements.md | 63 ++++++ docs/work-items/ORCH-063/10-tech-risks.md | 39 ++++ 5 files changed, 358 insertions(+) create mode 100644 docs/architecture/adr/adr-0024-disk-watchdog.md create mode 100644 docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md create mode 100644 docs/work-items/ORCH-063/07-infra-requirements.md create mode 100644 docs/work-items/ORCH-063/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 16da09e..497b78d 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -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 Сквозной (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` + diff --git a/docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md b/docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md new file mode 100644 index 0000000..2531eec --- /dev/null +++ b/docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md @@ -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`). + + diff --git a/docs/work-items/ORCH-063/07-infra-requirements.md b/docs/work-items/ORCH-063/07-infra-requirements.md new file mode 100644 index 0000000..c89f221 --- /dev/null +++ b/docs/work-items/ORCH-063/07-infra-requirements.md @@ -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). + diff --git a/docs/work-items/ORCH-063/10-tech-risks.md b/docs/work-items/ORCH-063/10-tech-risks.md new file mode 100644 index 0000000..9b87335 --- /dev/null +++ b/docs/work-items/ORCH-063/10-tech-risks.md @@ -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 вне +объёма. +