148 lines
15 KiB
Markdown
148 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
|
||
---
|
||
|
||
# 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).
|