architect(ET): auto-commit from architect run_id=687
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user