architect(ET): auto-commit from architect run_id=484
All checks were successful
CI / test (push) Successful in 31s
All checks were successful
CI / test (push) Successful in 31s
This commit is contained in:
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