166 lines
14 KiB
Markdown
166 lines
14 KiB
Markdown
# ТЗ — ORCH-021: Post-deploy мониторинг прода + авто-rollback
|
||
|
||
Work Item: ORCH-021
|
||
Стадия: analysis → (architecture)
|
||
|
||
> Документ описывает ТРЕБОВАНИЯ к изменениям и НАЗЫВАЕТ задействованные модули.
|
||
> Выбор механизма (стадия vs watchdog vs reserved-agent) и точная реализация —
|
||
> зона архитектора (см. BRD §7). Здесь фиксируется, ЧТО должно измениться и КАКИЕ
|
||
> контракты НЕЛЬЗЯ ломать.
|
||
|
||
## 1. Контекст в коде (как есть сейчас)
|
||
|
||
- Конвейер заканчивается в `src/stages.py`: `deploy → done`, gate `check_deploy_status`.
|
||
Терминальный переход `deploy → done` исполняется в `src/stage_engine.py::advance_stage`
|
||
(блок «Terminal sync», `set_issue_done`, release merge-lease). После этого ничего
|
||
не наблюдает за продом.
|
||
- `scripts/orchestrator-deploy-hook.sh` уже умеет:
|
||
- `health_check(max_attempts, sleep, label)` — опрос `http://localhost:$TARGET_PORT/health`
|
||
с проверкой `"status":"ok"`;
|
||
- `do_rollback()` — retag `PREV_IMAGE_FILE` → `TARGET_IMAGE` + рестарт + пост-rollback
|
||
health-check; коды возврата 0 (ок) / 1 (нет prev-образа) / 2 (rollback тоже упал);
|
||
- режим `--rollback` (ручной откат);
|
||
- при обычном деплое сохраняет `PREV_IMG` в `PREV_IMAGE_FILE`
|
||
(`.deploy-prev-image-prod` для прода, см. `settings.deploy_prod_prev_image_file`).
|
||
- Self-deploy прода идёт через detached host-процесс: `src/self_deploy.py`
|
||
(`build_deploy_command`, `initiate_deploy`, sentinel-маркеры под
|
||
`.deploy-state-<repo>/<wi>/`, `read_result`, `map_exit_code_to_status`).
|
||
- Фоновый daemon-паттерн: `src/reconciler.py` (`threading.Thread(daemon=True)` +
|
||
`threading.Event`, старт/стоп в `src/main.py::lifespan` после `worker.start()` /
|
||
перед `worker.stop()`, `status()` в `GET /queue`).
|
||
- Reserved-agent (детерминированный no-LLM job) паттерн: `deploy-finalizer` —
|
||
перехват в `src/agents/launcher.py::launch_job` ДО `_spawn`, исполнение
|
||
`stage_engine.run_deploy_finalizer`, отложенная постановка через
|
||
`enqueue_job(..., available_at_delay_s=...)`.
|
||
- Условность self-hosting: `src/qg/checks.py::is_self_hosting_repo`,
|
||
`src/self_deploy.py::self_deploy_applies` (флаг + CSV-репо; пусто → только `orchestrator`).
|
||
- Наблюдаемые эндпоинты прода (`src/main.py`): `GET /health`, `GET /status`, `GET /queue`.
|
||
- API БД: `src/db.py::enqueue_job` (с `available_at_delay_s`), `get_db`,
|
||
`update_task_stage`, `get_active_tasks_for_reconcile`.
|
||
|
||
## 2. Требуемые изменения
|
||
|
||
### 2.1. Новый leaf-модуль чистой логики наблюдения — `src/post_deploy.py` (новый)
|
||
Контракт **never-raise** (по образцу `self_deploy.py` / `staging_verdict.py`).
|
||
Чистые, юнит-тестируемые функции:
|
||
- **Опрос сигналов:** функция, опрашивающая `/health` и ключевые эндпоинты
|
||
(`/status`, `/queue`) прод-инстанса (base-url из config), возвращающая структуру
|
||
с результатами (код ответа, ok-флаг, доля 5xx). Сеть/таймаут → консервативный
|
||
результат, не исключение.
|
||
- **Классификация деградации** (чистая, без сети): на вход — серия результатов
|
||
опросов; на выход — вердикт `HEALTHY | DEGRADED` по порогам (BR-3):
|
||
`≥ post_deploy_fail_threshold` последовательных провалов health ИЛИ доля 5xx
|
||
выше `post_deploy_5xx_threshold` на окне. Эта функция — основной предмет
|
||
юнит-тестов (детерминированная, как `compute_staging_verdict` в ORCH-061).
|
||
- **Решение о реакции** (чистая): по `(repo, вердикт, политика)` → одно из
|
||
`NONE | ROLLBACK | ALERT_ONLY`, с учётом self-hosting (BR-5).
|
||
- **Запись артефакта** результата наблюдения (см. §2.5), best-effort.
|
||
- Условность: хелпер `post_deploy_applies(repo)` (флаг + CSV-репо, пусто →
|
||
только self-hosting), по образцу `self_deploy_applies` / `_merge_gate_applies`.
|
||
|
||
### 2.2. Оркестрация наблюдения (механизм — выбор архитектора)
|
||
Требования к механизму (независимо от выбора стадия/watchdog/reserved-agent):
|
||
- запускается ПОСЛЕ перехода `deploy → done` для применимого репозитория (BR-1);
|
||
- наблюдает окно `post_deploy_window_s` с интервалом `post_deploy_interval_s`;
|
||
- **restart-safe и идемпотентен** (BR-7): состояние наблюдения — в sentinel-файлах
|
||
(по образцу `.deploy-state-<repo>/<wi>/`, напр. маркеры `monitor-started` /
|
||
`monitor-done`) ИЛИ через отложенные `enqueue_job(available_at_delay_s=...)`;
|
||
повторный старт не задваивает наблюдение и не теряет его при рестарте;
|
||
- по итогу вызывает «Решение о реакции» из `src/post_deploy.py` и исполняет реакцию (§2.3).
|
||
|
||
Кандидатные точки интеграции (на выбор архитектора, см. BRD §7):
|
||
- хук в `stage_engine.advance_stage` в блоке `next_stage == "done"` — арм наблюдения;
|
||
- reserved-agent `post-deploy-monitor` (расширение `launcher.launch_job` ДО `_spawn`,
|
||
как `deploy-finalizer`), с само-перепостановкой через `available_at_delay_s`;
|
||
- отдельный daemon-поток `PostDeployWatcher` (как `Reconciler`), старт/стоп в `main.lifespan`.
|
||
|
||
### 2.3. Реакция на деградацию
|
||
- **Не-self репозитории / политика auto:** вызвать существующий хук в режиме отката
|
||
(`scripts/orchestrator-deploy-hook.sh --rollback` с прод-параметрами окружения,
|
||
как в `self_deploy.build_deploy_command`, но action=`--rollback`). Маппинг
|
||
exit-code хука (0/1/2) в исход переиспользует логику `self_deploy.map_exit_code_to_status`
|
||
по смыслу (0 → откат успешен; 1/2 → откат не выполнен/провалился → громкий алерт).
|
||
- **Self-hosting (`orchestrator`) по умолчанию (BR-5):** НЕ откатывать автоматически.
|
||
Сформировать громкий алерт (Telegram + Plane-коммент) и запросить ручной approve
|
||
отката (по образцу deploy Phase A — статус Plane / Telegram CTA). Откат самого
|
||
прод-орка, если выполняется, — только через detached host-процесс (нельзя надёжно
|
||
откатить контейнер, который при этом умирает; переиспользовать механику
|
||
`self_deploy.initiate_deploy`).
|
||
- Команда отката для self НЕ должна ронять прод-контейнер в рамках обычного тика
|
||
наблюдения (CLAUDE.md: не ронять/не рестартить прод-контейнер вне явного действия).
|
||
|
||
### 2.4. Конфигурация — `src/config.py` (расширение `Settings`)
|
||
Добавить (env-префикс `ORCH_`, дефолты безопасные):
|
||
- `post_deploy_monitor_enabled: bool = True` — глобальный kill-switch (BR-8).
|
||
- `post_deploy_repos: str = ""` — CSV применимых репо; пусто → только self-hosting
|
||
(по образцу `self_deploy_repos` / `merge_gate_repos` / `image_freshness_repos`).
|
||
- `post_deploy_window_s: int = 900` — длина окна наблюдения (дефолт ~15 мин, BR-1).
|
||
- `post_deploy_interval_s: int = 30` — интервал между опросами.
|
||
- `post_deploy_fail_threshold: int = 3` — N последовательных провалов health → DEGRADED.
|
||
- `post_deploy_5xx_threshold: float = 0.5` — порог доли 5xx на окне → DEGRADED.
|
||
- `post_deploy_auto_rollback: bool = False` — глобально разрешён ли авто-откат;
|
||
при `True` действует для не-self репо; для self всегда требует approve (BR-5).
|
||
- `post_deploy_base_url: str = "http://localhost:8500"` — base-url наблюдаемого прода.
|
||
- `post_deploy_target` параметры отката — переиспользовать существующие
|
||
`deploy_prod_*` (service/port/image/prev_image_file), новых дублей не вводить.
|
||
|
||
### 2.5. Артефакт задачи — `16-post-deploy-log.md` (новый)
|
||
В `docs/work-items/<plane-id>/`. YAML-frontmatter (машиночитаемо, канон гейтов;
|
||
для будущей петли уроков BR-10):
|
||
```
|
||
---
|
||
post_deploy_status: HEALTHY | DEGRADED
|
||
action_taken: NONE | ROLLBACK_OK | ROLLBACK_FAILED | ALERT_ONLY
|
||
work_item: <plane-id>
|
||
window_s: <int>
|
||
checks_total: <int>
|
||
checks_failed: <int>
|
||
---
|
||
```
|
||
Тело — человекочитаемая сводка опросов. Записывается best-effort (по образцу
|
||
`self_deploy.write_deploy_log`); отсутствие файла не должно ничего ронять.
|
||
> Артефакт `16-post-deploy-log.md` добавить в перечень артефактов в `CLAUDE.md`
|
||
> и таблицу/описание в `docs/architecture/README.md` (golden-source, в том же PR).
|
||
|
||
### 2.6. Наблюдаемость — `GET /queue` (`src/main.py`) (BR-9)
|
||
Добавить блок `post_deploy` со снимком состояния (enabled, window, активные
|
||
наблюдения, последний исход) — по образцу блока `reconcile` (метод `status()`).
|
||
|
||
### 2.7. Изменения схемы БД
|
||
**Не требуются.** Состояние наблюдения — sentinel-файлы (restart-safe, без миграции,
|
||
по образцу ORCH-36) и/или отложенные jobs. Если архитектор выберет колонку в `tasks`
|
||
для отметки наблюдения — потребуется миграция; предпочтительно избежать (как ORCH-36/53/58).
|
||
|
||
### 2.8. Новые QG checks
|
||
**Не требуются.** Наблюдение происходит ПОСЛЕ `done` и не является gate'ом стадии;
|
||
реестр `QG_CHECKS` и `STAGE_TRANSITIONS` не меняются (если архитектор НЕ выберет
|
||
вариант «отдельная пост-deploy стадия» — тогда потребуется новая стадия+gate, что
|
||
надо явно отразить в ADR; по умолчанию предпочтителен вариант без изменения реестров).
|
||
|
||
## 3. Инварианты (НЕ ломать)
|
||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, контракт `check_deploy_status` /
|
||
`_parse_deploy_status`, момент вердикта `deploy_status`, БАГ-8 откат, terminal-sync
|
||
`deploy → done`, merge-gate, exit-code-контракт хука (0/1/2) — без изменений.
|
||
- Контракт хука: дефолты STAGING-безопасны; прод-параметры приходят только через env.
|
||
- Условность как ORCH-35/36/43/58: реально для `orchestrator`/listed-repos, прочие — no-op.
|
||
- Never-raise: ошибка в наблюдении не роняет worker / lifespan / конвейер других проектов.
|
||
- Self-hosting: тик наблюдения НИКОГДА не рестартит прод-контейнер сам по себе (BR-5).
|
||
|
||
## 4. Задействованные модули (сводка)
|
||
| Модуль | Изменение |
|
||
|--------|-----------|
|
||
| `src/post_deploy.py` | **новый** — чистая логика опроса/классификации/решения/артефакта, never-raise |
|
||
| `src/config.py` | +параметры `post_deploy_*` (kill-switch, окно, пороги, политика) |
|
||
| `src/stage_engine.py` и/или `src/agents/launcher.py` и/или `src/main.py` | арм/исполнение наблюдения (точка — за архитектором) |
|
||
| `scripts/orchestrator-deploy-hook.sh` | переиспользуется (`--rollback`); правки — только если откат self требует отдельной ветки (за архитектором) |
|
||
| `src/main.py` | блок `post_deploy` в `GET /queue` (BR-9); возможный старт daemon в `lifespan` |
|
||
| `docs/work-items/<id>/16-post-deploy-log.md` | **новый** артефакт |
|
||
| `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md` | обновить (golden-source, в том же PR) |
|
||
| ADR | `docs/work-items/ORCH-021/06-adr/ADR-001-*.md` (+ возможный сквозной `adr/adr-00NN`) |
|
||
|
||
## 5. Артефакты по pipeline, которые должны появиться/обновиться
|
||
- `16-post-deploy-log.md` (новый, машиночитаемый frontmatter).
|
||
- Обновлённые `CLAUDE.md` (перечень артефактов), `docs/architecture/README.md`
|
||
(описание пост-деплой наблюдения), `CHANGELOG.md`.
|
||
- ADR work-item (`06-adr/`) с зафиксированным выбором механизма и порогов.
|