---
work_item: ORCH-110
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---

# adr-0042: Merge-gate re-test — толерантность к инфра-таймауту + tree-kill спавненных процессов + контракт re-test

- **Статус:** proposed
- **Дата:** 2026-06-15
- **Задача:** ORCH-110 (bug → escalate full-cycle)
- **Детальный ADR:** `docs/work-items/ORCH-110/06-adr/ADR-001-merge-gate-retest-infra-tolerance-and-tree-kill.md`
- **Парные/смежные ADR:** `adr-0006` (merge-gate ORCH-043), `adr-0040` (timeout-бюджеты ORCH-109),
  `adr-0029` (coverage-gate ORCH-027), `adr-0011` (reaper/lease ORCH-065),
  `adr-0041` (ORCH-111 `proc_blocking` — комплементарный наблюдатель)

## Контекст

Merge-gate (ORCH-043) на ребре `deploy-staging → deploy` локально пере-прогоняет тест-сюит
(`retest_branch`) для защиты от семантического конфликта слияния. Инцидент ORCH-109/PR#129: при
зелёном tester `PASS` (1899 passed / 516.7s), зелёном CI и актуальной ветке re-test упал по
**таймауту** (600s) из-за CPU-голодания от **осиротевших** pytest-процессов, переживших > 2 суток.
Таймаут классифицировался как код-фейл → откат `deploy-staging → development` + 3 сожжённых
developer-retry → manual-gate. Корни: (1) `subprocess.run(timeout=)` убивает только прямого потомка —
внуки pytest репарентируются на PID 1 и живут (в `merge_gate.retest_branch` и
`coverage_gate.measure_coverage`); (2) нет толерантности к инфра-таймауту; (3) тонкий бюджет (≈16%);
(4) избыточный re-test на уже актуальной ветке (`premerge_rebase_always=True` форсит rebase+retest
даже на no-op rebase).

Решение кросс-каттинговое: затрагивает merge-gate, coverage-gate и сквозной инвариант времени
reaper/lease — поэтому регистрируется глобально.

## Решение (сводка)

Аддитивно, под kill-switch, never-raise, скоуп self-hosting; исходная защита merge-gate от
семантического конфликта сохранена (красный re-test по-прежнему откатывает).

- **D1 — tree-kill.** Новый leaf `src/proc_group.py::run_in_process_group` спавнит
  оркестратор-порождённые pytest-прогоны в отдельной группе процессов (`start_new_session`) и при
  таймауте убивает **всё дерево** (`os.killpg`, каскад SIGTERM→grace→SIGKILL, зеркало
  `launcher.stop_process`). Используют `retest_branch` и `measure_coverage`; контракты возврата 1:1,
  меняется лишь побочный эффект (нет сирот). Fallback на прежний `subprocess.run` при kill-switch off
  / не-POSIX. Kill-switch `subprocess_tree_kill_enabled`.
- **D2 — классификация.** Чистый `merge_gate.classify_retest_failure(reason) → timeout|red|lock-busy|
  other`; `check_branch_mergeable` не меняет имя/семантику/PASS-FAIL (реестр `QG_CHECKS` цел).
- **D3 — маршрутизация.** Инфра-таймаут → `_handle_merge_gate_infra_retry` (ограниченный повтор/defer
  по образцу `_handle_merge_gate_defer`, **без** отката на `development`, **без** расхода
  developer-retry); исчерпание → отдельный **инфра-alert** (не «developer must fix»). Красный re-test
  → прежний `_handle_merge_gate_rollback`. Kill-switch `merge_retest_infra_tolerance_enabled`,
  бюджеты `merge_retest_infra_max_retries`/`merge_retest_infra_retry_delay_s`.
- **D4 — контракт re-test.** Локальный re-test исполняется ⇔ rebase реально сдвинул HEAD (`main`
  уехал); доказанный no-op rebase пропускает re-test (как уже делает путь
  `premerge_rebase_always=False` для не-behind ветки), offline, без сетевого CI-запроса. Fail-safe: на
  любой неопределённости re-test бежит. Kill-switch `merge_retest_skip_when_current_enabled`.
- **D5 — бюджет.** `merge_retest_timeout_s` 600 → 900 (запас 74%) + валидация (непозитив → дефолт +
  WARNING). Сквозной инвариант `reaper_max_running_s (5400) > Σ(deploy-staging gate-work ≈4460)+grace`
  проверен — `reaper_max_running_s` **не меняется**.
- **D6 — наблюдаемость.** Счётчики `merge_gate` + блок `merge_gate` в `GET /queue`; координация с
  ORCH-111 без дубля (ORCH-110 предотвращает/толерирует у источника, ORCH-111 наблюдает).

## Инварианты (неприкосновенны)

- `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика `check_*` / machine-verdict ключи / схема БД —
  **байт-в-байт** (под-гейт — врезка в `advance_stage`, не новая стадия/QG; новых таблиц/колонок нет).
- INV-4: никогда push/force-push `main`, merge только через Gitea PR API; прод-контейнер не
  рестартится; detached-деплой не трогается.
- never-raise во всех новых функциях/врезках; исключение не уходит в `advance_stage`/монитор.
- Kill-switch + нулевая регрессия: каждый флаг off → байт-в-байт до-ORCH-110; enduro (non-self) — no-op.

## Последствия

- **+** Устранён ложный откат/manual-gate при инфра-таймауте; устранена утечка CPU от сирот;
  re-test не избыточен на актуальной ветке.
- **−** До ~34 мин на инфра-ретраи перед alert (вместо мгновенного ложного отката); +5 конфиг-ключей.
- **Откат:** вернуть 4 kill-switch и `merge_retest_timeout_s=600`.

## Ссылки
- Детально: `docs/work-items/ORCH-110/06-adr/ADR-001-merge-gate-retest-infra-tolerance-and-tree-kill.md`
- Код: `src/merge_gate.py`, `src/coverage_gate.py`, `src/qg/checks.py`, `src/stage_engine.py`,
  `src/config.py`, `src/agents/launcher.py`, `src/job_reaper.py`, новый `src/proc_group.py`
</content>
