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

16 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-113 analysis analyst ready-for-review 2026-06-15 claude-opus-4-8 full-cycle

01 — BRD (бизнес-требования): ORCH-113 — BUG: job-reaper не должен повторно запускать финализацию deploy-staging, пока жив исходный finalizer

Work Item: ORCH-113 · Repo: orchestrator · Стадия: analysis

Багфикс-трек → эскалация в полный цикл (escalate: full-cycle). Задача помечена Bug, но сама баг-карточка явно требует «анализ контракта reaper, статуса running/finalizing, длительности grace и идемпотентности edge-гейтов» (см. «Ограничение» в бизнес-запросе) — это решение с несколькими проектными альтернативами (liveness-heartbeat finalizer'а / явный sub-state finalizing / per-stage grace / ownership-lease на edge-гейты) и нетривиальными инвариантами self-hosting, затрагивающее задокументированный сквозной инвариант ORCH-065 (контракт живости reaper, adr-0011). По правилу ORCH-019 (ADR-001 D5) выпускается полный analysis-пакет, а трек эскалируется (POST /bug-fast-track/escalate?work_item=ORCH-113) → задача проходит стадию architecture. Прецедент — родственные задачи того же инцидент-кластера: ORCH-110 / ORCH-111 («bug → escalate full-cycle»).


1. Бизнес-контекст и проблема

Оркестратор — self-hosting инструмент: его прод-контейнер обслуживает конвейер всех проектов из одного инстанса с общей БД и общей очередью и дорабатывает сам себя. Фоновый демон job-reaper (src/job_reaper.py, ORCH-065) — страховочный слой: он добивает «зомби»-job'ы, чей монитор умер, не записав терминальный статус. Его Tier-2-ветка (процесс агента завершился — agent_runs.exit_code записан, — но job всё ещё running) неоднозначна: это одновременно «монитор умер посреди финализации» И «живой монитор ещё финализирует». Reaper разрешает неоднозначность таймером — finalization grace reaper_finalize_grace_s = 300 (db.py:1345-1348, job_reaper.py:36-44): если exit_code записан дольше grace — трактует ситуацию как мёртвый монитор и сам до-водит стадию.

Корневая ошибка контракта: grace=300с построен на задокументированном допущении, что после записи finished_at монитор делает лишь «git commit/push (+PR), БАГ-8-проверку и сетевые Plane-комментарии — секунды…десятки секунд, и ТОЛЬКО ПОТОМ _try_advance_stage». Для ребра deploy-staging → deploy это неверно: _try_advance_stage (launcher._monitor_agent, строка 998) синхронно, в потоке монитора, прогоняет весь набор тяжёлых детерминированных edge-под-гейтовsecuritymerge-gate (полный локальный re-test, бюджет merge_retest_timeout_s = 900) → coverage (ещё один полный pytest --cov) → image-freshness (docker-rebuild + пересоздание staging). Это минуты, и весь этот объём идёт ПОСЛЕ штампа finished_at (launcher:861) и ДО _finalize_job (launcher:1003). finished_age_s reaper меряет именно от finished_at (db.py:1360) — то есть от начала финализации. Итог: пока живой монитор ещё внутри edge-гейтов, reaper по истечении 300с считает его мёртвым и независимо запускает тот же тяжёлый набор edge-гейтов повторно.

Установленные факты инцидента (ORCH-111, deployer job 1914, run_id 683):

  • deployer завершился exit_code=0 и записал 15-staging-log.md со staging_status: SUCCESS;
  • после выхода агента job 1914 оставалась running, т.к. монитор ещё финализировал стадию;
  • финализация deploy-staging заняла дольше 300с (полный локальный re-test + coverage);
  • reaper после reaper_finalize_grace_s=300 трактовал это как умерший monitor и повторно вызвал gate-driven advance (_reap_exit0_gate_driven_advance_try_advance_stageadvance_stage);
  • edge-гейты deploy-staging исполнились повторно: security-gate, merge-gate, rebase, полный локальный re-test;
  • один из повторных re-test стал красным: 3 failed, 1916 passed, 1 warning, 14 errors in 444.79s;
  • задача была откатана deploy-staging → development (+ ложный developer-retry), хотя параллельно исходный prod-deploy/finalizer дошёл до deploy_status: SUCCESS, PR #130 был смержен, задача перешла deploy → done.

Симптом: две ветки системы расходятся по состоянию одной задачи — одна повторно откатывает deploy-staging, другая успешно завершает deploy. Гонка + ложный rollback + ложный developer-retry + шумные алерты + несогласованное состояние Plane/БД.

Почему существующие гарды reaper не спасли: атомарный claim-before-act (reap_running_job(... WHERE status='running'), job_reaper.py:280) защищает строку job от двойного терминального флипа, но не защищает побочное исполнение edge-гейтов: reaper вызывает _gate_driven_advance → advance_stage, который и прогоняет тяжёлые под-гейты, до/независимо от монитора. Гонка — в side-effectful исполнении edge-гейтов, а не в флипе строки. Дешёвая read-only пред-проверка _gate_is_green('deploy-staging') читает лишь check_staging_status (frontmatter 15-staging-log.md = SUCCESS, зелёный) → reaper уверенно идёт в тяжёлый advance. Tier-3 backstop (reaper_max_running_s = 5400) при этом не срабатывает — баг чисто в Tier-2 grace.

2. Объём (scope)

В объёме

  • Reaper не должен повторно исполнять тяжёлую финализацию deploy-staging/merge-gate (security / merge-gate / локальный re-test / coverage / image-freshness), пока исходный monitor/finalizer ещё жив или пока edge-гейты для этого job/stage уже исполняются.
  • Повторная обработка завершившегося-но-ещё-running job на deploy-staging должна быть идемпотентной: без второго локального re-test/merge-gate для того же job/stage без строгого владения состоянием.
  • Согласование Tier-2 grace (reaper_finalize_grace_s) с фактической wall-clock-длительностью финализации deploy-staging ИЛИ замена таймерного критерия живости на сигнал, переживающий «долгую, но живую» финализацию.
  • Сохранение основной функции reaper (ORCH-065): реально мёртвый finalizer на deploy-staging по-прежнему добивается за ограниченное время.

Вне объёма

  • Изменение STAGE_TRANSITIONS / QG_CHECKS / семантики любого check_* / machine-verdict ключей / схемы существующих таблиц (правки — только аддитивные).
  • Инфра-толерантность merge-gate к таймауту re-test и tree-kill осиротевших pytest-процессов — это ORCH-110 (союзная задача того же инцидента; не дублировать).
  • Починка конкретных «мигающих» тестов, давших 3 failed … 14 errors.
  • Полный редизайн reaper или модели финализации монитора.
  • Выбор механизма решения (heartbeat / sub-state finalizing / per-stage grace / ownership-lease) — это архитектурное решение (06-adr), не зона аналитика.

3. Заинтересованные стороны

  • Owner / Слава — заказчик исправления, держатель инвариантов self-hosting.
  • Конвейер всех проектов (orchestrator self-hosting + enduro-trails) — общий инстанс/БД/очередь: ложный rollback и гонка состояния касаются стабильности платформы в целом.
  • Операторы — получатели алертов; именно их будят ложные «merge-gate FAILED / rolled back».
  • Архитектор — принимает решение по механизму владения/живости (06-adr) после эскалации.

4. Бизнес-требования (BR)

  • BR-1 — Reaper не должен запускать второй прогон edge-гейтов ребра deploy-staging → deploy (security / merge-gate / re-test / coverage / image-freshness) для job, чей исходный monitor/finalizer ещё жив.
  • BR-2 — Повторная обработка завершившегося-но-running job на deploy-staging идемпотентна: не более одного локального re-test/merge-gate на пару (job, stage) без строгого владения состоянием; второй актор, не владеющий состоянием, не исполняет побочных шагов.
  • BR-3 — Критерий живости Tier-2 должен учитывать реальную wall-clock-длительность финализации deploy-staging (включающую полный набор edge-гейтов), ИЛИ живость должна определяться сигналом, который переживает долгую-но-живую финализацию (не одним finished_age_s).
  • BR-4 — Реально мёртвый монитор (краш посреди финализации deploy-staging) по-прежнему должен добиваться reaper'ом за ограниченное время — основная функция ORCH-065 сохраняется; фикс не превращает reaper в no-op для deploy-staging.
  • BR-5 — После согласования у задачи — единственное консистентное состояние: никакого ложного отката deploy-staging → development и никакого ложного developer-retry после фактически успешного deploy; ветки системы сходятся, не расходятся.

5. Нефункциональные требования (NFR)

  • NFR-1 — Контракт reaper сохранён: never-raise на единицу работы, kill-switch, fail-safe; reaper остаётся наблюдателем-страховкой, не Quality Gate'ом.
  • NFR-2STAGE_TRANSITIONS / QG_CHECKS / каждый check_* / machine-verdict ключи / схема существующих таблиц — байт-в-байт; любые БД-правки — только аддитивные (_ensure_column / CREATE TABLE IF NOT EXISTS).
  • NFR-3 — Self-hosting-безопасно: фикс никогда не рестартит/не роняет прод-контейнер и никогда не пушит/force-push'ит main.
  • NFR-4 — Обратная совместимость и обратимость: поведение reaper для не-deploy-staging стадий и путь добивания мёртвого монитора сохранены; выключенный kill-switch → строго прежнее поведение; раскат обратим.
  • NFR-5 — Restart-safe: in-memory состояние reaper сбрасывается при рестарте (это покрыто стартовым requeue_running_jobs); любой новый маркер владения/живости должен быть либо durable, либо безопасно восстановимым после рестарта.
  • NFR-6 — Сквозной инвариант ORCH-065/109/110 сохранён: reaper_max_running_s (5400) > Σ(deploy-staging gate-work) + grace (Tier-3 backstop). Любая правка grace/таймаутов не должна его нарушить.

6. Допущения и ограничения

  • Задача помечена Bug; ввиду архитектурной природы — эскалация в полный цикл (нужен ADR + анализ тех-рисков архитектором: 06-adr / 07 / 08 / 10).
  • Инстанс общий для всех проектов (общая БД/очередь) — фикс не должен вносить регрессию для enduro-trails и не-self репо.
  • Выбор конкретного механизма владения/живости — за архитектором; настоящий BRD фиксирует требования и инварианты, а не реализацию.
  • Источник истины о «жив ли finalizer» сегодня отсутствует: pid агента в Tier-2 уже мёртв в обоих случаях (proc.wait() вернулся), а живости потока-монитора/финализатора система не наблюдает — это и есть пробел, который закрывает фикс.

7. Критерии успеха

Reaper при живом finalizer'е deploy-staging не запускает второй прогон edge-гейтов и не откатывает задачу; повторная обработка идемпотентна; мёртвый finalizer по-прежнему добивается; после фикса нет ложного rollback/developer-retry и расхождения состояния; инварианты ORCH-065/NFR-2 целы; полный регресс tests/ зелёный. Детальные PASS/FAIL — 03-acceptance-criteria.md.

8. Риски

  • Гонка/расхождение состояния (наблюдалось): повторный откат после успешного deploy. Высокий.
  • Над-толерантность: слишком «доверять живости» → реально мёртвый finalizer не добивается (регресс ORCH-065). Сдерживается BR-4 + Tier-3 backstop.
  • Нарушение сквозного бюджета при правке grace/таймаутов (NFR-6). Детальная проработка и контрмеры — 10-tech-risks.md (заполняет архитектор).