20 KiB
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 должен учитывать и их (архитектор фиксирует границу).