diff --git a/docs/work-items/ORCH-114/01-brd.md b/docs/work-items/ORCH-114/01-brd.md new file mode 100644 index 0000000..17a4852 --- /dev/null +++ b/docs/work-items/ORCH-114/01-brd.md @@ -0,0 +1,186 @@ +--- +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 должен учитывать и их + (архитектор фиксирует границу). diff --git a/docs/work-items/ORCH-114/02-trz.md b/docs/work-items/ORCH-114/02-trz.md new file mode 100644 index 0000000..ad2e392 --- /dev/null +++ b/docs/work-items/ORCH-114/02-trz.md @@ -0,0 +1,143 @@ +--- +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 +--- + +# 02 — ТЗ (TRZ): ORCH-114 — Ownership-lease для side-effectful переходов стадий + умное восстановление при старте + +Work Item: **ORCH-114** · Repo: **orchestrator** · Стадия: analysis + +> ТЗ описывает **конкретные требования к реализации**, выведенные из BRD (`01-brd.md`) и фактического +> кода. **Выбор механизма** (durable lease / heartbeat / transition-epoch / форма хранения, набор +> покрываемых рёбер) и архитектурное обоснование — задача архитектора (`06-adr/`). Здесь — *что* +> должно быть истинно и *какие модули* затрагиваются, не *как* именно. + +## 1. Сводка изменения + +Вводится **единый инвариант владения** side-effectful переходом стадии: запись стадии и исполнение +тяжёлых под-гейтов/финализации защищаются **durable-механизмом владения** (lease/epoch) + **CAS** на +запись стадии, так что в любой момент переход конкретной задачи доводит **ровно один** актор, а +конкурентный/после-рестартовый повторный вход (reaper / reconciler / webhook / startup-requeue) +**откладывается или становится no-op** вместо повторного применения необратимых эффектов +(merge_pr / coverage-ratchet / image-rebuild / инициация прод-деплоя / противоречивый rollback↔done). +Аддитивно, под kill-switch, never-raise; `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / вердикт-ключи +/ схемы существующих таблиц — не трогаются (обобщает и делает durable процесс-локальный +`finalizer_liveness` ORCH-113). + +## 2. Задействованные модули / пути + +| Путь | Действие | Назначение в ORCH-114 | +|------|----------|------------------------| +| `src/stage_engine.py` | изменить | `advance_stage`: захват владения на границе side-effectful перехода/финализации; CAS на запись стадии; release в `try/finally`; проигравший — чистый аборт. Покрыть `_handle_merge_verify`, под-гейты `deploy-staging→deploy`, `run_deploy_finalizer` (Phase C), `advance_if_gate_passed` (F-1). | +| `src/db.py` | изменить | CAS-вариант записи стадии (запись только при совпадении ожидаемой текущей стадии/эпохи; rowcount-результат). Durable-хелперы владения (acquire / heartbeat-touch / release / reclaim / snapshot) — форму хранилища задаёт архитектор. | +| `src/finalizer_liveness.py` | изменить/обобщить | Отправная точка: обобщить process-local реестр до **durable, кросс-путевого** владения (или надстроить durable-слой поверх). Сохранить контракт never-raise + kill-switch. | +| `src/job_reaper.py` | изменить | Tier-2/Tier-3 осведомлены о durable-владении на **всех** релевантных путях (не только Tier-2/`deploy-staging`): defer при живом, реклейм мёртвого/устаревшего в ограниченное время (NFR-6). | +| `src/queue_worker.py` | изменить | `requeue_running_jobs` / стартовое восстановление сверяется с durable-владением: не пере-исполнять уже применённый необратимый шаг (умное восстановление). `claim_next_job` — не вносить сетевых зависимостей. | +| `src/reconciler.py` | изменить | F-1 (`advance_if_gate_passed`) — defer при активном lease перехода (по образцу skip-guard'ов escalated/Blocked/deps). | +| `src/webhooks/plane.py` | изменить | Пути продвижения (`_try_advance_stage`, Approved / Confirm Deploy) — defer при активном lease. | +| `src/webhooks/gitea.py` | изменить (учесть) | Прямые записи стадии в обход `advance_stage` (`handle_push`/`handle_ci_status`/`handle_pr`) должны попадать под тот же CAS-инвариант либо явно исключаться архитектором (граница в ADR). | +| `src/main.py` | изменить | Порядок старта демонов / точка восстановления; read-only блок в `GET /queue`; опц. operator-эндпоинт реклейма. | +| `src/config.py` | изменить | Kill-switch(и) + бюджеты/таймауты владения + (опц.) CSV-скоуп репо. | +| `tests/test_orch114_transition_ownership.py` | создать | Покрытие FR-1…FR-7 (см. `04-test-plan.yaml`). | + +## 3. Функциональные требования + +### FR-1 — Единое владение side-effectful переходом (BR-1, BR-6) +На границе, где начинается side-effectful финализация/переход (минимум: под-гейты +`deploy-staging→deploy`, `_handle_merge_verify` на `deploy→done`, Phase C `run_deploy_finalizer`), +актор **захватывает владение** задачей. Пока владение активно, другой актор не исполняет тот же +переход. Release — детерминированно в `try/finally` (в т.ч. на исключении/откате). Владение +**durable** (NFR-4): переживает рестарт и доступно для проверки другому актору/новому процессу. + +### FR-2 — Compare-and-swap / epoch на запись стадии (BR-2) +Запись стадии для side-effectful переходов выполняется только если предусловие (ожидаемая текущая +стадия и/или эпоха перехода) не изменилось с момента чтения. Реализуется через CAS-вариант +`update_task_stage` (`UPDATE … SET stage=?[, epoch=epoch+1] WHERE id=? AND stage=?[ AND epoch=?]`, +решение по форме — архитектор). Проигравший гонку писатель получает «lost-race» результат, **не** +мутирует стадию и **не** выполняет ни одного побочного эффекта (merge_pr / ratchet / rebuild / +deploy-init / enqueue следующего агента). Инвариант распространяется и на пути, пишущие стадию в +обход `advance_stage` (gitea-webhook), — либо CAS, либо явное исключение (граница в ADR). + +### FR-3 — Reaper, осведомлённый о владении на всех путях (BR-3, NFR-6) +Job-reaper перед реклеймом сверяется с durable-владением **не только** в Tier-2 для `deploy-staging` +(текущая область ORCH-113), а на всех релевантных тирах/рёбрах: **живой** владелец → **defer** +(лог + счётчик, без повторного advance); **мёртвый/устаревший** владелец → реклейм в ограниченное +время (Tier-3 backstop `reaper_max_running_s` добивает зависшего; маркер владения backstop не +обходит инвариант бюджета). Сохранить атомарный `reap_running_job` rowcount-guard. + +### FR-4 — Умное восстановление при старте (BR-4, BR-6, NFR-7) +Стартовое восстановление (`requeue_running_jobs` + последующий цикл) использует durable-владение/эпоху, +чтобы **детерминированно** различить: (a) финализация не начиналась / безопасно перезапустить → +re-drive; (b) необратимый шаг уже применён (мерж в `main` / ratchet / прод-деплой инициирован) → +**сойтись к done/консистентному исходу без повторного применения**. Источник истины для «уже +применено» — авторитетные durable-факты (SHA-in-main ORCH-071/073, маркер `INITIATED` self-deploy, +durable-lease/эпоха), а не in-memory состояние. + +### FR-5 — Skip/defer в reconciler и webhook (BR-5) +Reconciler F-1 (`advance_if_gate_passed`) и webhook-пути продвижения (`plane._try_advance_stage`, +Approved/Confirm Deploy) при **активном** lease перехода для задачи **откладывают** действие +(silent skip + наблюдаемость), по образцу существующих skip-guard'ов F-1 (escalated / Blocked / +task-deps). Fail-safe: неопределённость состояния lease → консервативный skip (не дублировать). + +### FR-6 — Наблюдаемость (BR-7) +Аддитивный read-only блок в `GET /queue` (по образцу `serial_gate`/`merge_gate`/`reaper`): +держатели lease, возраст владения, defer-счётчики, форсированные/устаревшие реклеймы. Алерт +(`send_telegram`, кликабельный номер) на форсированный/устаревший реклейм. Опц. запись в +lessons-journal (ORCH-098, `source="auto"`). Опц. operator-эндпоинт ручного реклейма (по образцу +`POST /serial-gate/unfreeze`). + +### FR-7 — Конфигурация, обратимость, never-raise (BR-9, NFR-1, NFR-2, NFR-8) +Все публичные функции владения — never-raise (ошибка → безопасный дефолт + WARNING). Kill-switch +возвращает поведение **байт-в-байт** к до-ORCH-114 (lease не пишется/не читается, CAS вырождается в +прежний безусловный `update_task_stage`). Hot-path — fail-open; prod-safety — fail-closed. + +## 4. Изменения API +- **`GET /queue`** — аддитивный read-only блок владения переходом (имя ключа уточнит архитектор, + напр. `transition_ownership` / `transition_lease`). Существующие ключи `/queue` — байт-в-байт. +- **(Опционально, по решению архитектора)** `POST /transition-lease/release?work_item=` — + операторский ручной реклейм застрявшего владения (паттерн `POST /serial-gate/unfreeze`). +- `GET /metrics` (ORCH-099) — при необходимости аддитивное поле без бампа `schema_version` (sidecar + обязан толерировать незнакомые ключи). Прочие эндпоинты — не трогаются. + +## 5. Изменения схемы БД +**Требование (форму выбирает архитектор, `06-adr/` + `08-data-requirements.md`):** для durable-владения +(NFR-4) и CAS/epoch (FR-2) требуется **аддитивное, идемпотентное** durable-состояние. Кандидаты +(не предписание): +- доп. аддитивная таблица владения (`CREATE TABLE IF NOT EXISTS`, паттерн `repo_freeze`/ + `coverage_baseline`/`lessons`) с `(task_id/job_id, owner, run_id, stage, acquired_at, heartbeat_at, + expires_at)`; **либо** +- аддитивные колонки на `tasks`/`jobs` (`_ensure_column`, паттерн `tasks.track`/`tasks.cancelled_at`), + включая возможную `epoch/version`-колонку для CAS. + +**Жёсткие ограничения (NFR-3):** только аддитивно/идемпотентно; **схемы существующих таблиц +(`tasks`/`jobs`/`agent_runs` и пр.) — байт-в-байт**; никаких изменений существующих столбцов/индексов, +ломающих обратную совместимость; restart-safe инициализация в `init_db()`. + +## 6. Требования к новым/изменённым QG checks +**Нет.** `QG_CHECKS` / `check_*` / `_parse_*` / машинные вердикт-ключи — **не трогаются**. Владение +переходом — свойство **движка переходов и фоновых акторов**, а **не** Quality Gate и **не** стадия +(аналогично тому, как merge-lease/serial-gate/finalizer-liveness — врезки/leaf'ы, а не QG). Никаких +новых стадий/рёбер в `STAGE_TRANSITIONS`. + +## 7. Совместимость / регресс +- **Kill-switch** (новый флаг в `config.py`, env `ORCH_*`): `False` → CAS вырождается в прежний + безусловный `update_task_stage`, lease не пишется/не читается, reaper/reconciler/webhook ведут себя + как до ORCH-114 — **байт-в-байт** (включая зелёный существующий `pytest tests/`). +- **Область раската:** по образцу leaf-гейтов; durable-lease минимально применяется к self-hosting + рёбрам (`deploy-staging`/`deploy→done`, где живут необратимые эффекты); generic-CAS инертен при + отсутствии гонки (нулевая стоимость на не-затронутых переходах). Точную область фиксирует архитектор. +- **Обратимость:** механизм аддитивен и изолирован; откат = выключить kill-switch (durable-таблица/ + колонки остаются инертными). +- **never-raise / fail-open / fail-closed:** hot-path claim/guard — fail-open (не клинить общую + очередь, AC-8 ORCH-088); prod-safety/необратимость — fail-closed; любой сбой механизма — WARNING + + безопасный дефолт. +- **Сквозной бюджет:** lease согласован с `reaper_max_running_s`/`reaper_finalize_grace_s` (ORCH-065/ + 109/110/113) — не удлиняет финализацию за backstop; устаревший владелец добивается в ограниченное время. +- **Маркеры трассировки (ORCH-078):** правки в блоках с маркерами ORCH-065/109/110/111/113 (reaper, + finalizer-liveness, merge-gate) сверяются с их ADR перед изменением; новый код помечается `ORCH-114`. +- **enduro-trails:** при флаге off / репо вне области — нулевая регрессия. diff --git a/docs/work-items/ORCH-114/03-acceptance-criteria.md b/docs/work-items/ORCH-114/03-acceptance-criteria.md new file mode 100644 index 0000000..64c5760 --- /dev/null +++ b/docs/work-items/ORCH-114/03-acceptance-criteria.md @@ -0,0 +1,177 @@ +--- +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 +--- + +# 03 — Критерии приёмки (Acceptance Criteria): ORCH-114 — Ownership-lease для side-effectful переходов + умное восстановление при старте + +Work Item: **ORCH-114** · Repo: **orchestrator** · Стадия: analysis + +Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что +считается провалом). Reviewer/CI проверяет их буквально по файлам и тестам репозитория. + +--- + +## AC-1 — Обязательный регресс: нет двойного эффекта при конкурентном входе в переход + +**Условие:** два актора одновременно входят в `advance_stage(deploy-staging)` для одной задачи +(живой монитор-финализатор + второй путь: reaper / reconciler F-1 / webhook). Тест-двойники для +`merge_pr` / coverage-ratchet / image-rebuild / deploy-init считают число вызовов. +- **PASS:** side-effectful шаги (merge_pr, ratchet baseline, image-rebuild, инициация прод-деплоя, + enqueue следующего агента) выполняются **ровно один раз**; персистится **ровно один** согласованный + исход стадии; второй актор получает «lost-race»/defer и **не** выполняет побочных эффектов. Тест + **красный до фикса, зелёный после** (воспроизводит класс инцидента ORCH-111). +- **FAIL:** любой side-effectful шаг вызван дважды; либо два противоречивых исхода (один откатил на + `development`, другой довёл до `done`); либо тест не воспроизводит проблему до фикса. + +--- + +## AC-2 — Compare-and-swap на запись стадии + +**Условие:** CAS-вариант записи стадии вызывается двумя писателями с одинаковым ожидаемым предусловием; +первый применяется, второй приходит со «устаревшим» ожиданием. +- **PASS:** первый writer применяет переход (rowcount=1); второй получает «lost-race» (rowcount=0), + стадия **не** мутируется повторно; при выключенном kill-switch CAS вырождается в прежний безусловный + `update_task_stage` (байт-в-байт). +- **FAIL:** второй writer перезатирает стадию; либо CAS меняет семантику записи при выключенном флаге. + +--- + +## AC-3 — Жизненный цикл владения: acquire / release / реклейм + +**Условие:** актор начинает side-effectful финализацию. +- **PASS:** владение захватывается на границе финализации и освобождается в `try/finally` — в т.ч. + при исключении и при откате (rollback); durable-запись владения видна другому актору; после release + владение реклеймится/свободно. +- **FAIL:** владение не освобождается при исключении/откате (lease «течёт» и клинит задачу); либо не + захватывается на границе. + +--- + +## AC-4 — Reaper откладывает реклейм при живом владении (все пути, не только Tier-2/deploy-staging) + +**Условие:** durable-владение активно (живой финализатор), reaper делает тик. +- **PASS:** reaper **defer** (лог + счётчик, без повторного `advance_stage`) пока владение живо и в + пределах бюджета; область defer обобщена за пределы Tier-2/`deploy-staging` ORCH-113 на релевантные + пути; атомарный `reap_running_job` rowcount-guard сохранён. +- **FAIL:** reaper повторно исполняет финализацию при живом владельце; либо defer ограничен только + прежней узкой областью, оставляя кросс-путь открытым. + +--- + +## AC-5 — Reaper добивает мёртвое/устаревшее владение в ограниченное время + +**Условие:** владелец провально мёртв/завис (lease устарел), финализация не прогрессирует. +- **PASS:** reaper реклеймит задачу в пределах Tier-3 backstop `reaper_max_running_s` (маркер владения + backstop не обходит); задача не остаётся навсегда заклиненной; сквозной инвариант + `reaper_max_running_s > Σ(deploy-staging gate-work) + grace` сохранён. +- **FAIL:** мёртвое владение блокирует задачу бессрочно; либо нарушен бюджетный инвариант ORCH-065/ + 109/110/113. + +--- + +## AC-6 — Умное восстановление при рестарте процесса + +**Условие:** процесс убит **в середине** финализации `deploy-staging`/`deploy` (in-memory состояние +потеряно); процесс перезапущен (`requeue_running_jobs` + цикл). +- **PASS:** восстановление через durable-владение/эпоху + авторитетные факты (SHA-in-main ORCH-071/073, + маркер `INITIATED`) детерминированно сходится к **единственному** согласованному исходу: незавершённое + дорешается, **уже применённый** необратимый шаг (мерж/ratchet/прод-деплой) **не** применяется повторно. +- **FAIL:** после рестарта переход исполняется заново с двойным необратимым эффектом; либо возникает + противоречие rollback↔done; либо задача застревает нетерминальной с удержанным lease. + +--- + +## AC-7 — Reconciler F-1 пропускает переход при активном lease + +**Условие:** lease перехода активен; reconciler F-1 сканирует задачу. +- **PASS:** F-1 **defer/skip** (silent, наблюдаемо), не вызывает `advance_stage`, по образцу skip-guard'ов + escalated/Blocked/task-deps; fail-safe: неопределённость lease → консервативный skip. +- **FAIL:** F-1 продвигает стадию параллельно живому владельцу. + +--- + +## AC-8 — Webhook-путь пропускает переход при активном lease + +**Условие:** lease перехода активен; приходит Plane-webhook (Approved / Confirm Deploy) на ту же задачу. +- **PASS:** webhook-путь продвижения **defer**, не дублирует переход/финализацию при живом владельце; + поздний легитимный сигнал не теряется (повторно отработает после release или станет идемпотентным no-op). +- **FAIL:** webhook повторно входит в переход параллельно владельцу и даёт двойной эффект. + +--- + +## AC-9 — Kill-switch off → поведение байт-в-байт прежнее + +**Условие:** новый kill-switch выключен. +- **PASS:** lease не пишется/не читается; CAS вырождается в прежний безусловный `update_task_stage`; + reaper/reconciler/webhook/startup ведут себя как до ORCH-114; существующий `pytest tests/` зелёный + без правок ожиданий; enduro не затронут. +- **FAIL:** при выключенном флаге наблюдается любое отличие от до-ORCH-114 поведения. + +--- + +## AC-10 — never-raise + fail-open (hot-path) / fail-closed (prod-safety) + +**Условие:** механизм владения сталкивается с ошибкой (БД-сбой/повреждённая запись lease/исключение). +- **PASS:** ни одна публичная функция владения не роняет конвейер; горячий путь claim/guard — + **fail-open** (общая очередь всех проектов не клинится); решения, критичные для необратимости/прода — + **fail-closed**; на ошибке — WARNING + безопасный дефолт. +- **FAIL:** ошибка механизма роняет claim/конвейер; либо hot-path заклинивает очередь; либо + prod-критичное решение фейлит «открыто». + +--- + +## AC-11 — Инварианты конвейера не тронуты + +**Условие:** аудит диффа против `src/stages.py`, `src/qg/checks.py`, схемы БД. +- **PASS:** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` / машинные + вердикт-ключи (`verdict:`/`result:`/`deploy_status:`/`staging_status:`/`security_status:`/ + `coverage_status:`) — **байт-в-байт**; любое новое хранилище аддитивно/идемпотентно + (`CREATE TABLE IF NOT EXISTS` / `_ensure_column`), схемы существующих таблиц неизменны. +- **FAIL:** изменены состав/порядок стадий, реестр/семантика гейтов, вердикт-ключи или существующие + столбцы/индексы. + +--- + +## AC-12 — Наблюдаемость + +**Условие:** `GET /queue` при включённом флаге. +- **PASS:** присутствует аддитивный read-only блок владения (держатели/возраст/defer-счётчики/реклеймы); + существующие ключи `/queue` не сломаны; форсированный/устаревший реклейм даёт Telegram-алерт с + кликабельным номером. +- **FAIL:** блок отсутствует/ломает существующий вывод; реклейм происходит молча без наблюдаемости. + +--- + +## AC-13 — Self-hosting безопасность + +**Условие:** аудит механизма владения на побочные действия. +- **PASS:** механизм **никогда** не рестартит прод-контейнер `orchestrator`, не пушит/force-push в + `main`, не трогает detached deploy-процесс; деплой орка по-прежнему только через staging-гейт (8501) + и `Confirm Deploy`. +- **FAIL:** механизм владения инициирует рестарт прода/мутацию `main`/вмешательство в detached-деплой. + +--- + +## Сводная матрица AC ↔ FR/BR/NFR + +| AC | Покрывает | Тип проверки | +|----|-----------|--------------| +| AC-1 | BR-1, BR-6, BR-8 / FR-1, FR-2 | integration (regression, red→green) | +| AC-2 | BR-2 / FR-2 | unit | +| AC-3 | BR-1 / FR-1 | unit | +| AC-4 | BR-3 / FR-3 | integration | +| AC-5 | BR-3, NFR-6 / FR-3 | unit/integration | +| AC-6 | BR-4, BR-6, NFR-4, NFR-7 / FR-1…FR-4 | integration (restart-recovery) | +| AC-7 | BR-5 / FR-5 | unit/integration | +| AC-8 | BR-5 / FR-5 | unit/integration | +| AC-9 | BR-9, NFR-8 / FR-7 | regression (kill-switch off) | +| AC-10 | NFR-1 / FR-7 | unit | +| AC-11 | NFR-3 / FR-6 (negative) | structural audit | +| AC-12 | BR-7 / FR-6 | unit/integration | +| AC-13 | NFR-5 / FR-1 | structural audit | diff --git a/docs/work-items/ORCH-114/04-test-plan.yaml b/docs/work-items/ORCH-114/04-test-plan.yaml new file mode 100644 index 0000000..8ffbaa1 --- /dev/null +++ b/docs/work-items/ORCH-114/04-test-plan.yaml @@ -0,0 +1,107 @@ +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 +title: "Ownership-lease для side-effectful переходов стадий + умное восстановление при старте" +framework: pytest +scope: > + Покрывается: единое владение side-effectful переходом (FR-1), CAS/epoch на запись стадии (FR-2), + осведомлённость reaper о живом/мёртвом владении на всех путях (FR-3), умное восстановление при + рестарте (FR-4), skip/defer в reconciler F-1 и webhook (FR-5), наблюдаемость (FR-6), kill-switch + и never-raise (FR-7). Вне покрытия: переход на uvicorn --workers>1, частные симптомы ORCH-110/112 + (уже закрыты и переиспользуются), изменение STAGE_TRANSITIONS/QG_CHECKS/check_*. +notes: > + TC-01 — ОБЯЗАТЕЛЬНЫЙ регресс класса инцидента ORCH-111: красный до фикса, зелёный после. + Все side-effectful вызовы (merge_pr / coverage-ratchet / image-rebuild / deploy-init) проверяются + через тест-двойники со счётчиком вызовов — без сети/реального git/прода/ssh. Restart-recovery + моделируется сбросом in-memory состояния + повторным прогоном стартового восстановления над durable + состоянием БД. Полный регресс tests/ должен оставаться зелёным; при выключенном kill-switch + поведение байт-в-байт прежнее. + +tests: + - id: TC-01 + type: integration + description: "ОБЯЗАТЕЛЬНЫЙ РЕГРЕСС. Два конкурентных входа в advance_stage(deploy-staging) одной задачи (живой финализатор + reaper/reconciler/webhook): каждый side-effectful шаг (merge_pr/ratchet/rebuild/deploy-init/enqueue) вызывается ровно один раз; персистится один согласованный исход; второй актор — lost-race/defer без побочных эффектов. Красный до фикса, зелёный после." + module: tests/test_orch114_transition_ownership.py + expected: PASS + + - id: TC-02 + type: unit + description: "CAS-запись стадии: первый writer применяет (rowcount=1), второй с устаревшим предусловием получает lost-race (rowcount=0) и не мутирует стадию (FR-2/AC-2)." + module: tests/test_orch114_transition_ownership.py + expected: PASS + + - id: TC-03 + type: unit + description: "Жизненный цикл владения: acquire на границе финализации; release в try/finally при нормальном завершении, при исключении и при откате (lease не течёт); durable-запись видна другому актору (FR-1/AC-3)." + module: tests/test_orch114_transition_ownership.py + expected: PASS + + - id: TC-04 + type: integration + description: "Reaper defer при живом владении на путях за пределами Tier-2/deploy-staging ORCH-113: повторный advance не выполняется, атомарный reap_running_job rowcount-guard сохранён, ведётся счётчик defer (FR-3/AC-4)." + module: tests/test_orch114_transition_ownership.py + expected: PASS + + - id: TC-05 + type: unit + description: "Reaper добивает мёртвое/устаревшее владение в пределах Tier-3 backstop reaper_max_running_s; маркер владения backstop не обходит; задача не клинится бессрочно (FR-3/AC-5/NFR-6)." + module: tests/test_orch114_transition_ownership.py + expected: PASS + + - id: TC-06 + type: integration + description: "Умное восстановление при рестарте: процесс убит в середине финализации, in-memory сброшен; стартовое восстановление над durable-состоянием + авторитетными фактами (SHA-in-main, INITIATED) сходится к единственному исходу без повторного необратимого эффекта (FR-4/AC-6)." + module: tests/test_orch114_transition_ownership.py + expected: PASS + + - id: TC-07 + type: integration + description: "Reconciler F-1 (advance_if_gate_passed) делает defer/skip при активном lease перехода; fail-safe: неопределённость lease → консервативный skip (FR-5/AC-7)." + module: tests/test_orch114_transition_ownership.py + expected: PASS + + - id: TC-08 + type: integration + description: "Webhook-путь (plane._try_advance_stage, Approved/Confirm Deploy) делает defer при активном lease; поздний легитимный сигнал не теряется (повтор после release / идемпотентный no-op) (FR-5/AC-8)." + module: tests/test_orch114_transition_ownership.py + expected: PASS + + - id: TC-09 + type: integration + description: "Kill-switch off: lease не пишется/не читается, CAS вырождается в прежний безусловный update_task_stage, reaper/reconciler/webhook/startup — байт-в-байт до ORCH-114; существующий pytest tests/ зелёный (FR-7/AC-9/NFR-8)." + module: tests/test_orch114_transition_ownership.py + expected: PASS + + - id: TC-10 + type: unit + description: "never-raise + fail-open/fail-closed: ошибка/повреждённая запись lease/исключение БД не роняют конвейер; hot-path claim/guard fail-open; prod-safety решение fail-closed; WARNING + безопасный дефолт (FR-7/AC-10/NFR-1)." + module: tests/test_orch114_transition_ownership.py + expected: PASS + + - id: TC-11 + type: unit + description: "Структурный аудит инвариантов: STAGE_TRANSITIONS / QG_CHECKS / имена-семантика check_* / вердикт-ключи байт-в-байт; новое хранилище аддитивно/идемпотентно (CREATE TABLE IF NOT EXISTS / _ensure_column), схемы существующих таблиц неизменны (NFR-3/AC-11)." + module: tests/test_orch114_transition_ownership.py + expected: PASS + + - id: TC-12 + type: integration + description: "Наблюдаемость: GET /queue несёт аддитивный read-only блок владения (держатели/возраст/defer/реклеймы), существующие ключи не сломаны; форсированный/устаревший реклейм даёт Telegram-алерт с кликабельным номером (FR-6/AC-12)." + module: tests/test_orch114_transition_ownership.py + expected: PASS + + - id: TC-13 + type: unit + description: "Self-hosting безопасность: механизм владения не инициирует рестарт прод-контейнера, не пушит/force-push в main, не трогает detached deploy-процесс (NFR-5/AC-13)." + module: tests/test_orch114_transition_ownership.py + expected: PASS + + - id: TC-14 + type: integration + description: "Полный регресс конвейера: pytest tests/ остаётся зелёным; deploy-staging-ребро и deploy-finalizer (Phase C) проходят при включённом механизме без двойных эффектов в одно-акторном happy-path (BR-8)." + module: tests/ + expected: PASS