15 KiB
15 KiB
ТЗ — 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.pyGET /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-<repo>.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): живой долгий агент не реапится.