179 lines
18 KiB
Markdown
179 lines
18 KiB
Markdown
---
|
||
work_item: ORCH-110
|
||
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-110 — BUG: merge-gate local re-test timeout causes false rollback after green CI
|
||
|
||
Work Item: **ORCH-110** · Repo: **orchestrator** · Стадия: analysis
|
||
|
||
> **Багфикс-трек → эскалация в полный цикл (`escalate: full-cycle`).** Задача помечена `Bug`, но
|
||
> сама баг-карточка требует «отдельный анализ вариантов и контрактов merge-gate» (см. «Ограничение»
|
||
> ниже) — это решение с несколькими проектными альтернативами и нетривиальными инвариантами
|
||
> self-hosting, которому нужен ADR. По правилу ORCH-019 (ADR-001 D5) выпускается **полный**
|
||
> analysis-пакет, а трек эскалируется (`POST /bug-fast-track/escalate?work_item=ORCH-110`) → задача
|
||
> проходит стадию `architecture`. Прецедент — родственная задача ORCH-111 («bug → escalate
|
||
> full-cycle»).
|
||
|
||
---
|
||
|
||
## 1. Бизнес-контекст и проблема
|
||
|
||
Оркестратор — self-hosting инструмент: его прод-контейнер обслуживает конвейер всех проектов и
|
||
дорабатывает сам себя. На ребре `deploy-staging → deploy` исполняется детерминированный под-гейт
|
||
**merge-gate** (`check_branch_mergeable`, ORCH-043): он догоняет ветку до текущего `origin/main`
|
||
(`auto_rebase_onto_main`) и затем **локально пере-прогоняет весь тест-сюит** (`retest_branch` →
|
||
`python -m pytest tests/ -q`) в worktree, чтобы поймать **семантический** конфликт слияния (ветка
|
||
зелёная по своей базе, но ломает уехавший `main`).
|
||
|
||
**Установленные факты инцидента (ORCH-109, PR #129):**
|
||
- tester завершился `result: PASS`; полный регресс — **`1899 passed` за `516.70s`**;
|
||
- CI Gitea по HEAD — зелёный (push + pull_request success); PR после rebase — open, `mergeable=true`;
|
||
- merge-gate локальный re-test упал по **таймауту**: `re-test timeout after 600s`
|
||
(`merge_retest_timeout_s = 600`);
|
||
- на хосте обнаружены **старые зависшие pytest-процессы** `tests/test_install_lite_script.py`,
|
||
жившие **> 2 суток** и грузившие CPU; прибиты вручную 2026-06-14.
|
||
|
||
**Цепочка отказа.** Зависшие осиротевшие pytest-процессы (CPU-голодание) → тот же сюит, что у tester
|
||
шёл 516.70s (запас до 600s ≈ 16%), под нагрузкой превысил 600s → `check_branch_mergeable` вернул
|
||
`(False, "re-test timeout after 600s")` → `_handle_merge_gate_rollback`: откат `deploy-staging →
|
||
development` + developer-retry. Каждый из 3 retry повторно падал по тому же CPU-голоданию → финальный
|
||
alert **«Merge-gate still failing after 3 developer retries (re-test timeout after 600s)»** → задача
|
||
застряла, потребовалось ручное вмешательство.
|
||
|
||
**Корень (подтверждён по коду):**
|
||
1. **Утечка осиротевших процессов.** `merge_gate.retest_branch` и `coverage_gate.measure_coverage`
|
||
запускают `subprocess.run([... pytest ...], timeout=...)` **без изоляции группы процессов**
|
||
(`start_new_session`/`preexec_fn`). При `TimeoutExpired` Python убивает только **прямого
|
||
потомка**; внуки pytest репарентируются на PID 1 (tini жнёт зомби, но не убивает живых сирот) и
|
||
живут сутками, грузя CPU. Это источник CPU-голодания (ровно симптом из фактов).
|
||
2. **Нет толерантности к инфра-таймауту.** Re-test **таймаут** (ресурсная/инфра-причина)
|
||
классифицируется идентично **красному** re-test (реальный дефект кода): оба → откат на
|
||
`development` + расход developer-retry. Разработчик не может «починить» CPU-голодание → retry
|
||
сгорают вхолостую и упираются в alert «Manual intervention needed».
|
||
3. **Тонкий бюджет.** Бюджет re-test `600s` практически равен фактическому времени сюита
|
||
(`516.70s`); запас не растёт вместе с сюитом (ср. ORCH-109, где по той же причине были подняты
|
||
бюджеты агентов developer/reviewer).
|
||
4. **Контракт необходимости re-test.** На ветке, уже актуальной к `origin/main` (rebase — no-op), и
|
||
с зелёным CI по этому же HEAD локальный полный re-test пере-проверяет ровно тот коммит, что CI
|
||
уже подтвердил, — становясь избыточной единственной точкой ложного отказа.
|
||
|
||
## 2. Объём (scope)
|
||
|
||
### В объёме
|
||
- Поведение merge-gate при **таймауте** локального re-test: классификация и путь восстановления
|
||
(толерантность к инфра-таймауту вместо ложного отката на `development`).
|
||
- **Жизненный цикл подпроцессов**, которые оркестратор запускает САМ для проверок: re-test merge-gate
|
||
(`merge_gate.retest_branch`) и coverage-run (`coverage_gate.measure_coverage`) — гарантия отсутствия
|
||
осиротевших процессов после таймаута/kill.
|
||
- **Согласованность бюджета** re-test с фактическим временем полного сюита (адекватный запас) с учётом
|
||
сквозных инвариантов reaper/lease.
|
||
- **Контракт необходимости** локального re-test merge-gate (когда он реально нужен относительно
|
||
зелёного CI и состояния `branch vs origin/main`) — анализ вариантов под решение архитектора.
|
||
- Наблюдаемость инфра-таймаута (отличить «инфра-таймаут, повтор/defer» от «дефект кода → developer»).
|
||
|
||
### Вне объёма
|
||
- **Алерт sidecar-watchdog на осиротевший тест-процесс** — это **ORCH-111** (`proc_blocking`,
|
||
наблюдатель только сигналит, никогда не убивает, C-1). ORCH-110 — комплементарная сторона
|
||
(предотвращение утечки + толерантность), дубля детекции не вводит.
|
||
- Ручное умерщвление уже зависших хост-процессов — операционная мера (выполнена 2026-06-14), не код.
|
||
- Любые правки `STAGE_TRANSITIONS` / реестра `QG_CHECKS` / `check_*`-семантики / machine-verdict
|
||
ключей / схемы БД (инвариант NFR-1).
|
||
- Изменение конкретного теста `tests/test_install_lite_script.py` (его поведение — отдельный предмет;
|
||
здесь важен класс «оркестратор-спавненный pytest не должен переживать свой бюджет»).
|
||
- Поведение не-self-hosting репозиториев (enduro-trails) — нулевая регрессия.
|
||
- Изменение хука прод-деплоя/рестарт прод-контейнера (self-hosting безопасность).
|
||
|
||
## 3. Заинтересованные стороны
|
||
- **Owner / оператор self-hosting** — страдает от ручного разбора застрявших задач и зависших
|
||
процессов; заказчик исправления.
|
||
- **Конвейер всех проектов** — общий прод-контейнер: утечка CPU деградирует обслуживание enduro.
|
||
- **Пакетный автономный режим (эпик ORCH-088)** — ложные откаты и manual-gate'ы ломают цель
|
||
«10–20 задач за ночь без вмешательства».
|
||
- **Принимает результат:** reviewer → tester → deployer штатного конвейера.
|
||
|
||
## 4. Бизнес-требования (BR)
|
||
|
||
- **BR-1 — Зелёный путь без ручного вмешательства.** При зелёном tester `PASS` и зелёном CI задача
|
||
**не должна** требовать ручного вмешательства из-за инфраструктурного/локального re-test таймаута
|
||
(прямое «Ожидаемое поведение» баг-карточки).
|
||
- **BR-2 — Инфра-таймаут ≠ дефект кода.** Таймаут локального re-test merge-gate (ресурсная/инфра
|
||
причина) **не должен** трактоваться как код-фейл: путь восстановления **не** должен сжигать
|
||
developer-retry и приводить к «Manual intervention after N developer retries», если CI и tester
|
||
были зелёными. Реакция на таймаут — ограниченный повтор/defer и/или отдельный инфра-сигнал, не
|
||
безусловный откат на `development`.
|
||
- **BR-3 — Нет осиротевших процессов.** Подпроцессы pytest, запущенные самим оркестратором для
|
||
re-test и coverage-run, **должны** полностью завершаться (всё дерево, включая внуков) при
|
||
таймауте/kill. Ни один оркестратор-спавненный pytest не должен переживать свой бюджет и продолжать
|
||
грузить CPU.
|
||
- **BR-4 — Адекватный бюджет re-test.** Бюджет времени re-test **должен** иметь достаточный запас над
|
||
фактическим временем полного сюита, чтобы здоровый сюит при штатной нагрузке не падал по таймауту;
|
||
бюджет конфигурируем и со-эволюционирует с ростом сюита.
|
||
- **BR-5 — Контракт необходимости re-test.** Merge-gate **должен** различать «ветка реально отстала
|
||
от уехавшего `origin/main` и была ребейзнута» (риск семантического конфликта → re-test оправдан) и
|
||
«ветка уже актуальна / rebase — no-op, CI по этому HEAD зелёный» (re-test избыточен). Локальный
|
||
re-test не должен быть избыточной единственной точкой ложного отказа на коммите, уже подтверждённом
|
||
CI. Конкретный контракт (skip/scope/trust-CI-SHA) выбирает архитектор и фиксирует в ADR.
|
||
- **BR-6 — Сохранение защиты от семантического конфликта.** Толерантность к таймауту **не должна**
|
||
ослаблять исходную цель merge-gate (ORCH-043): **детерминированно красный** re-test (реальный сбой
|
||
теста, а не таймаут) по-прежнему обязан откатывать на `development`. Послабление применяется ТОЛЬКО
|
||
к таймауту/инфра, никогда к красному результату.
|
||
- **BR-7 — Наблюдаемость.** Состояние «инфра-таймаут» должно быть видимым (лог + Telegram с
|
||
кликабельным номером + read-only в `GET /queue`) и отличимым от код-фейл-отката; согласовано с
|
||
сигналом ORCH-111 (без дубля).
|
||
|
||
## 5. Нефункциональные требования (NFR)
|
||
|
||
- **NFR-1 — Инварианты конвейера неприкосновенны.** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` /
|
||
семантика `check_*` / machine-verdict ключи (`verdict:`/`result:`/`deploy_status:`/
|
||
`staging_status:`/`security_status:`/`coverage_status:`) / схема БД — **байт-в-байт** прежние.
|
||
Исправление — аддитивное (врезка/leaf-логика), не новая стадия и не новый зарегистрированный QG.
|
||
- **NFR-2 — Kill-switch + нулевая регрессия.** Новое поведение под флагом; при выключенном флаге —
|
||
поведение **байт-в-байт** как до ORCH-110 (таймаут → прежний откат). Скоуп — self-hosting
|
||
(`orchestrator`); enduro не затронут.
|
||
- **NFR-3 — Self-hosting безопасность.** Исправление **никогда** не пушит/force-push в `main` (INV-4;
|
||
merge только через Gitea PR-merge API), не рестартит прод-контейнер, не трогает detached-деплой.
|
||
- **NFR-4 — never-raise.** Любая ошибка в новом пути → безопасный дефолт + WARNING; исключение
|
||
никогда не уходит в `advance_stage`/monitor-поток (контракт merge-gate сохранён).
|
||
- **NFR-5 — Ограниченность (anti-loop).** Любой повтор/defer таймаута строго ограничен по числу
|
||
попыток и суммарному времени; исчерпание → **чёткий инфра-alert**, отличный от «developer must
|
||
fix», а не бесконечный bounce и не молчаливое зависание.
|
||
- **NFR-6 — Сквозные инварианты времени.** Любое изменение бюджета re-test должно уважать
|
||
существующие соотношения: `merge_lock_timeout_s` (TTL merge-lease), `reaper_max_running_s`
|
||
(Tier-3 backstop reaper, ORCH-065/109), `coverage_run_timeout_s` — без рассинхрона.
|
||
|
||
## 6. Допущения и ограничения
|
||
- **Ограничение из баг-карточки (дословно):** «Решение намеренно не описано в этой баге; нужен
|
||
отдельный анализ вариантов и контрактов merge-gate». → Аналитик фиксирует требования и
|
||
тест-план; **варианты и контракт merge-gate** прорабатывает архитектор (06-adr) — основание
|
||
эскалации в полный цикл.
|
||
- Допущение: tini (PID 1) жнёт зомби, но не убивает живых осиротевших процессов (подтверждено
|
||
поведением инцидента) — отсюда требование tree-kill (BR-3).
|
||
- Допущение: таймаут merge-gate re-test в зелёном инциденте вызван внешним CPU-голоданием, а не
|
||
реальным зависанием теста ветки; но решение обязано остаться **fail-safe** к случаю реального
|
||
зависшего теста (см. Риск R-2 / BR-6).
|
||
- Среда верификации — staging-контур (8501), обязательная страховка перед прод-деплоем self.
|
||
|
||
## 7. Критерии успеха
|
||
Резюме: зелёный tester `PASS` + зелёный CI + актуальная ветка → задача доходит до `deploy` без
|
||
ложного отката на `development` и без manual-gate из-за инфра-таймаута; оркестратор-спавненные
|
||
pytest-процессы не переживают свой бюджет; реальный красный re-test по-прежнему откатывает на
|
||
`development`; инварианты конвейера и self-hosting не тронуты. Детальные PASS/FAIL —
|
||
`03-acceptance-criteria.md`.
|
||
|
||
## 8. Риски
|
||
- **R-1** — Над-толерантность маскирует реальный зависший тест (бесконечный/долгий) как «инфра» →
|
||
смягчение: строгая ограниченность (NFR-5) + отдельный инфра-alert + сохранение красно-откат-пути
|
||
(BR-6).
|
||
- **R-2** — Поднятие бюджета без правки tree-kill лишь отодвигает отказ (сюит растёт) → исправление
|
||
должно бить корень (BR-3), бюджет (BR-4) — вторично.
|
||
- **R-3** — Рассинхрон сквозных таймаутов (reaper/lease) при изменении бюджета (NFR-6).
|
||
- **R-4** — Дубль/конфликт с сигналом ORCH-111 (`proc_blocking`) → координация: ORCH-110
|
||
предотвращает/толерирует, ORCH-111 наблюдает; разные слои.
|
||
- Детальная оценка и митигации — `10-tech-risks.md` (заполняет архитектор).
|