diff --git a/docs/work-items/ORCH-065/01-brd.md b/docs/work-items/ORCH-065/01-brd.md new file mode 100644 index 0000000..01de429 --- /dev/null +++ b/docs/work-items/ORCH-065/01-brd.md @@ -0,0 +1,103 @@ +# BRD — ORCH-065: zombie jobs + залипший merge-lease + +Work Item ID: ORCH-065 +Тип: BUG (P0) +Репозиторий: orchestrator (self-hosting) +Эпик: блокер ORCH-54 (полностью автономный self-deploy) + +## 1. Контекст и проблема + +Оркестратор — единый инстанс с **общей БД и общей очередью** (`jobs`, +`max_concurrency=1` для self-hosting), обслуживающий несколько проектов. Финальная +автономность self-deploy упирается в два связанных класса отказов, оба сводящиеся +к «процесс умер/завершился, а состояние осталось захваченным навсегда»: + +### Проблема A — zombie jobs (строка `jobs` навсегда `running`) +Агент (deployer/developer/reviewer) завершается **или умирает** (краш, OOM, +рестарт контейнера в ходе self-deploy, гибель monitor-потока), но строка в таблице +`jobs` остаётся в статусе `running`. Финализация статуса job выполняется **только** +в `_monitor_agent` → `_finalize_job` внутри того же процесса; если этот поток/процесс +не доживает до финализации — job «зомбирован». + +- Единственная имеющаяся защита — `requeue_running_jobs()` в `main.lifespan`, + срабатывающая **исключительно на старте процесса**. Зомби, возникший **без** + рестарта (умер дочерний процесс/monitor-поток, а сервис жив), не реанимируется + никогда. +- При `max_concurrency=1` одна зомби-строка `running` блокирует claim всех + последующих job (`count_running_jobs() >= max_concurrency` → claim не происходит) + → **встаёт конвейер всех проектов**. + +### Проблема B — залипший merge-lease +Merge-gate (ORCH-043) берёт файловый lease `/.merge-lease-.json` +ПЕРЕД rebase+re-test и держит его до фактического merge PR в `main`. Если процесс +умирает на финальном merge **с зажатым lease**: + +- Реклейм lease реализован **лениво и только по возрасту** (`age >= + merge_lock_timeout_s`) и **только в момент `acquire_merge_lease` другой задачей**. + Проактивного освобождения (на старте / периодически) нет; **liveness держателя по + pid не проверяется** (хотя `pid` в lease пишется). +- Пострадавшая задача сама re-drive не получает: merge не финализируется → задача + висит, lease мешает чужим merge до истечения TTL. + +### Проблема C — неидемпотентная финализация merge +Если rebase+re-test прошли зелёно (ветка догнана и проверена), но процесс умер до +завершения слияния PR — повторного «докатывания» merge нет. Задача застревает в +полу-выполненном состоянии, хотя вся дорогая работа (rebase+re-test) уже сделана. + +## 2. Бизнес-последствия +- **Это ПОСЛЕДНЯЯ ручная точка автономного деплоя.** Без фикса ни одна self-hosting + задача не доезжает до прода без оператора (cancel zombie + ручной merge PR + + ручной `--deploy`). +- Прямой блокер эпика ORCH-54. +- Доказанные инциденты (07.06): ORCH-58/60/61/21 — каждый раз после успешного + deployer-прохода job оставался `running`; jobs **236/239/242/254** — зомби, + прод-merge/deploy доводились вручную. +- Групповой риск: зомби в общей очереди при concurrency=1 останавливает конвейер + enduro-trails и всех прочих проектов. + +## 3. Цель +Сделать так, чтобы **смерть процесса/потока на любой стадии (включая self-restart +во время deploy) НЕ оставляла навсегда захваченных ресурсов** — ни строки `jobs` в +`running`, ни merge-lease. Конвейер должен самовосстанавливаться без оператора, при +этом сохраняя все инварианты self-hosting (не ронять прод-контейнер, не трогать +`main`, fail-closed на реальных ошибках). + +## 4. Объём (Scope) + +### В объёме +1. **Job-reaper** — фоновый watchdog (паттерн `reconciler`/`queue_worker`), + детектирующий «мёртвый» `running`-job и приводящий его строку в корректный + терминальный/повторный статус (`done`/`failed`/`queued`) детерминированно, + без LLM. Restart-safe и работающий **без** рестарта процесса. +2. **Проактивный реклейм stale merge-lease** — освобождение lease, чей держатель + мёртв (pid не жив) ИЛИ возраст превысил TTL — на старте и периодически (reaper/ + reconciler), а не только лениво при чужом `acquire`. +3. **Идемпотентная финализация merge** — если rebase+re-test зелёные, но merge не + состоялся, операция повторяется/докатывается без потери уже сделанной работы. + +### Вне объёма +- Переход на внешний брокер очередей / смену схемы блокировок merge на БД-lock. +- Полный авто-approve деплоя (ORCH-54) — отдельная задача; здесь только снятие + технического блокера. +- Изменение конвейера стадий (`STAGE_TRANSITIONS`) и реестра гейтов как контрактов. + +## 5. Заинтересованные стороны +- Owner оркестратора (self-hosting автономность). +- Все проекты на общем инстансе (enduro-trails и пр.) — страдают от блокировки + общей очереди. + +## 6. Допущения и ограничения +- `max_concurrency=1` для self-hosting сохраняется. +- Self-hosting safety (CLAUDE.md): нельзя ронять/рестартить прод-контейнер в рамках + задачи; нельзя пушить/форс-пушить `main`; реклейм lease не должен прерывать + легитимно работающий merge. +- Никаких ложных реанимаций: живой, но долгий job не должен помечаться зомби + (нужен порог/грейс «N тиков» + проверка реальной смерти, а не просто долготы). +- Контракт **never-raise** для всей новой фоновой логики (как у reconciler/merge_gate). +- Kill-switch на каждый новый механизм (как `reconcile_enabled` / `merge_gate_enabled`). + +## 7. Критерий успеха (бизнес-уровень) +После фикса воспроизводимый сценарий «успешный deployer-проход + смерть процесса/ +self-restart» НЕ оставляет зомби-job и зажатого lease: задача либо корректно +доезжает до `done` сама, либо откатывается по штатному контракту — **без участия +оператора**. Регресс-тест на jobs-зомби и stale-lease зелёный. diff --git a/docs/work-items/ORCH-065/02-trz.md b/docs/work-items/ORCH-065/02-trz.md new file mode 100644 index 0000000..8909924 --- /dev/null +++ b/docs/work-items/ORCH-065/02-trz.md @@ -0,0 +1,170 @@ +# ТЗ — ORCH-065: job-reaper + stale merge-lease reclaim + идемпотентный merge + +Work Item ID: ORCH-065 +Базируется на: 01-brd.md +Примечание архитектору: ТЗ фиксирует ТРЕБОВАНИЯ и кандидатные модули. Выбор +конкретной реализации (новый модуль vs расширение reconciler; колонка `jobs.pid` +vs эвристика на `agent_runs`) — за стадией architecture (ADR). Если какая-либо +часть ТЗ окажется нереализуемой/спорной — вернуть в Анализ, не комментировать +задним числом. + +## 0. Текущее состояние (факты из кода) + +| Место | Факт | +|-------|------| +| `src/queue_worker.py` `_drain_once` | claim не происходит, пока `count_running_jobs() >= max_concurrency`. Одна зомби-строка `running` при concurrency=1 блокирует всю очередь. | +| `src/agents/launcher.py` `_monitor_agent` → `_finalize_job` | статус job (`done`/`queued`/`failed`) выставляется ТОЛЬКО в этом monitor-потоке. Смерть потока/процесса до финализации ⇒ job навсегда `running`. | +| `src/main.py` (lifespan, строки ~55-61) | `requeue_running_jobs()` вызывается ТОЛЬКО при старте процесса. | +| `src/db.py` `requeue_running_jobs` | flip всех `running`→`queued`. Без рестарта не запускается. | +| `src/db.py` таблица `jobs` | колонки `pid`/`heartbeat` НЕТ; есть `run_id`, `started_at`, `attempts`, `max_attempts`. | +| `src/merge_gate.py` `acquire_merge_lease` | реклейм stale lease (age `>= merge_lock_timeout_s`) и corrupt — ТОЛЬКО лениво в момент `acquire`. Lease пишет `pid`, но liveness по pid нигде не проверяется. | +| `src/merge_gate.py` `release_merge_lease` | holder-aware (по `branch`), идемпотентен. Вызовы: `webhooks/gitea.py:380` (PR-merged), `stage_engine.py:352/740/876/952`, `qg/checks.py:683/691/697`. | +| `src/qg/checks.py` `check_branch_mergeable` | при SUCCESS lease ДЕРЖИТСЯ до фактического merge PR. Если процесс умрёт после этого — lease зажат. | +| `src/reconciler.py` | паттерн-образец фонового daemon-потока (never-raise, kill-switch, observability в `/queue`). | + +## 1. Задействованные модули `src/` + +- `src/db.py` — новые job-запросы для reaper (выборка stale running-job, атомарный + reap). Возможна lightweight-миграция (см. §3). +- **Job-reaper** — НОВЫЙ модуль (кандидат `src/job_reaper.py`) ИЛИ расширение + `src/reconciler.py`. Решение — за архитектором; ТЗ требует daemon-поток по образцу + `reconciler` (never-raise, `_stop`-Event, старт/стоп в `main.lifespan`, снимок в + `/queue`). +- `src/merge_gate.py` — функция проактивного реклейма stale/dead lease (по pid- + liveness + по TTL); helper проверки liveness pid; helper идемпотентной + финализации merge. +- `src/main.py` — старт/стоп нового daemon-потока в `lifespan` (после `worker.start()` + / `reconciler.start()`, симметрично остановка перед `worker.stop()`); вызов + стартового реклейма stale-lease рядом с `requeue_running_jobs()`. +- `src/config.py` — новые настройки/флаги (см. §5). +- `src/main.py` `GET /queue` — блок наблюдаемости reaper (образец `reconcile`/ + `post_deploy`). + +## 2. Функциональные требования + +### FR-1. Job-reaper (Проблема A) +- Фоновый поток периодически (`reaper_interval_s`) сканирует строки `jobs` в статусе + `running`. +- Для каждого `running`-job определяет, **жив ли его исполнитель**. «Мёртвым» job + считается, когда выполнено и устойчиво (см. FR-1.3) хотя бы одно из: + - процесс агента (по pid/run_id) не существует, а финализация не произошла; + - `agent_runs` строки run_id имеет `finished_at`/`exit_code` (процесс реально + завершился), но `jobs.status` всё ещё `running` (monitor умер между записью + exit_code и `_finalize_job`); + - job висит `running` дольше предохранительного потолка + `reaper_max_running_s` (заведомо больше любого легитимного `agent_timeout` + + grace) — backstop на случай, когда liveness определить нельзя. +- FR-1.2 Действие при подтверждённой смерти: + - если есть достоверный успешный исход (`agent_runs.exit_code == 0`) — довести job + к корректному завершению **через тот же контракт**, что `_finalize_job` + (включая, при необходимости, повторную попытку auto-advance) — НЕ дублировать + переход, если он уже произошёл (идемпотентность через `has_active_job_for_task` + / проверку стадии); + - если исход неуспешный/неизвестен и бюджет попыток не исчерпан + (`attempts < max_attempts`) — `queued` (повторная постановка), как делает + `requeue_running_jobs`; + - если бюджет исчерпан — `failed` + Telegram-алерт. +- FR-1.3 **Анти-ложноположительность.** Job помечается зомби только после + устойчивого подтверждения смерти: процесс мёртв на протяжении `reaper_dead_ticks` + последовательных тиков (≥2) ИЛИ превышен `reaper_max_running_s`. Живой долгий + агент (в пределах своего `agent_timeout`) НИКОГДА не реапится. +- FR-1.4 Работает **без рестарта** процесса (главное отличие от существующего + `requeue_running_jobs`). +- FR-1.5 Restart-safe: после рестарта поведение корректно совмещается со стартовым + `requeue_running_jobs()` (нет двойной обработки одной строки; атомарность reap- + UPDATE с guard по `status='running'`, как в `claim_next_job`). + +### FR-2. Проактивный реклейм stale/dead merge-lease (Проблема B) +- FR-2.1 На старте процесса (рядом с `requeue_running_jobs()` в `lifespan`) и + периодически в фоновом потоке: для каждого репо с merge-gate проверить lease и + освободить его, если держатель **мёртв** или lease **просрочен**. +- FR-2.2 «Держатель мёртв» = pid из lease не существует в системе (liveness-проба, + напр. `os.kill(pid, 0)` с обработкой `ProcessLookupError`/`PermissionError`), + при условии что pid принадлежит этому хосту/неймспейсу. «Просрочен» = `age >= + merge_lock_timeout_s` (существующий TTL-контракт сохраняется). +- FR-2.3 Реклейм **holder-aware и безопасен**: НЕ освобождать lease, чей держатель + жив и в пределах TTL (защита легитимного merge). Логировать `warning` при каждом + реклейме (наблюдаемость, как сейчас в `acquire_merge_lease`). +- FR-2.4 Условность как ORCH-35/43: реально только для self-hosting/`merge_gate_repos`; + прочие репо — no-op. +- FR-2.5 Контракт **never-raise**; любая ошибка реклейма не должна валить поток. + +### FR-3. Идемпотентная финализация merge (Проблема C) +- FR-3.1 Если ветка прошла rebase+re-test (догнана до `origin/main` и зелёная), но + merge PR не состоялся из-за смерти процесса — система должна **докатить/повторить** + merge без повторного прогона дорогих шагов, когда это безопасно. +- FR-3.2 Финализация merge должна быть **идемпотентной**: повторный вызов при уже + слитом PR — no-op (определять по состоянию PR в Gitea и/или по + `branch_is_behind_main`/состоянию `main`), без ошибки и без второго слияния. +- FR-3.3 Восстановление re-drive обеспечивается штатными механизмами (reaper + довёл job до `queued` → повторный проход стадии `deploy`/merge-gate; либо + reconciler доигрывает переход). Дублирующая логика merge НЕ создаётся — переиспользуются + существующие пути (`check_branch_mergeable` / deployer-merge). +- FR-3.4 При повторе lease берётся заново (идемпотентный re-acquire «held by self» + по branch уже поддержан в `acquire_merge_lease`). + +### FR-4. Наблюдаемость +- FR-4.1 Блок `reaper` в `GET /queue`: enabled, interval, last_run_ts, reaped_total, + last_reaped (job_id/agent), lease_reclaimed_total (best-effort, как у reconciler). +- FR-4.2 Каждый reap и каждый lease-reclaim — `logger.warning` с идентификаторами + (job_id, run_id, pid, repo, branch). +- FR-4.3 При reap→`failed` и при lease-reclaim — Telegram (как существующие алерты). + +## 3. Изменения схемы БД +- Текущая `jobs` НЕ содержит `pid`. Для надёжной pid-liveness job-reaper'у, скорее + всего, потребуется **lightweight-миграция**: добавить `jobs.pid INTEGER` (через + `_ensure_column`, идемпотентно, безопасно на live prod DB — паттерн уже + применяется в `db.py`). pid проставляется в `_spawn` рядом с `run_id`/`started_at`. +- **Альтернатива без миграции** (на усмотрение архитектора): определять смерть по + `agent_runs.finished_at/exit_code` + потолку `reaper_max_running_s`, без хранения + pid в `jobs`. ADR должен зафиксировать выбор и обоснование. +- Реестры `STAGE_TRANSITIONS` и `QG_CHECKS` — **без изменений** (новых стадий/гейтов + не вводим; reaper и lease-reclaim — фоновые механизмы, не стадии). +- Merge-lease остаётся файловым (`.merge-lease-.json`); схема файла lease + не меняется (pid и acquired_at уже есть). + +## 4. Изменения API +- `GET /queue` — добавить блок `reaper` (read-only наблюдаемость). Прочие endpoints + без изменений. Новых webhook-роутов нет. + +## 5. Конфигурация / kill-switches (`src/config.py`) +Именование — по образцу `reconcile_*` / `merge_*`. Кандидаты (точные имена/дефолты +уточняет архитектор): + +| Настройка | Назначение | Дефолт (предложение) | +|-----------|-----------|----------------------| +| `reaper_enabled` | глобальный kill-switch job-reaper | `true` | +| `reaper_interval_s` | период сканирования | `60` | +| `reaper_dead_ticks` | сколько подряд тиков pid должен быть мёртв перед reap | `2` | +| `reaper_max_running_s` | потолок «running» (backstop), > max agent_timeout+grace | `3600` | +| `lease_reclaim_enabled` | kill-switch проактивного реклейма lease | `true` | +| (переиспользуется) `merge_lock_timeout_s` | TTL lease | `300` (как есть) | +| (переиспользуется) `merge_gate_repos` | область применения lease-reclaim | как есть | + +Все флаги — пробрасываются из env (`ORCH_*`), `false` → строго прежнее поведение. + +## 6. Требования к QG checks +- Новых QG checks НЕ вводить (это фоновые resilience-механизмы, не гейты выхода со + стадии). `check_branch_mergeable` остаётся контрактно неизменным; допускается лишь + переиспользование его как идемпотентного пути финализации merge (FR-3.3). + +## 7. Артефакты pipeline, создаваемые/обновляемые в ЭТОМ PR +- Код: см. §1. +- `06-adr/ADR-001-*.md` — архитектурное решение (где живёт reaper; pid-колонка vs + эвристика; механизм идемпотентного merge) — создаёт architect. +- `docs/architecture/README.md` — новый раздел про job-reaper + lease-reclaim + (golden-source, в этом же PR). +- `docs/architecture/internals.md` — детали (если затрагивается схема БД / потоки). +- `CHANGELOG.md` — запись ORCH-065. +- `.env.example` — новые `ORCH_*` флаги (канон секретов/настроек). +- `docs/operations/INFRA.md` — упоминание поведения при self-restart, если + затрагивается (best-effort). + +## 8. Инварианты (НЕ нарушать) +- Не ронять/не рестартить прод-контейнер `orchestrator` в рамках задачи. +- Никогда не пушить/форс-пушить `main`; реклейм lease не инициирует git-операций. +- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, контракты `check_*`, БАГ-8 откат, + exit-коды deploy-хука — без изменений. +- never-raise на единицу фоновой работы; идемпотентность; restart-safe; тишина при + отсутствии аномалий (как reconciler). +- Анти-ложноположительность (FR-1.3): живой долгий агент не реапится. diff --git a/docs/work-items/ORCH-065/03-acceptance-criteria.md b/docs/work-items/ORCH-065/03-acceptance-criteria.md new file mode 100644 index 0000000..f1630bc --- /dev/null +++ b/docs/work-items/ORCH-065/03-acceptance-criteria.md @@ -0,0 +1,122 @@ +# Критерии приёмки — ORCH-065 + +Work Item ID: ORCH-065 +Формат: каждый критерий имеет явное условие PASS/FAIL. Все критерии должны быть +PASS для прохождения review/testing. + +## A. Job-reaper (Проблема A) + +### AC-1 — реап мёртвого running-job без рестарта +- PASS: при наличии строки `jobs` в статусе `running`, чей процесс/исполнитель + достоверно мёртв (pid не существует ИЛИ `agent_runs.exit_code` записан, а job всё + ещё `running`) и условие устойчивости (FR-1.3) выполнено, фоновый reaper переводит + строку в корректный статус (`done`/`queued`/`failed`) **без перезапуска процесса**. +- FAIL: строка остаётся `running` после `reaper_dead_ticks` тиков / превышения + `reaper_max_running_s`. + +### AC-2 — разблокировка очереди при concurrency=1 +- PASS: после реапа зомби-строки `count_running_jobs()` снижается, и следующий + queued-job успешно claim'ится воркером. +- FAIL: очередь остаётся заблокированной зомби-строкой. + +### AC-3 — анти-ложноположительность (живой долгий агент не реапится) +- PASS: `running`-job с ЖИВЫМ процессом в пределах его `agent_timeout` НЕ помечается + зомби (ни по одному тику, ни в пределах `reaper_max_running_s`, если потолок + больше таймаута). +- FAIL: живой агент помечен `failed`/`queued` reaper'ом. + +### AC-4 — корректный исход по результату +- PASS: при `agent_runs.exit_code == 0` reaper доводит до успешного завершения без + дублирования уже выполненного stage-advance (идемпотентно); при неуспехе и + `attempts < max_attempts` → `queued`; при исчерпании → `failed` + Telegram. +- FAIL: успешный исход помечен `failed`; либо дублируется stage-переход; либо + исчерпанный бюджет молча зацикливается на `queued`. + +### AC-5 — restart-safe совместимость +- PASS: одновременная работа стартового `requeue_running_jobs()` и периодического + reaper не приводит к двойной обработке одной строки (атомарный UPDATE с guard + `status='running'`). +- FAIL: одна строка обработана дважды / гонка приводит к рассинхрону статуса. + +## B. Stale/dead merge-lease reclaim (Проблема B) + +### AC-6 — реклейм lease мёртвого держателя +- PASS: lease `.merge-lease-.json`, чей `pid` не существует, проактивно + освобождается на старте И периодическим потоком (не дожидаясь TTL и не дожидаясь + чужого `acquire`). +- FAIL: lease мёртвого держателя остаётся до истечения `merge_lock_timeout_s` или + до следующего чужого `acquire`. + +### AC-7 — реклейм по TTL сохранён +- PASS: lease старше `merge_lock_timeout_s` освобождается (существующий контракт не + сломан), с `logger.warning`. +- FAIL: просроченный lease не освобождается. + +### AC-8 — не трогать живой lease +- PASS: lease с ЖИВЫМ держателем (pid жив) и возрастом `< merge_lock_timeout_s` НЕ + освобождается (защита легитимного merge). +- FAIL: освобождён lease живого держателя → возможен параллельный конфликтный merge. + +### AC-9 — условность и never-raise +- PASS: реклейм реален только для `merge_gate_repos`/self-hosting; для прочих репо + — no-op; любая ошибка реклейма логируется и не валит поток (never-raise). +- FAIL: реклейм выполняется для не-self-hosting репо; либо ошибка пробрасывается + наружу/роняет поток. + +## C. Идемпотентная финализация merge (Проблема C) + +### AC-10 — докатывание незавершённого merge +- PASS: сценарий «rebase+re-test зелёные, merge не состоялся, процесс умер» + восстанавливается автоматически (job → `queued` reaper'ом / reconciler доигрывает), + и merge доводится без повторного ненужного прогона дорогих шагов. +- FAIL: задача остаётся в полу-выполненном состоянии, требует ручного merge. + +### AC-11 — идемпотентность при уже слитом PR +- PASS: повторный вызов финализации при уже слитом PR — no-op (определяется по + состоянию PR/`main`), без ошибки и без второго merge. +- FAIL: второй merge / ошибка при уже слитом PR. + +## D. Инварианты и безопасность self-hosting + +### AC-12 — прод-контейнер не трогается +- PASS: ни reaper, ни lease-reclaim не рестартят/не роняют прод-контейнер и не + инициируют git-push в `main`. +- FAIL: любая из новых веток кода рестартит self / пушит main. + +### AC-13 — контракты неизменны +- PASS: `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, сигнатуры/поведение `check_*`, + БАГ-8 откат, exit-коды deploy-хука — без изменений; новых QG checks/стадий нет. +- FAIL: затронут любой из перечисленных контрактов. + +### AC-14 — kill-switches +- PASS: `reaper_enabled=false` → reaper не работает (строго прежнее поведение); + `lease_reclaim_enabled=false` → проактивный реклейм отключён (остаётся лишь + прежний ленивый TTL-реклейм в `acquire`). +- FAIL: флаг `false` не отключает соответствующий механизм. + +## E. Наблюдаемость + +### AC-15 — блок reaper в /queue +- PASS: `GET /queue` содержит блок `reaper` (enabled, interval, last_run_ts, + reaped_total, last_reaped, lease_reclaimed_total). +- FAIL: блок отсутствует/не обновляется. + +### AC-16 — логи и алерты +- PASS: каждый reap и lease-reclaim → `logger.warning` с идентификаторами; + reap→`failed` и lease-reclaim → Telegram. +- FAIL: реап/реклейм происходят молча. + +## F. Документация (gate reviewer) + +### AC-17 — golden-source обновлён в этом же PR +- PASS: обновлены `docs/architecture/README.md` (раздел про reaper + lease-reclaim), + `CHANGELOG.md`, `.env.example` (новые `ORCH_*` флаги); заведён + `06-adr/ADR-001-*.md`. +- FAIL: код изменён, документация — нет (reviewer → REQUEST_CHANGES). + +## G. Тесты + +### AC-18 — регресс-тесты зелёные +- PASS: новые unit/integration тесты (см. 04-test-plan.yaml) проходят; существующий + `pytest tests/ -q` зелёный (нет регресса merge_gate / queue / reconciler). +- FAIL: любой тест из плана красный или сломан существующий тест. diff --git a/docs/work-items/ORCH-065/04-test-plan.yaml b/docs/work-items/ORCH-065/04-test-plan.yaml new file mode 100644 index 0000000..78b6b37 --- /dev/null +++ b/docs/work-items/ORCH-065/04-test-plan.yaml @@ -0,0 +1,196 @@ +work_item: ORCH-065 +description: > + Тест-план для фикса zombie jobs (job-reaper), залипшего merge-lease + (проактивный реклейм dead/stale lease) и идемпотентной финализации merge. + Все новые фоновые механизмы — never-raise, restart-safe, kill-switch. + Модуль reaper и точные имена функций уточнит архитектор; в module указаны + кандидатные пути (tests/test_job_reaper.py / tests/test_merge_lease_reclaim.py). + +tests: + # ---- A. Job-reaper ---- + - id: TC-01 + type: unit + description: > + Reaper переводит running-job с мёртвым исполнителем в корректный статус + без рестарта процесса (pid не существует / exit_code записан, job всё ещё + running). Покрывает AC-1. + module: tests/test_job_reaper.py + expected: PASS + + - id: TC-02 + type: unit + description: > + Анти-ложноположительность: running-job с ЖИВЫМ процессом в пределах + agent_timeout НЕ реапится (ни по одному тику, ни в пределах reaper_max_running_s). + Покрывает AC-3. + module: tests/test_job_reaper.py + expected: PASS + + - id: TC-03 + type: unit + description: > + Устойчивость: job помечается зомби только после reaper_dead_ticks + последовательных тиков смерти pid (>=2), а не на первом тике. Покрывает FR-1.3. + module: tests/test_job_reaper.py + expected: PASS + + - id: TC-04 + type: unit + description: > + Backstop по потолку: job, висящий running дольше reaper_max_running_s, + реапится даже если liveness определить нельзя. Покрывает FR-1.1/AC-1. + module: tests/test_job_reaper.py + expected: PASS + + - id: TC-05 + type: unit + description: > + Корректный исход: exit_code==0 -> успешное завершение без дублирования + stage-advance; неуспех при attempts queued; исчерпан бюджет -> failed + + Telegram. Покрывает AC-4. + module: tests/test_job_reaper.py + expected: PASS + + - id: TC-06 + type: unit + description: > + Атомарность reap-UPDATE с guard status='running': параллельная обработка + одной строки (стартовый requeue_running_jobs + reaper) не приводит к двойному + reap. Покрывает AC-5. + module: tests/test_job_reaper.py + expected: PASS + + - id: TC-07 + type: unit + description: > + Kill-switch reaper_enabled=false -> reaper не трогает ни одной строки + (строго прежнее поведение). Покрывает AC-14. + module: tests/test_job_reaper.py + expected: PASS + + - id: TC-08 + type: unit + description: > + never-raise: ошибка БД/ОС внутри одного тика reaper не пробрасывается и не + останавливает поток (изоляция per-job, образец reconciler). Покрывает AC-9. + module: tests/test_job_reaper.py + expected: PASS + + - id: TC-09 + type: integration + description: > + Очередь разблокируется: после reap зомби-строки при max_concurrency=1 + следующий queued-job успешно claim'ится (claim_next_job + count_running_jobs). + Покрывает AC-2. + module: tests/test_queue.py + expected: PASS + + # ---- B. Stale/dead merge-lease reclaim ---- + - id: TC-10 + type: unit + description: > + Реклейм lease с мёртвым pid (os.kill(pid,0) -> ProcessLookupError) + проактивно, не дожидаясь TTL и чужого acquire. Покрывает AC-6. + module: tests/test_merge_lease_reclaim.py + expected: PASS + + - id: TC-11 + type: unit + description: > + Реклейм по TTL (age >= merge_lock_timeout_s) сохранён, с logger.warning. + Покрывает AC-7. (расширяет существующий stale-lease сценарий merge_gate.) + module: tests/test_merge_lease_reclaim.py + expected: PASS + + - id: TC-12 + type: unit + description: > + Живой lease (pid жив, age < TTL) НЕ освобождается — защита легитимного merge. + Покрывает AC-8. + module: tests/test_merge_lease_reclaim.py + expected: PASS + + - id: TC-13 + type: unit + description: > + Условность: реклейм реален только для merge_gate_repos/self-hosting; для + прочих репо no-op. Покрывает AC-9. + module: tests/test_merge_lease_reclaim.py + expected: PASS + + - id: TC-14 + type: unit + description: > + never-raise: ошибка чтения/удаления lease-файла не валит реклейм-поток. + Покрывает AC-9. + module: tests/test_merge_lease_reclaim.py + expected: PASS + + - id: TC-15 + type: unit + description: > + Kill-switch lease_reclaim_enabled=false -> проактивный реклейм отключён, + остаётся лишь прежний ленивый TTL-реклейм в acquire_merge_lease. + Покрывает AC-14. + module: tests/test_merge_lease_reclaim.py + expected: PASS + + # ---- C. Идемпотентная финализация merge ---- + - id: TC-16 + type: unit + description: > + Идемпотентность финализации: повторный вызов при уже слитом PR / уже + актуальном main (branch_is_behind_main == False) — no-op, без ошибки и без + второго merge. Покрывает AC-11. + module: tests/test_merge_gate.py + expected: PASS + + - id: TC-17 + type: integration + description: > + Восстановление: сценарий "rebase+re-test зелёные, merge не состоялся, + процесс умер" -> job доводится до queued reaper'ом и merge докатывается + штатным путём без повторного дорогого re-test, когда безопасно. Покрывает AC-10. + module: tests/test_merge_gate_race.py + expected: PASS + + # ---- D/E. Инварианты и наблюдаемость ---- + - id: TC-18 + type: integration + description: > + GET /queue содержит блок reaper (enabled, interval, last_run_ts, + reaped_total, last_reaped, lease_reclaimed_total). Покрывает AC-15. + module: tests/test_queue.py + expected: PASS + + - id: TC-19 + type: unit + description: > + Контракты неизменны: STAGE_TRANSITIONS и реестр QG_CHECKS не получили новых + стадий/чеков; check_branch_mergeable сигнатурно не изменён. Покрывает AC-13. + module: tests/test_config.py + expected: PASS + + - id: TC-20 + type: unit + description: > + Новые настройки reaper_*/lease_reclaim_* присутствуют в config с дефолтами и + читаются из ORCH_* env. Покрывает §5 ТЗ / AC-14. + module: tests/test_config.py + expected: PASS + + - id: TC-21 + type: unit + description: > + Стартовый реклейм stale/dead lease вызывается в lifespan рядом с + requeue_running_jobs (smoke на регистрацию старт/стоп reaper-потока). + Покрывает FR-2.1 / AC-6. + module: tests/test_job_reaper.py + expected: PASS + +regression: + - command: pytest tests/ -q + expected: PASS + note: > + Полный прогон не должен ломать существующие тесты merge_gate / queue / + reconciler / deploy.