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

20 KiB
Raw Blame History

work_item, stage, author_agent, status, created_at, model_used, escalate
work_item stage author_agent status created_at model_used escalate
ORCH-114 analysis analyst ready-for-review 2026-06-15 claude-opus-4-8 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 должен учитывать и их (архитектор фиксирует границу).