--- work_item: ORCH-063 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-09 model_used: claude-opus-4-8 --- # 01 — BRD (бизнес-требования): ORCH-063 — INFRA: мониторинг диска mva154 + алерт при >85% Work Item: **ORCH-063** · Repo: **orchestrator** (self-hosting) · Стадия: analysis Заказчик: Слава (Владелец/оператор) Тип: INFRA · Приоритет: **P1** --- ## 1. Бизнес-контекст и проблема ### 1.1. Инцидент (установленный факт) **07.06.2026** диск на хосте **mva154** (`slin@82.22.50.71`) незаметно дорос до **100%** и положил **весь конвейер**: CI стал красным, очередь Gitea застряла. Сбой произошёл **тихо** — не было ни одного предупреждающего сигнала до полного исчерпания диска. Разбор был ручным и пост-фактум. ### 1.2. Корневая боль У оркестратора **нет проактивного сигнала о заполнении диска**. Диск хоста заполняется накопительно и предсказуемо (git-worktree в `/repos/_wt/...`, образы Docker, БД `./data/orchestrator.db`, логи), но оператор узнаёт о проблеме только когда уже **поздно** — конвейер всех проектов (self-hosting: `orchestrator` + `enduro-trails` из одного инстанса) уже встал. ### 1.3. Self-hosting контекст (групповой риск) Прод-инстанс `orchestrator` (8500) — ОДИН на ВСЕ прод-проекты, с общей БД и общей очередью (`docs/operations/INFRA.md`). Исчерпание диска роняет конвейер **всех** проектов сразу. Ранний сигнал (heartbeat-watchdog) — дешёвая страховка от дорогого группового простоя. ### 1.4. Что нужно (формулировка Владельца) **Heartbeat-watchdog:** периодически измерять заполнение диска (`df`); при превышении порога **85%** — слать алерт Славе (Telegram). Сигнал должен прийти **заранее**, пока есть запас места на ручную/будущую авто-очистку. --- ## 2. Объём (scope) ### 2.1. В объёме - **Фоновый watchdog-демон** (по образцу `reconciler`/`job_reaper`, ORCH-053/065): периодически семплит заполнение хост-ФС, на которой живут рабочие данные оркестратора (репозитории, БД, Docker), и при пересечении порога шлёт Telegram-алерт оператору. - **Конфигурируемый порог** (дефолт **85%**), период опроса, kill-switch. - **Анти-спам:** алерт по факту пересечения порога + ограниченное по частоте повторение, пока заполнение выше порога (а не на каждом тике); сообщение о возврате «ниже порога» (recovery). - **Наблюдаемость** последнего замера/состояния алерта в `GET /queue` (read-only). - **never-raise:** любой сбой watchdog не влияет на конвейер. ### 2.2. Вне объёма (явно, не делать) - **Авто-очистка / garbage collection диска** (прунинг старых worktree, образов, логов, vacuum БД) — отдельная задача; ORCH-063 только **сигнализирует**, не **лечит**. - Интеграция с внешними системами мониторинга (Prometheus/Grafana/Zabbix), метрики/экспортёры. - Алерт-каналы кроме существующего Telegram (`send_telegram`). - Мониторинг ресурсов кроме диска (CPU/RAM/inode — возможное расширение, не сейчас; inode — кандидат на follow-up, см. §8 R-4). - Мониторинг нескольких хостов / удалённый сбор (только локальный хост текущего инстанса). - Изменение `STAGE_TRANSITIONS`, реестра `QG_CHECKS`, стадий конвейера, схемы БД-контрактов. --- ## 3. Заинтересованные стороны - **Владелец/оператор (Слава):** получает алерт, выполняет ручную очистку/реакцию; принимает результат. - **Self-hosting прод (`orchestrator`):** обслуживает enduro-trails из того же инстанса — watchdog не должен мешать/ронять конвейер (изоляция через never-raise). - **Все прод-проекты:** косвенные бенефициары — ранний сигнал предотвращает групповой простой. --- ## 4. Бизнес-требования (BR) | ID | Требование | Связь | |----|------------|-------| | BR-1 | Оркестратор **периодически** (heartbeat) измеряет заполнение хост-файловой системы, на которой растут его рабочие данные (репозитории `/repos`, БД `/app/data`, Docker). | FR-1, AC-1 | | BR-2 | При достижении/превышении **порога заполнения** (дефолт **85%**) оператор получает **Telegram-алерт** с действенными деталями: точка монтирования/путь, занято %, свободно (ГБ/%). | FR-2, FR-3, AC-2 | | BR-3 | **Анти-спам:** алерт шлётся при **пересечении** порога (переход «ниже→на/выше»), а далее повторяется не чаще, чем раз в настраиваемый период (`re-alert`), пока заполнение остаётся выше порога — конвейер/чат не заваливается одинаковыми сообщениями на каждом тике. | FR-4, AC-3 | | BR-4 | При возврате заполнения **ниже порога** состояние алерта сбрасывается и отправляется однократное сообщение восстановления «диск ниже порога» (recovery), чтобы оператор знал, что инцидент снят. | FR-4, AC-4 | | BR-5 | Порог, период опроса, период повторного алерта и набор отслеживаемых путей **конфигурируемы**; есть **kill-switch** для полного отключения watchdog (нулевая регрессия). | FR-5, AC-5 | | BR-6 | **never-raise:** любая ошибка измерения/отправки алерта/самого демона **не роняет** и не блокирует конвейер (фоновый поток, изолированный как `reconciler`/`reaper`). | NFR-1, AC-6 | | BR-7 | Текущее состояние watchdog (последний замер по путям, состояние алерта, время последнего алерта, порог/период) наблюдаемо в `GET /queue` (read-only). | FR-6, AC-7 | | BR-8 | Watchdog стартует/останавливается вместе с приложением (в `main.lifespan`) и не требует ручного запуска. | FR-1, AC-8 | --- ## 5. Нефункциональные требования (NFR) | ID | Требование | |----|------------| | NFR-1 | **never-raise / изоляция:** watchdog — отдельный daemon-поток (паттерн `reconciler`/`job_reaper`); исключение в тике логируется и не прерывает ни поток, ни конвейер. | | NFR-2 | **Дешевизна:** замер диска — лёгкая операция (предпочтительно stdlib `shutil.disk_usage`, без тяжёлого порождения процессов на каждом тике); период опроса по умолчанию — порядка минут (не секунд), чтобы не создавать нагрузки. | | NFR-3 | **Корректность источника замера (self-hosting):** измеряется заполнение **хост-ФС**, а не overlay-ФС контейнера. Контейнер видит хост-разделы через bind-mount'ы (`/repos`, `/app/data`); замер обязан отражать раздел(ы), которые реально заполняются на хосте (см. §6). | | NFR-4 | **Нулевая регрессия:** при выключенном kill-switch поведение приложения идентично текущему; enduro-trails и конвейер не затрагиваются. | | NFR-5 | **Инварианты неизменны:** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, существующие таблицы-контракты БД — не меняются. Допустимо не вводить новую миграцию (состояние watchdog — best-effort, может жить в памяти). | | NFR-6 | **Self-hosting безопасность:** watchdog только **читает** заполнение и **шлёт** уведомление — не выполняет действий над диском/контейнером, не рестартит прод. | --- ## 6. Допущения и ограничения - **Видимость хост-диска из контейнера.** Оркестратор бежит в контейнере с `network_mode: host` и bind-mount'ами `/home/slin/repos → /repos`, `./data → /app/data`, `/var/run/docker.sock` (`docs/operations/INFRA.md`). Замер `shutil.disk_usage()`/`df` по **смонтированному пути** (`/repos`, `/app/data`) отражает заполнение **хост-раздела**, который этот путь подмонтировал — именно той ФС, что переполнилась 07.06. Замер по `/` (overlay контейнера) **нерепрезентативен** и не должен использоваться как источник истины. - **Один заполняющийся раздел.** На mva154, вероятно, рабочие данные (`/home/slin/repos`, `./data`, Docker) лежат на одном host-разделе; набор отслеживаемых путей по умолчанию должен покрывать его и при совпадении физического устройства не дублировать алерт (дедуп по устройству — желательное, не блокирующее требование; решение — за архитектором). - **Best-effort алертинг.** Доставка Telegram не гарантирована (та же `send_telegram`, never-raise); watchdog — ранний сигнал, не SLA-гарантия. Состояние анти-спама может быть in-memory (после рестарта допустим повторный алерт, если всё ещё выше порога — это безопасно). - **Порог 85%** — зафиксирован Владельцем как дефолт; конфигурируем (BR-5) на случай тюнинга. - **Только сигнал, не лечение.** Авто-освобождение места — вне объёма (§2.2). --- ## 7. Критерии успеха (резюме; детали — 03-acceptance-criteria.md) - AC-1 watchdog периодически измеряет заполнение хост-ФС и стартует с приложением. - AC-2 при ≥85% оператор получает Telegram-алерт с действенными деталями. - AC-3 анти-спам: один алерт на пересечение + ограниченное повторение, не на каждом тике. - AC-4 возврат ниже порога → сброс состояния + recovery-сообщение. - AC-5 порог/период/пути/kill-switch конфигурируемы; выключение → нулевая регрессия. - AC-6 любой сбой watchdog не роняет конвейер (never-raise). - AC-7 состояние watchdog видно в `GET /queue`. --- ## 8. Риски (детали — 10-tech-risks.md, заполняет архитектор) - **R-1** — замер по неверной ФС (overlay `/` контейнера вместо хост-раздела) → ложно-низкое заполнение → watchdog «молчит» при реально полном хосте (повтор инцидента 07.06). Митигировать: замер по bind-mount-путям хост-разделов (NFR-3). - **R-2** — спам-алерты на каждом тике при длительном превышении порога → шум, оператор глохнет к сигналу. Митигировать: анти-спам/cooldown (BR-3). - **R-3** — порог 85% слишком близок к 100% при быстром росте (один большой build/worktree) → оператор не успевает среагировать. Зафиксирован как дефолт Владельцем; конфигурируемость (BR-5) оставляет рычаг. Возможный follow-up — второй «критический» порог (напр. 95%) с более громким алертом (кандидат, не в объёме). - **R-4** — исчерпание **inode** (а не байтов) тоже валит ФС, но не ловится замером по %-байтам. Кандидат на расширение (вне объёма ORCH-063). - **R-5** — `df`/субпроцесс на каждом тике — лишняя нагрузка; предпочесть stdlib (NFR-2).