Files
orchestrator/docs/work-items/ORCH-114/01-brd.md

187 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
work_item: ORCH-114
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
escalate: full-cycle
---
# 01 — BRD (бизнес-требования): ORCH-114 — Ownership-lease для side-effectful переходов стадий + умное восстановление при старте
Work Item: **ORCH-114** · Repo: **orchestrator** · Стадия: analysis
> **Багфикс-трек → ЭСКАЛАЦИЯ В ПОЛНЫЙ ЦИКЛ (`escalate: full-cycle`).** Задача пришла под меткой
> `Bug` (укороченный маршрут ORCH-019, пропуск `architecture`), но дефект **системный и
> архитектурный**: вводится глобальный инвариант владения переходом, durable-механизм, переживающий
> рестарт процесса, и compare-and-swap на запись стадии. Это требует **ADR** (выбор механизма:
> lease / heartbeat / transition-epoch / CAS) и затрагивает поведение всего конвейера и нескольких
> фоновых акторов. Поэтому выпускается **полный** analysis-пакет; оператор снимает багфикс-трек
> эндпоинтом `POST /bug-fast-track/escalate?work_item=ORCH-114` → задача уходит в `architecture`
> (ADR-001 D5 ORCH-019).
---
## 1. Бизнес-контекст и проблема
ORCH-114 — **системный наследник** инцидент-цепочки ORCH-110 / ORCH-111 / ORCH-112 / ORCH-113.
Каждый предшественник закрыл **точечный** симптом, но **корневой класс** остался открыт:
**у side-effectful переходов стадий нет единого владения (ownership)**.
### Корень (установленный факт, верифицировано кодом)
`stage_engine.advance_stage(...)` — единая точка перехода между стадиями и исполнения тяжёлых
под-гейтов ребра `deploy-staging → deploy` (security → merge-gate re-test → coverage →
image-freshness) и под-гейта `deploy → done` (`_handle_merge_verify`: `merge_pr`, ratchet
coverage-baseline, запись proof-of-merge). При этом:
- **Запись стадии не атомарна по предусловию.** `db.update_task_stage(task_id, stage)`
«голый» `UPDATE tasks SET stage=? WHERE id=?` **без** `WHERE stage=?` (нет compare-and-swap, нет
epoch/version-колонки). Любой второй вызов безусловно перезатирает результат первого.
- **`advance_stage` ре-ентерабельна без защиты.** Внутри неё нет ни in-memory-лока на `task_id`,
ни durable-маркера «переход в процессе». Два конкурентных вызова для одной задачи оба читают
`stage='deploy-staging'`, оба прогоняют ВСЕ под-гейты, оба пишут `deploy`, оба ставят
следующего агента.
- **Минимум 5 путей входят в переход независимо:** (1) монитор агента (`launcher._try_advance_stage`,
auto-advance по `exit_code==0`), (2) Plane-webhook (`webhooks/plane._try_advance_stage`,
Approved / Confirm Deploy), (3) reconciler F-1 (`advance_if_gate_passed → advance_stage`,
`finished_agent=None`), (4) job-reaper (`job_reaper._gate_driven_advance → launcher._try_advance_stage`),
(5) deploy-finalizer Phase C (`run_deploy_finalizer → advance_stage(finished_agent="deployer")`).
Ни один не проверяет, не находится ли **другой** актор уже внутри того же перехода.
### Почему предшественники не закрыли класс
| Задача | Что закрыла | Что осталось открытым |
|--------|-------------|------------------------|
| **ORCH-110** | merge-gate re-test: ложный rollback по инфра-таймауту + tree-kill осиротевших pytest | Только merge-gate re-test; не вводит владения переходом |
| **ORCH-112** | гигиена общего deploy-checkout (грязь блокировала `git pull`) | Только чистка артефактов; не про конкурентные переходы |
| **ORCH-113** | reaper не пере-исполняет **живую** финализацию `deploy-staging` (Tier-2) | **process-local in-memory** реестр (`finalizer_liveness`), **только reaper**, **только Tier-2**, **только `deploy-staging`**; **теряется при рестарте**; **НЕ** покрывает reconciler / webhook / restart-recovery |
Таким образом, **остаточный кросс-путь** (ORCH-113 §ограничения сам это фиксирует): живой монитор
внутри `advance_stage(deploy-staging)` — и параллельно reaper (при выключенном liveness-флаге или
**после рестарта**, когда in-memory реестр пуст), либо reconciler F-1, либо webhook-путь — повторно
входят в тот же переход. Результат — **двойные** эффекты (security/merge/coverage/image-freshness/
прод-деплой) и **противоречивые** исходы (один путь откатывает на `development`, другой доводит до
`done`). Именно это наблюдалось в инциденте ORCH-111 (job 1914 / PR #130): повторный re-test покраснел
и дал ложный откат `deploy-staging → development` с ложным developer-retry, **одновременно** с успешной
финализацией и мержем оригинального монитора.
### Особый разрез — рестарт процесса (self-hosting)
Прод-контейнер `orchestrator` рестартится при self-деплое. Если процесс умирает **в середине**
финализации, in-memory `finalizer_liveness._OWNED` исчезает, `requeue_running_jobs` переводит
`running → queued`, и переход может быть **пере-исполнен с нуля** без знания, что часть необратимых
шагов (мерж в `main`, ratchet baseline, прод-деплой) уже применена. **Durable**-сигнал владения,
переживающий рестарт, отсутствует — это ключевая дельта ORCH-114 над ORCH-113.
---
## 2. Объём (scope)
### В объёме
- Единый **инвариант владения** side-effectful переходом/финализацией: в любой момент времени
переход конкретной задачи исполняет **не более одного** актора.
- **Compare-and-swap (CAS)** / transition-epoch на запись стадии: писатель применяет переход
только если предусловие (текущая стадия / эпоха) не изменилось с момента чтения; проигравший —
аборт **без** побочных эффектов.
- **Durable** механизм владения (lease/heartbeat/epoch — выбор за архитектором), переживающий
рестарт процесса.
- Осведомлённость **job-reaper** и **startup-requeue** о живой / устаревшей финализации (обобщение
ORCH-113 за пределы Tier-2 / `deploy-staging` / in-memory).
- **Reconciler F-1** и **webhook**-пути: skip/defer при активном lease перехода.
- **Умное восстановление при старте**: после смерти процесса в середине финализации система сходится
к **единственному** согласованному исходу (без двойных необратимых эффектов и без противоречий
rollback↔done).
- **Наблюдаемость**: read-only блок в `GET /queue` + алерт на форсированный/устаревший реклейм lease.
- **Регресс-тесты**: `deploy-staging`-ребро, deploy-finalizer (Phase C), restart-recovery.
### Вне объёма
- Изменение состава/порядка стадий (`STAGE_TRANSITIONS`), реестра `QG_CHECKS`, семантики/имён
`check_*`, машинных вердикт-ключей (`verdict:`/`result:`/`deploy_status:`/`staging_status:`/
`security_status:`/`coverage_status:`) — **байт-в-байт не трогаются**.
- Повторная починка частных симптомов ORCH-110/112 (merge-retest tree-kill, checkout-hygiene) —
они уже закрыты; ORCH-114 их **переиспользует**, не переписывает.
- Переход на `uvicorn --workers>1` / мульти-процессную модель (остаётся одно-процессной; durable-lease
лишь делает инвариант корректным и на этот случай, но миграция модели — отдельная задача).
- Выбор конкретного механизма (lease vs heartbeat vs epoch), точная форма хранения (доп. таблица vs
доп. колонки) и порядок старта демонов — **решает архитектор** в `06-adr/` (это требования к
свойствам, не к реализации).
---
## 3. Заинтересованные стороны
- **Owner / оператор self-hosting** — заказчик; страдает от ложных откатов, двойных деплоев и ручного
разбора расхождений состояния.
- **Все проекты в общем инстансе** (orchestrator + enduro-trails) — групповой риск: расхождение
состояния и ложный freeze репо клинят общую очередь.
- **Принимает результат:** Owner; технически — финальная стадия конвейера (CI/гейты), не агент сам.
---
## 4. Бизнес-требования (BR)
| ID | Требование (проверяемое) | Покрытие |
|----|---------------------------|----------|
| **BR-1** | Side-effectful переход/финализацию задачи в любой момент исполняет **не более одного** актора (единое владение). | FR-1 / AC-1 |
| **BR-2** | Запись стадии для side-effectful переходов защищена **compare-and-swap / epoch**: проигравший гонку писатель не мутирует стадию и **не выполняет** побочных эффектов. | FR-2 / AC-1, AC-2 |
| **BR-3** | **job-reaper** осведомлён о живой vs устаревшей финализации на **всех** релевантных путях (не только Tier-2/`deploy-staging`): defer при живом владении, реклейм мёртвого/устаревшего владельца в **ограниченное** время. | FR-3 / AC-4, AC-5 |
| **BR-4** | **Startup-requeue / восстановление при старте** учитывает незавершённую финализацию через durable-состояние: не пере-исполняет уже применённый необратимый шаг. | FR-4 / AC-6 |
| **BR-5** | **Reconciler F-1** и **webhook**-пути продвижения **пропускают/откладывают** переход, пока активен lease владения для задачи. | FR-5 / AC-7, AC-8 |
| **BR-6** | После смерти процесса в середине финализации система сходится к **единственному** согласованному исходу: **нет** двойного `merge_pr` / ratchet baseline / image-rebuild / инициации прод-деплоя и **нет** противоречия rollback↔done. | FR-1…FR-4 / AC-1, AC-6 |
| **BR-7** | Состояние владения переходом **наблюдаемо**: read-only блок в `GET /queue` + алерт при форсированном/устаревшем реклейме. | FR-6 / AC-12 |
| **BR-8** | Поставляются **регресс-тесты** на конкурентный двойной эффект (`deploy-staging`), deploy-finalizer (Phase C) и restart-recovery; обязательный регресс воспроизводит исходный класс (красный до фикса, зелёный после). | FR-7 / AC-1, AC-6, тест-план |
| **BR-9** | Механизм **обратим**: kill-switch возвращает поведение **байт-в-байт** к состоянию до ORCH-114. | FR-7 / AC-9 |
---
## 5. Нефункциональные требования (NFR)
| ID | Требование |
|----|------------|
| **NFR-1** | **never-raise.** Любая ошибка механизма владения изолируется. Горячий путь claim/guard — **fail-open** (не заклинить общую очередь всех проектов, AC-8 ORCH-088); решения, критичные для безопасности прода/необратимости — **fail-closed**. |
| **NFR-2** | **Kill-switch + область раската** по образцу leaf-гейтов (`serial_gate`/`coverage_gate`/`finalizer_liveness`): глобальный флаг + при необходимости CSV-скоуп репо (пусто → self-hosting only). При выключенном флаге — нулевая регрессия (enduro не затронут). |
| **NFR-3** | **Инварианты конвейера не тронуты:** `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / машинные вердикт-ключи / схемы существующих таблиц — байт-в-байт. Любое новое хранилище — **аддитивно и идемпотентно** (`CREATE TABLE IF NOT EXISTS` / `_ensure_column`). |
| **NFR-4** | **Durable / restart-safe.** Сигнал владения переживает рестарт процесса (ключевая дельта над in-memory `finalizer_liveness` ORCH-113); после рестарта восстановление детерминированно решает «дорешать vs уже применено». |
| **NFR-5** | **Self-hosting безопасность.** Механизм владения сам по себе **никогда** не рестартит прод-контейнер, не пушит/force-push в `main`, не трогает detached deploy-процесс (NFR-3 ORCH-090/112). |
| **NFR-6** | **Сквозной бюджет reaper сохранён:** инвариант ORCH-065/109/110/113 `reaper_max_running_s (5400) > Σ(deploy-staging gate-work ≈4460) + grace`. Lease **не** удлиняет финализацию за backstop без согласованной правки бюджета; устаревший/мёртвый владелец добивается Tier-3 в ограниченное время. |
| **NFR-7** | **Идемпотентность.** Повторный заход в уже применённый переход — **no-op** (по epoch / SHA-in-main / lease), никогда не второй побочный эффект. |
| **NFR-8** | **Обратная совместимость.** При флаге off / репо вне области — путь старта, claim и переходы байт-в-байт прежние (enduro и текущий orchestrator). |
---
## 6. Допущения и ограничения
- **Одно-процессная модель сейчас** (один uvicorn-воркер без `--workers`), но требование NFR-4
(durable) делает инвариант корректным и при будущем рестарте/мульти-процессности — без переписывания.
- **Источник истины планировщика — локальная БД** (offline hot-path, NFR-2/ORCH-026/088): механизм
владения не должен вносить сетевых зависимостей в горячий claim.
- **Переиспользуются существующие durable-примитивы:** атомарный `reap_running_job` (rowcount-guard),
`claim_next_job` (rowcount-guard), `requeue_running_jobs`, merge-lease (ORCH-043). ORCH-114 **достраивает**
владение поверх них, а не дублирует.
- **`finalizer_liveness` (ORCH-113)** — отправная точка: ORCH-114 обобщает её до durable, кросс-путевого
владения; решение «расширить / заменить / надстроить» принимает архитектор.
- Точные **D-решения** (durable shape, эпоха vs lease-таблица, набор покрываемых рёбер сверх
`deploy-staging`/`deploy→done`, порядок старта демонов) — за архитектором (`06-adr/`, `10-tech-risks.md`).
## 7. Критерии успеха
Кратко (детальные PASS/FAIL — `03-acceptance-criteria.md`):
- Конкурентный/после-рестартовый повторный вход в side-effectful переход **не** даёт двойных эффектов
и противоречивых исходов; ровно один актор владеет и доводит переход.
- CAS/epoch на запись стадии: проигравший — чистый аборт.
- reaper / startup / reconciler / webhook осведомлены о живом lease (defer) и о мёртвом (реклейм в
ограниченное время).
- Полный `pytest tests/` зелёный; новые регресс-тесты (двойной эффект, restart-recovery) зелёные;
при выключенном kill-switch — поведение байт-в-байт прежнее.
## 8. Риски
Краткий перечень (детали — `10-tech-risks.md`, заполняет архитектор):
- **Дедлок / over-block:** слишком «жёсткое» владение может заклинить легитимный путь (reaper не
добьёт зависший финализатор) → требование NFR-6 (bounded reclaim) и fail-open на hot-path.
- **Бюджет vs lease:** lease, удерживаемый дольше `reaper_max_running_s`, конфликтует со сквозным
бюджетом → согласование с ORCH-065/109/110/113.
- **Durable-состояние и гонки на рестарте:** некорректный «умный recovery» может сам стать источником
двойного применения → обязательный restart-recovery регресс (BR-8).
- **Скрытые пути перехода** (gitea-webhook `handle_push`/`handle_ci_status`/`handle_pr` пишут стадию
**в обход** `advance_stage` через прямой `update_task_stage`) → охват CAS должен учитывать и их
(архитектор фиксирует границу).