--- work_item: ORCH-113 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-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-под-гейтов** — `security` → `merge-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_stage` → `advance_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-2** — `STAGE_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` (заполняет архитектор).