168 lines
16 KiB
Markdown
168 lines
16 KiB
Markdown
---
|
||
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` (заполняет архитектор).
|