diff --git a/docs/architecture/README.md b/docs/architecture/README.md index c8b2de5..11a6e47 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -91,6 +91,42 @@ sentinel-файлы (`/.deploy-state-//`), без мигр Подробнее: [adr-0007](adr/adr-0007-executable-self-deploy.md), детально — `docs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md`. +### Post-deploy наблюдение прода + реакция на деградацию (ORCH-021 — design) +Конвейер заканчивался на `deploy → done` и **забывал про прод**: «успех» = health-check +в момент рестарта (~60с). Класс «зелёный деплой, красный прод» (прецедент ET-8 — +деградация через минуты под трафиком, health `200 ok`, фича сломана). ORCH-021 продлевает +ответственность **ЗА** `done`: для применимого репо после терминального перехода армится +наблюдение окна `post_deploy_window_s` (~15 мин) с интервалом `post_deploy_interval_s`; +деградация фиксируется по детерминированным порогам, при подтверждении — реакция. + +Механизм — **reserved-agent job `post-deploy-monitor`** (калька `deploy-finalizer`, НЕ +стадия и НЕ daemon): арм в `advance_stage` в блоке `next_stage == "done"` +(`post_deploy.arm_monitor`, sentinel `armed` = идемпотентность); тик перехватывается в +`launcher.launch_job` ДО `_spawn` → `stage_engine.run_post_deploy_monitor` (один опрос → +append в `series` → классификация → перепостановка с задержкой ИЛИ реакция+артефакт+`done`). +Чистая логика — новый leaf-модуль `src/post_deploy.py` (never-raise): `post_deploy_applies`, +`probe_signals` (`/health` 200+`{"status":"ok"}` + доля 5xx на `/status`,`/queue`), +`classify` (HEALTHY|DEGRADED — главный предмет юнит-тестов), `decide_action`, +sentinel-state, `write_post_deploy_log`. +- **Пороги (BR-3):** `DEGRADED` ⇔ `≥ post_deploy_fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ провалов + health ИЛИ доля 5xx `> post_deploy_5xx_threshold`; одиночный глюк → HEALTHY (нет ложных + откатов). +- **Реакция:** self-hosting (`orchestrator`) — ВСЕГДА `ALERT_ONLY` (Telegram+Plane, ручной + approve; тик НИКОГДА не откатывает/рестартит прод-контейнер); не-self + + `post_deploy_auto_rollback=true` → хук `--rollback` (`0→ROLLBACK_OK`, + `1/2→ROLLBACK_FAILED`+алерт); дефолт → `ALERT_ONLY`. +- **Артефакт** `16-post-deploy-log.md` (YAML-frontmatter `post_deploy_status`/ + `action_taken`/…) — машиночитаемо для петли уроков ORCH-8; best-effort. +- **Наблюдаемость** — блок `post_deploy` в `GET /queue` (образец `reconcile`). +- **Инварианты:** `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`, terminal-sync, + merge-gate, exit-коды хука (0/1/2), схема БД — НЕ меняются. Restart-safe (sentinel + `.post-deploy-state-//` + jobs-очередь). Kill-switch + `post_deploy_monitor_enabled`, область `post_deploy_repos` (пусто → self-hosting). + Условность как ORCH-35/36/43/58. + +Подробнее: [adr-0010](adr/adr-0010-post-deploy-monitor.md), детально — +`docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md`. + ### Свежесть артефакта BUILD-ONCE: провенанс staging-образа (ORCH-058 — реализовано) BUILD-ONCE retag (ORCH-36) промоутит `SOURCE_IMAGE=orchestrator-orchestrator-staging` в прод **без rebuild**, полагаясь на «staging-образ свеж и провалидирован». Этой гарантии нет: @@ -197,7 +233,7 @@ never-raise на единицу работы; тишина при синхрон |--------|------|----------| | GET | `/health` | health check | | GET | `/status` | активные задачи (stage != done) | -| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + последние jobs | +| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + post_deploy (ORCH-021) + последние jobs | | POST | `/webhook/plane` | Plane webhook | | POST | `/webhook/gitea` | Gitea webhook (push, PR, CI status) | @@ -211,4 +247,4 @@ never-raise на единицу работы; тишина при синхрон Схема БД, потоки данных, resilience-слой, детали Dockerfile — [internals.md](internals.md). --- -*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled).* +*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — design, ветка feature/ORCH-021-post-deploy-rollback (`arch:major-change`; при реализации обновлять также при изменении src/post_deploy.py, src/stage_engine.py арм/run_post_deploy_monitor, src/agents/launcher.py перехват, флаги post_deploy_*; артефакт 16-post-deploy-log.md).* diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index cac6d70..76ffba8 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -15,11 +15,12 @@ Per-work-item решения живут в `docs/work-items//06-adr/ADR-NNN- | adr-0007 | Исполняемый самодеплой стадии `deploy` (файл adr-0007-executable-self-deploy) | accepted | 2026-06-06 | ORCH-036 | | adr-0008 | Провенанс staging-образа перед BUILD-ONCE retag | accepted | 2026-06-06 | ORCH-058 | | adr-0009 | Толерантность staging-вердикта к инфраструктурным FAIL | accepted | 2026-06-07 | ORCH-061 | +| adr-0010 | Post-deploy мониторинг прода + реакция на деградацию | proposed | 2026-06-07 | ORCH-021 | > ⚠️ Историческая коллизия: номер `0007` занят двумя файлами — > `adr-0007-reconciler.md` (ORCH-053) и `adr-0007-executable-self-deploy.md` > (ORCH-036). Оба accepted; для новых сквозных ADR использовать следующий -> свободный номер (текущий максимум — `0009`). +> свободный номер (текущий максимум — `0010`). ## Формат **Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded. diff --git a/docs/architecture/adr/adr-0010-post-deploy-monitor.md b/docs/architecture/adr/adr-0010-post-deploy-monitor.md new file mode 100644 index 0000000..2fc7883 --- /dev/null +++ b/docs/architecture/adr/adr-0010-post-deploy-monitor.md @@ -0,0 +1,85 @@ +# adr-0010: Post-deploy мониторинг прода + реакция на деградацию + +- **Статус:** proposed (design) — реализация в ветке `feature/ORCH-021-post-deploy-rollback` +- **Дата:** 2026-06-07 +- **Задача:** ORCH-021 +- **Метка:** `arch:major-change` (новая под-компонента + новый reserved-agent job-kind) +- **Детальный ADR:** `docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md` + +## Контекст +Конвейер заканчивается на `deploy → done`: `check_deploy_status` видит +`deploy_status: SUCCESS` → terminal-sync (Plane → Done, release merge-lease), и +оркестратор **забывает про прод**. «Успех» сегодня = health-check в момент рестарта +(~60с окно в `orchestrator-deploy-hook.sh`). Класс инцидентов «зелёный деплой, красный +прод» (прецедент **ET-8**): деградация проявляется через минуты под боевым трафиком, +health отвечает `200 ok`, фича сломана. Для self-hosting опасно вдвойне — сломанный +прод-орк (8500) обслуживает ВСЕ проекты из общего инстанса. + +## Решение +Продлить ответственность конвейера **ЗА** `done`: после терминального перехода для +применимого репо армится пост-деплой наблюдение окна `post_deploy_window_s` (дефолт +~15 мин) с интервалом `post_deploy_interval_s`; деградация фиксируется по +**детерминированным порогам**, при подтверждении выполняется реакция. + +**Механизм — reserved-agent job `post-deploy-monitor`** (калька `deploy-finalizer`, +ORCH-36), НЕ отдельная стадия и НЕ daemon-поток: +- **Арм:** в `stage_engine.advance_stage`, в блоке `next_stage == "done"`, при + `post_deploy.post_deploy_applies(repo)` → `post_deploy.arm_monitor(...)` (sentinel + `armed` = идемпотентность, первый job через `enqueue_job(available_at_delay_s=...)`). +- **Тик:** `launcher.launch_job` перехватывает `agent == "post-deploy-monitor"` ДО + `_spawn` → `stage_engine.run_post_deploy_monitor(job)`: один опрос сигналов, append в + персистентный `series`, классификация; HEALTHY и окно не истекло → перепостановка с + задержкой; иначе → реакция + артефакт + `mark_done`. +- **Чистая логика — новый leaf-модуль `src/post_deploy.py`** (never-raise, по образцу + `self_deploy.py`/`staging_verdict.py`): `post_deploy_applies`, `probe_signals` + (опрос `/health` + доля 5xx на `/status`,`/queue`), `classify` (HEALTHY|DEGRADED — + главный предмет юнит-тестов), `decide_action` (NONE|ROLLBACK|ALERT_ONLY с учётом + self-hosting), sentinel-state хелперы, `write_post_deploy_log`. + +**Сигналы и пороги (детерминированно, AC-3…AC-6):** `DEGRADED` ⇔ `≥ +post_deploy_fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ провалов health ИЛИ доля 5xx на окне `> +post_deploy_5xx_threshold`. Одиночный глюк < порога → HEALTHY (нет ложных откатов). + +**Реакция (BR-4/BR-5):** +- **Self-hosting (`orchestrator`) — ВСЕГДА `ALERT_ONLY`:** громкий Telegram + Plane, + запрос ручного approve отката. Тик НИКОГДА не откатывает/рестартит прод-контейнер + (структурный инвариант). Откат прод-орка, если оператор решит, — только detached + host-процесс (`self_deploy.initiate_deploy`), вне тика (MVP). +- **Не-self + `post_deploy_auto_rollback=True`:** хук `--rollback` с прод-env; exit + `0 → ROLLBACK_OK`, `1/2 → ROLLBACK_FAILED` + громкий алерт. +- Дефолт (`auto_rollback=False`) → `ALERT_ONLY`. + +**Артефакт `16-post-deploy-log.md`** (новый) с YAML-frontmatter (`post_deploy_status`, +`action_taken`, `window_s`, `checks_total/failed`) — машиночитаемо для петли уроков +ORCH-8; best-effort. **Наблюдаемость** — блок `post_deploy` в `GET /queue` (образец +`reconcile.status()`). + +## Альтернативы +- **Daemon-watchdog (как reconciler)** — отклонён: per-task серия опросов в памяти не + restart-safe (а деплой орка = рестарт); restart-safe-вариант требует тех же sentinel, + reserved-agent проще и уже имеет проверенную jobs+sentinel машинерию. +- **Отдельная пост-deploy стадия + QG** — отклонён: меняет `STAGE_TRANSITIONS`/ + `QG_CHECKS`, ломает семантику терминального `done`; наблюдение принципиально ПОСЛЕ + `done`. +- **Авто-rollback прод-орка из тика** — отклонён (self-hosting safety): групповой риск; + контейнер не откатит себя надёжно. Self → alert + ручной approve (как ORCH-54). +- **Колонка в `tasks`** — отклонён: миграция на проде; sentinel-файлы restart-safe + (как ORCH-36/53/58). + +## Последствия +- Класс «зелёный деплой, красный прод» закрыт измеримыми порогами; деградация = + сигнал для ORCH-8. +- Реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`), контракт `check_deploy_status`, + terminal-sync, merge-gate, exit-code-контракт хука, схема БД — **не меняются**. +- Дефолты безопасны: kill-switch on, auto-rollback off, self только alert. +- Ограничение: монитор self бежит внутри наблюдаемого прода — полностью wedged + контейнер = пропущенный тик/алерт (known MVP gap; внешний watchdog — follow-up). +- Self-hosting: тик не рестартит/не роняет прод-контейнер; kill-switch + `post_deploy_monitor_enabled` обязателен; поэтапный раскат через `post_deploy_repos`. + +## Связи +adr-0007-executable-self-deploy (ORCH-36 — sentinel/detached-host/finalizer образец, +`map_exit_code_to_status`), adr-0007-reconciler (ORCH-53 — daemon/`status()` образец, +отклонён как основной механизм), adr-0006 (merge-gate — условность/флаги раската), +adr-0003 (staging-gate — образец условности), adr-0008 (provenance — `.deploy-prev-image`/ +хук-откат). Прецедент ET-8. Будущее: ORCH-8 (петля уроков), ORCH-54 (полный авто). diff --git a/docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md b/docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md new file mode 100644 index 0000000..7e6d225 --- /dev/null +++ b/docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md @@ -0,0 +1,212 @@ +# ADR-001 (ORCH-021): Post-deploy мониторинг прода + реакция на деградацию + +## Статус +Proposed (design) — реализация в ветке `feature/ORCH-021-post-deploy-rollback`. +Сквозной индексный ADR: `docs/architecture/adr/adr-0010-post-deploy-monitor.md`. +Помечено `arch:major-change` (новая под-компонента + новый reserved-agent job-kind). + +## Контекст +Конвейер заканчивается на `deploy → done` (`check_deploy_status` видит +`deploy_status: SUCCESS` → terminal-sync, Plane → Done, release merge-lease). После +этого оркестратор **забывает про прод**. «Успех» сегодня = прохождение health-check +в момент рестарта (10×6с в `scripts/orchestrator-deploy-hook.sh`) — узкое окно ~60с. + +Класс инцидентов «зелёный деплой, красный прод» (прецедент **ET-8**): деградация +проявляется через минуты под боевым трафиком (прогрев кэшей, фоновые миграции, +утечки, рост 5xx), health отвечает `200 ok`, но фича сломана. Для self-hosting это +критично: сломанный прод-орк (8500) обслуживает ВСЕ проекты из общего инстанса. + +BRD/ТЗ задают требования (BR-1…BR-11, AC-1…AC-18) и оставляют архитектору **три +открытых вопроса** (BRD §7): (1) где живёт наблюдение — стадия / watchdog-daemon / +reserved-agent job; (2) механизм self-rollback; (3) пороги/веса сигналов. + +Существующие переиспользуемые механики: +- **deploy-finalizer** (ORCH-36, `stage_engine.run_deploy_finalizer` + перехват в + `launcher.launch_job` ДО `_spawn`) — детерминированный no-LLM reserved-agent job, + само-перепостановка через `enqueue_job(available_at_delay_s=...)`, defer-budget, + restart-safe (jobs-очередь + sentinel-файлы `.deploy-state-//`). +- **self_deploy.py** — sentinel-state хелперы (`write_marker`/`has_marker`/ + `read_result`/`clear_state`), detached host-процесс (`build_deploy_command`/ + `initiate_deploy`: ssh + setsid), `map_exit_code_to_status`, `self_deploy_applies`. +- **reconciler.py** — daemon-поток + `status()` в `GET /queue`. +- **хук `--rollback`** (`do_rollback`): retag `PREV_IMAGE_FILE` → `TARGET_IMAGE` + + рестарт + health, коды 0 / 1 (нет prev-образа) / 2 (rollback тоже упал). +- **Условность** ORCH-35/36/43/58: `is_self_hosting_repo`, флаг + CSV-репо. + +## Решение + +### 1. Механизм наблюдения — reserved-agent job `post-deploy-monitor` (Вариант B) +Наблюдение реализуется как **детерминированный no-LLM reserved-agent job**, точная +калька **deploy-finalizer**. Один «тик» наблюдения = один job: он делает ОДИН опрос +сигналов, обновляет персистентные счётчики в sentinel-файлах, классифицирует и либо +**перепостанавливает себя** с задержкой `post_deploy_interval_s` (окно не истекло и +ещё не DEGRADED), либо завершает наблюдение (DEGRADED → реакция; либо окно истекло → +HEALTHY). Это «watchdog поверх очереди»: между тиками job не выполняется (он +запланирован в будущем через `available_at_delay_s`), worker свободен для других +проектов — ровно как defer у finalizer. + +**Почему НЕ daemon-watchdog (Вариант A, как reconciler):** daemon тикает глобально, а +не per-task; серию опросов (последовательные провалы health, доля 5xx на окне) пришлось +бы держать в памяти → теряется/двоится при рестарте (а сам деплой орка = рестарт). Чтобы +сделать daemon restart-safe, всё равно нужны персистентные per-task счётчики в sentinel — +тогда reserved-agent проще и уже имеет проверенную restart-safe машинерию (jobs-очередь ++ `requeue_running_jobs` + sentinels). Per-task жизненный цикл естественно ложится на +job-цепочку, а не на глобальный sweep. + +**Почему НЕ отдельная пост-deploy стадия (Вариант C):** меняет `STAGE_TRANSITIONS` + +реестр `QG_CHECKS` (нарушает AC-12, ТЗ §2.8 — явно непредпочтительно); ломает семантику +`deploy → done` как терминального перехода (Plane уже Done). Наблюдение происходит +**ПОСЛЕ** `done` — «продление ответственности ЗА done», а не новая стадия конвейера. + +### 2. Арм наблюдения — хук в terminal-блоке `advance_stage` +В `stage_engine.advance_stage`, в существующем блоке `next_stage == "done"` (после +`set_issue_done` и `release_merge_lease`), добавляется арм: +``` +if next_stage == "done" and post_deploy.post_deploy_applies(repo): + post_deploy.arm_monitor(repo, work_item_id, branch, task_id) +``` +`arm_monitor` (never-raise): если sentinel `armed` отсутствует → создаёт state-dir, +пишет `armed` (идемпотентность, по образцу `INITIATED`), инициализирует `series`-файл, +ставит первый `post-deploy-monitor` job через `enqueue_job(available_at_delay_s= +post_deploy_interval_s)`. Если `armed` уже есть → no-op (двойной webhook / reconciler +F-1 / finalizer Phase C могут довести `done` повторно — AC-15). Выключенный +kill-switch / неприменимый репо → `post_deploy_applies` False → арма нет (AC-2/AC-10). + +### 3. Чистая логика — новый leaf-модуль `src/post_deploy.py` (never-raise) +По образцу `self_deploy.py` / `staging_verdict.py`. Импортирует только config (+lazy +`qg.checks.is_self_hosting_repo`), НЕ импортирует `stage_engine`/`launcher`. Функции: +- **`post_deploy_applies(repo) -> bool`** — флаг `post_deploy_monitor_enabled` + + CSV `post_deploy_repos` (пусто → только self-hosting). Калька `self_deploy_applies`. +- **`probe_signals(base_url) -> ProbeResult`** — один опрос: `GET /health` (HTTP 200 + + `{"status":"ok"}`) и ключевые эндпоинты `/status`, `/queue` (учёт доли 5xx). + Сеть/таймаут → консервативный «провал»-результат, не исключение. +- **`classify(series, fail_threshold, 5xx_threshold) -> "HEALTHY"|"DEGRADED"`** — + чистая, без сети, **главный предмет юнит-тестов** (детерминированная, как + `compute_staging_verdict`): `DEGRADED` если `≥ fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ + провалов health (AC-4) ИЛИ доля 5xx на окне `> 5xx_threshold` (AC-5). Иначе + `HEALTHY` (одиночный провал < порога с восстановлением → HEALTHY, AC-3/AC-6). +- **`decide_action(repo, verdict) -> "NONE"|"ROLLBACK"|"ALERT_ONLY"`** — чистая: + `HEALTHY → NONE`; `DEGRADED` + self-hosting → `ALERT_ONLY` (BR-5/AC-8, ВСЕГДА); + `DEGRADED` + не-self + `post_deploy_auto_rollback=True` → `ROLLBACK`; иначе → + `ALERT_ONLY`. +- **Sentinel-state хелперы** (state-dir `.post-deploy-state-//`, по образцу + `self_deploy._state_dir`): `armed`, `series` (JSON-список результатов опросов, + append каждый тик — restart-safe счётчики), `done`. `read_series`/`append_probe`/ + `mark_done`/`has_marker` — never-raise. +- **`write_post_deploy_log(...)`** — артефакт `16-post-deploy-log.md`, best-effort + (по образцу `self_deploy.write_deploy_log`). +- **`build_rollback_command(repo)`** — argv хука `--rollback` с прод-env (как + `build_deploy_command`, но action=`--rollback`; переиспользует `deploy_prod_*`). + +### 4. Исполнение тика — `stage_engine.run_post_deploy_monitor(job)` + перехват в launcher +По образцу `run_deploy_finalizer` / `_run_deploy_finalizer_job`: +`launcher.launch_job` перехватывает `agent == "post-deploy-monitor"` ДО `_spawn` → +`stage_engine.run_post_deploy_monitor(job)`. Алгоритм тика (never-raise): +1. `mark_done` уже стоит → no-op (AC-15, защита от дубля). +2. `probe = post_deploy.probe_signals(base_url)`; `append_probe(series, probe)`. +3. `verdict = classify(series, ...)`. +4. **Если `HEALTHY` и окно не истекло** (число тиков < `window_s/interval_s`) → + перепостановка `post-deploy-monitor` через `available_at_delay_s=interval_s` + (как finalizer defer; счётчик тиков — из jobs-очереди/`series`, restart-safe). +5. **Если `HEALTHY` и окно истекло** → исход `NONE`, `write_post_deploy_log(HEALTHY, + NONE)`, `mark_done`, нотификация «окно завершилось чисто» (BR-6/AC-17). +6. **Если `DEGRADED`** → `action = decide_action(...)`; исполнить реакцию (§5), + `write_post_deploy_log`, `mark_done`, нотификации. + +`mark_done` + sentinel `armed` дают идемпотентность; jobs-очередь + +`requeue_running_jobs` + `series` дают restart-safe (AC-15). Бюджет тиков bounded +(`window_s/interval_s`) — анти-livelock, как `deploy_finalize_max_attempts`. + +### 5. Реакция на деградацию +- **Self-hosting (`orchestrator`), всегда (BR-5/AC-8):** `ALERT_ONLY`. НЕ откатывать + и НЕ рестартить прод-контейнер в тике. Громкий Telegram + Plane-коммент с запросом + ручного approve отката (по образцу deploy Phase A CTA). `action_taken: ALERT_ONLY`. + Откат самого прод-орка (если оператор решит) — ТОЛЬКО через detached host-процесс + (контейнер не откатит себя, умирая); переиспользуется механика + `self_deploy.initiate_deploy`, но в MVP она вне тика наблюдения (ручной approve → + отдельный путь, как ORCH-54 для авто-deploy). Тик self НИКОГДА не запускает хук + `--rollback` (структурный инвариант). +- **Не-self + `post_deploy_auto_rollback=True` (AC-7):** вызвать хук `--rollback` с + прод-env (`build_rollback_command`). Маппинг exit-code по смыслу + `map_exit_code_to_status`: `0 → ROLLBACK_OK`; `1/2 → ROLLBACK_FAILED` + громкий + Telegram о необходимости ручного вмешательства (AC-9). Целевой контейнер не есть + orchestrator → его рестарт безопасен для конвейера. +- **Не-self + auto_rollback=False (дефолт):** `ALERT_ONLY`. + +### 6. Артефакт `16-post-deploy-log.md` (новый, машиночитаемый) +YAML-frontmatter (канон гейтов; для петли уроков ORCH-8, BR-10): +``` +--- +post_deploy_status: HEALTHY | DEGRADED +action_taken: NONE | ROLLBACK_OK | ROLLBACK_FAILED | ALERT_ONLY +work_item: +window_s: +checks_total: +checks_failed: +--- +``` +Тело — человекочитаемая сводка опросов. Best-effort (отсутствие файла ничего не роняет, +AC-13). **Не** читается ни одним гейтом — наблюдение происходит после `done`. + +### 7. Конфигурация — `src/config.py` (env-префикс `ORCH_`) +- `post_deploy_monitor_enabled: bool = True` — глобальный kill-switch (BR-8/AC-10). +- `post_deploy_repos: str = ""` — CSV применимых репо; пусто → только self-hosting. +- `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` — глоб. разрешение авто-отката (для self + всегда требует approve, BR-5). +- `post_deploy_base_url: str = "http://localhost:8500"` — наблюдаемый прод. +- Параметры отката — переиспользовать существующие `deploy_prod_*` (новых дублей нет). + +### 8. Наблюдаемость — блок `post_deploy` в `GET /queue` (BR-9/AC-14) +По образцу блока `reconcile` (метод `status()`): `enabled`, `window_s`, `interval_s`, +активные наблюдения (по sentinel `armed` без `done`), последний исход +(`post_deploy_status`/`action_taken`). Best-effort, never-raise. + +### Инварианты (НЕ меняются) +`STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_deploy_status`/`_parse_deploy_status`, +момент вердикта `deploy_status`, БАГ-8 откат, terminal-sync `deploy → done`, merge-gate, +exit-code-контракт хука (0/1/2), схема БД. Условность как ORCH-35/36/43/58. Never-raise +во всём наблюдении (AC-16). Тик self НИКОГДА не рестартит прод-контейнер (AC-8). + +## Альтернативы +- **Daemon-watchdog (как reconciler)** — отклонён: per-task серия в памяти не + restart-safe; restart-safe-вариант требует тех же sentinel-счётчиков → reserved-agent + проще и уже проверен. +- **Отдельная пост-deploy стадия + QG** — отклонён: меняет реестры (AC-12), ломает + семантику терминального `done`; наблюдение принципиально ПОСЛЕ `done`. +- **Авто-rollback прод-орка из тика** — отклонён (BR-5): контейнер не откатит себя + надёжно; групповой риск для всех проектов. Self → только ALERT + ручной approve. +- **Новая колонка в `tasks` для отметки наблюдения** — отклонён: миграция на проде + (риск, как в adr-0007); sentinel-файлы достаточны и restart-safe (как ORCH-36/53/58). +- **Прометей/APM** — вне рамок (BR out-of-scope): опираемся на существующие + HTTP-эндпоинты, не вводим сбор метрик. + +## Последствия +- Класс «зелёный деплой, красный прод» закрыт измеримыми порогами; деградация — + машиночитаемый сигнал для петли уроков (ORCH-8). +- Плюс: максимальное переиспользование проверенной finalizer/sentinel/hook-машинерии; + нулевая миграция БД; реестры не тронуты; дефолты безопасны (auto-rollback off, self + только alert). +- Минус/ограничение: монитор self бежит ВНУТРИ наблюдаемого прод-контейнера — если + контейнер полностью wedged, worker может не выполнить тик и алерта не будет (gap). + Это known limitation MVP; внешний независимый watchdog — follow-up (вне рамок). +- Минус: каждый тик на короткое время занимает single-worker (`max_concurrency=1`); + митигируется коротким опросом (~секунды) и `interval_s` между тиками (defer не держит + worker), как finalizer. +- Доменный smoke результата фичи (BR-11) — follow-up; MVP = health + 5xx. + +## Связи +- **ET-8** — обоснование (deploy SUCCESS, прод не работает). +- **adr-0007-executable-self-deploy** (ORCH-36) — sentinel-паттерн, detached + host-процесс, `map_exit_code_to_status`, deploy-finalizer reserved-agent (образец). +- **adr-0007-reconciler** (ORCH-53) — daemon/`status()` образец (рассмотрен и отклонён + как основной механизм; `status()`-снимок в `/queue` переиспользуется). +- **adr-0006-merge-gate** / **adr-0003-staging-gate** — образец условности и флагов + раската (`*_enabled` + `*_repos`). +- **adr-0008-staging-image-provenance** — `.deploy-prev-image` / хук-механика отката. +- **ORCH-8** — петля уроков (потребитель `16-post-deploy-log.md`). +- **ORCH-54** — будущий полный авто (включая авто-approve отката self), по аналогии + с авто-deploy. diff --git a/docs/work-items/ORCH-021/07-infra-requirements.md b/docs/work-items/ORCH-021/07-infra-requirements.md new file mode 100644 index 0000000..aca365a --- /dev/null +++ b/docs/work-items/ORCH-021/07-infra-requirements.md @@ -0,0 +1,56 @@ +# 07 — Инфраструктурные требования (ORCH-021) + +> Топология НЕ меняется. Фича опирается на уже существующие HTTP-эндпоинты прода и +> существующий деплой-хук. Этот документ фиксирует, какие инфра-предпосылки должны +> выполняться, чтобы наблюдение и реакция работали. + +## 1. Топология — без изменений +- Прод `orchestrator` (8500), staging `orchestrator-staging` (8501), один сервер + mva154 (см. `docs/operations/INFRA.md`). Новых контейнеров/портов/сервисов нет. +- Наблюдение — внутрипроцессный reserved-agent job в worker'е прод-контейнера. + Daemon-потоков не добавляется (в отличие от reconciler). + +## 2. Наблюдаемый прод — HTTP-эндпоинты +- Монитор опрашивает `post_deploy_base_url` (дефолт `http://localhost:8500`): + - `GET /health` → ожидается HTTP 200 + тело `{"status":"ok"}` (BR-2); + - `GET /status`, `GET /queue` → учёт доли HTTP 5xx (BR-2). +- Эндпоинты уже существуют (`src/main.py`). Новых эндпоинтов фича НЕ вводит + (out-of-scope APM/метрики). +- Для self-hosting `base_url=localhost:8500` означает: монитор бьёт по собственному + контейнеру. Это допустимо для MVP (см. риск R-1 в `10-tech-risks.md`). + +## 3. Деплой-хук `--rollback` — предпосылки реакции +- Реакция ROLLBACK (только не-self + `post_deploy_auto_rollback=True`) вызывает + `scripts/orchestrator-deploy-hook.sh --rollback` с прод-env (переиспользуются + `deploy_prod_*`: `TARGET_SERVICE`/`TARGET_PORT`/`TARGET_IMAGE`/`COMPOSE_PROFILE`/ + `PREV_IMAGE_FILE`), по образцу `self_deploy.build_deploy_command`. +- Предпосылка: при штатном деплое хук сохраняет предыдущий образ в + `PREV_IMAGE_FILE` (`.deploy-prev-image-prod`). Без снимка → хук вернёт exit 1 + («нет prev-образа») → `ROLLBACK_FAILED` + алерт (AC-9). Контракт exit-кодов хука + (0/1/2) НЕ меняется. +- **Self-hosting:** откат прод-орка хуком в тике ЗАПРЕЩЁН (контейнер не откатит себя, + умирая). Если оператор по алерту решит откатить — только detached host-процесс + (ssh + setsid, механика `self_deploy.initiate_deploy`), как у Phase B самодеплоя. + Предпосылки для detached-пути (ssh-доступ host, shared-mount state-dir) уже + выполнены для ORCH-36; в MVP detached-откат self вне тика наблюдения. + +## 4. Restart-safe состояние — shared mount +- Состояние наблюдения — sentinel-файлы под `.post-deploy-state-//` + (`armed`, `series`, `done`) на том же mount `settings.repos_dir`, что и + `.deploy-state-*` (ORCH-36). Миграции БД нет (см. `08-data-requirements.md`). +- `requeue_running_jobs` (ORCH-1) восстанавливает claimed `post-deploy-monitor` job + после рестарта; `series` хранит счётчики опросов → наблюдение продолжается + с того же места (BR-7/AC-15). + +## 5. Конфигурация окружения (env `ORCH_*`) +Новые ключи (дефолты безопасны, в `.env`/`.env.staging` по необходимости): +`post_deploy_monitor_enabled` (kill-switch, дефолт true), `post_deploy_repos` (CSV, +пусто → self-hosting), `post_deploy_window_s` (900), `post_deploy_interval_s` (30), +`post_deploy_fail_threshold` (3), `post_deploy_5xx_threshold` (0.5), +`post_deploy_auto_rollback` (false), `post_deploy_base_url` (localhost:8500). +Параметры отката — существующие `deploy_prod_*`, новых дублей не вводить. + +## 6. Чего НЕ требуется +- Новых контейнеров, портов, сетевых правил, секретов. +- Prometheus / Grafana / APM (out-of-scope). +- Изменений compose-топологии или деплой-пути не-self репо. diff --git a/docs/work-items/ORCH-021/08-data-requirements.md b/docs/work-items/ORCH-021/08-data-requirements.md new file mode 100644 index 0000000..bc7b040 --- /dev/null +++ b/docs/work-items/ORCH-021/08-data-requirements.md @@ -0,0 +1,40 @@ +# 08 — Требования к данным / схеме БД (ORCH-021) + +## Вывод: миграция БД НЕ требуется +Состояние наблюдения хранится в **sentinel-файлах** (restart-safe, без миграции — +по образцу ORCH-36/53/58), а не в таблицах. Реестры и схема не меняются (AC-12). + +## 1. Существующие таблицы — без изменений +- `events`, `tasks`, `agent_runs`, `jobs` — структура не меняется. +- В `tasks` НЕ вводится колонка статуса/окна наблюдения (намеренно — миграция на + проде = риск, как обосновано в adr-0007; альтернатива отклонена в ADR-001 §Альтернативы). + +## 2. Очередь `jobs` — переиспользование, без схемы +- `post-deploy-monitor` — новый **job-kind** (значение в существующей колонке + `agent`/`task_content`), НЕ новая колонка. Ставится через существующий + `enqueue_job(..., available_at_delay_s=...)` (ORCH-1). +- Счётчик тиков/деферов восстанавливается из jobs-очереди (как + `_deploy_finalize_defer_count` считает по `task_content LIKE`), restart-safe. + +## 3. Sentinel-состояние (файлы, не БД) +State-dir `.post-deploy-state-//` на `settings.repos_dir` +(по образцу `.deploy-state-*`): +| Файл | Назначение | +|------|------------| +| `armed` | наблюдение заармлено (идемпотентность арма; калька `INITIATED`) | +| `series` | JSON-список результатов опросов (счётчики health-fail / 5xx; restart-safe) | +| `done` | наблюдение завершено (защита от повторной обработки) | + +Все обращения — never-raise (по образцу `self_deploy.has_marker`/`write_marker`/ +`read_result`). Отсутствие/битость файла → консервативный фоллбэк, не исключение. + +## 4. Артефакт `16-post-deploy-log.md` — файл репозитория, не БД +Машиночитаемый YAML-frontmatter (`post_deploy_status`, `action_taken`, `window_s`, +`checks_total`, `checks_failed`) пишется best-effort в `docs/work-items//`; в БД +не реплицируется. Источник для петли уроков ORCH-8 (BR-10). + +## 5. Очистка состояния +По завершении окна / реакции `done`-маркер ставится; state-dir можно чистить +best-effort (по образцу `self_deploy.clear_state`) — необязательно для корректности, +но желательно для гигиены. Stale-`armed` без `done` после краха → виден в `/queue` +как «активное наблюдение» и доигрывается восстановленным job'ом. diff --git a/docs/work-items/ORCH-021/10-tech-risks.md b/docs/work-items/ORCH-021/10-tech-risks.md new file mode 100644 index 0000000..5ddd1e8 --- /dev/null +++ b/docs/work-items/ORCH-021/10-tech-risks.md @@ -0,0 +1,20 @@ +# 10 — Технические риски (ORCH-021) + +| # | Риск | Вероятн. | Влияние | Митигация | +|---|------|----------|---------|-----------| +| R-1 | **Монитор self бежит внутри наблюдаемого прода.** Полностью wedged прод-контейнер → worker не выполнит тик → деградация не замечена, алерта нет. | Сред. | Высок. | Known MVP limitation (зафиксировано в ADR-001 §Последствия). Health в момент рестарта (хук) + reconciler ловят часть случаев. Внешний независимый watchdog — follow-up (вне рамок). | +| R-2 | **Ложный авто-rollback** по сетевому глюку. | Низк. | Высок. | Пороги по N ПОСЛЕДОВАТЕЛЬНЫХ провалов + доля 5xx на окне (BR-3/AC-6), а не разовый провал. Self ВСЕГДА `ALERT_ONLY` (BR-5). `auto_rollback=False` по умолчанию. | +| R-3 | **Авто-rollback прод-орка убивает инструмент всех проектов.** | Низк. | Критич. | Структурный инвариант: тик self НИКОГДА не откатывает/рестартит прод-контейнер (AC-8). Self → только alert + ручной approve. Откат self — только detached host-процесс вне тика. | +| R-4 | **Нет prev-образа** при ROLLBACK → откат невозможен. | Сред. | Сред. | Хук возвращает exit 1 → `ROLLBACK_FAILED` + громкий алерт (AC-9), деградация не проглатывается тихо. | +| R-5 | **Дубль/потеря наблюдения** при двойном webhook / рестарте. | Сред. | Сред. | Идемпотентность: sentinel `armed` (арм-гард) + `done` (защита от повторной обработки) + restart-safe jobs-очередь + `series` (AC-15). По образцу finalizer. | +| R-6 | **Исключение в наблюдении роняет worker / конвейер других проектов.** | Низк. | Высок. | Контракт never-raise во всём `post_deploy.py` и `run_post_deploy_monitor` (AC-16), по образцу `self_deploy`/`staging_verdict`. | +| R-7 | **Тик занимает single-worker** (`max_concurrency=1`) → задержка других задач. | Низк. | Низк. | Опрос короткий (~секунды), между тиками job не выполняется (defer через `available_at_delay_s`) — worker свободен, как у finalizer. Окно bounded (`window_s/interval_s`). | +| R-8 | **Скрытое изменение контракта** (реестры/гейты/exit-коды/схема). | Низк. | Высок. | Инвариант: `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_deploy_status`/terminal-sync/merge-gate/exit-коды/схема БД НЕ меняются (AC-12). Существующие тесты deploy/staging/merge-gate должны остаться зелёными. | +| R-9 | **5xx на `/queue`/`/status` из-за самого монитора** (рекурсивная нагрузка). | Низк. | Низк. | Интервал `post_deploy_interval_s` (30с) — низкая частота; опрос лёгкий GET. | +| R-10 | **Артефакт `16-post-deploy-log.md` не пишется / невалиден** → петля уроков без данных. | Низк. | Низк. | Best-effort запись с валидным frontmatter (AC-13); отсутствие файла ничего не роняет. Парсинг — defensive. | + +## Эскалация +- Изменение помечено `arch:major-change` (новая под-компонента `src/post_deploy.py` + + новый reserved-agent job-kind `post-deploy-monitor`). +- R-1 (gap наблюдения для wedged self-контейнера) — кандидат на отдельную задачу + (внешний watchdog), вне рамок ORCH-021.