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