Files
orchestrator/docs/work-items/ORCH-065/02-trz.md

15 KiB
Raw Blame History

ТЗ — 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 всех runningqueued. Без рестарта не запускается.
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-<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): живой долгий агент не реапится.