Files
orchestrator/docs/work-items/ORCH-126/01-brd.md
claude-bot 453c5b7d04
All checks were successful
CI / test (push) Successful in 1m12s
analyst(ET): auto-commit from analyst run_id=773
2026-06-17 11:07:33 +03:00

12 KiB
Raw Blame History

work_item, stage, author_agent, status, created_at, model_used, track
work_item stage author_agent status created_at model_used track
ORCH-126 analysis analyst ready-for-review 2026-06-17 claude-opus-4-8 bug

01 — BRD / Bug-report: ORCH-126 — queued-job хранит протухший run_id/pid и не клеймится даже при выключенном serial-gate

Work Item: ORCH-126 · Repo: orchestrator · Стадия: analysis · Трек: Bug (укороченный маршрут, ORCH-019)

🐞 Багфикс-трек, облегчённый пакет (ORCH-019). Дефект контрол-плейна локализован, причина установлена по коду, корректное поведение однозначно (queued-job не должен нести run-ownership). Правка — точечная гигиена жизненного цикла строки jobs + диагностика, по существующим паттернам; ADR/макет не требуются → стадия architecture пропускается, escalate: full-cycle НЕ ставится. ⚠️ Трассировка (CLAUDE.md §9): правка затрагивает инварианты ORCH-065 (Tier-1 pid-liveness reaper'а), ORCH-113 (finalizer-liveness), ORCH-114 (recover_on_startup), ORCH-099 (/metrics читает pid/run_id) — перед изменением прочитать их 06-adr/ и не сломать.


1. Бизнес-контекст и проблема

Симптом (наблюдаемое, из инцидента)

Второй дефект контрол-плейна, найденный при попытке провести срочные задачи ORCH-124/125 мимо serial-gate. Даже при ORCH_SERIAL_GATE_ENABLED=false queued analyst-job'ы зависают и никогда не переходят в running:

  • job 2286 (ORCH-125): status=queued при run_id=759/760 и pid=35/42, тогда как started_at=NULLфизически невозможное состояние (run-ownership выставлен, но запуск не состоялся).
  • job 2303 (ORCH-124): при выключенном serial-gate минутами оставался queued; счётчики очереди queued=1, running=0 — задача не клеймится, хотя слот свободен.

Вывод инцидента: это claim/restart/zombie-state баг, независимый от семантики serial-gate.

Причина симптома (установленный факт — по коду)

Ни один путь возврата job'а в queued не сбрасывает run-ownership (run_id, pid). Эти колонки выставляются в launcher._spawn (run_id после INSERT в agent_runs, pid после Popen), но при любом откате в queued остаются «протухшими» от прошлой попытки. Затронуты 5 точек:

# Путь Что чистит run_id/pid
1 db.requeue_running_jobs() (restart-recovery, src/db.py:1475) started_at НЕ чистит
2 db.mark_job(status='queued') (src/db.py:1239) started_at/finished_at НЕ чистит
3 db.mark_job_transient() (src/db.py:1213) started_at/finished_at НЕ чистит
4 db.reap_running_job(status='queued') (src/db.py:1619) started_at/finished_at НЕ чистит
5 db.claim_next_job() (src/db.py:1143) ставит started_at, attempts++ НЕ сбрасывает stale pid

Это в точности воспроизводит наблюдаемое queued + run_id=759/760 + pid=35/42 + started_at=NULL (пути 14: задача требовала рестарта/ретрая/реапа).

Механизм «никогда не клеймится» (гипотеза к подтверждению в development — взаимодействие с reaper)

claim_next_job сам по себе на run_id/pid не смотрит (SELECT гейтит лишь status='queued' AND available_at<=now + dep/serial-gate), поэтому stale-метаданные не блокируют SELECT напрямую. Старвейшн рождается из взаимодействия stale-pid с job-reaper'ом (ORCH-065), который сканирует status='running' и судит Tier-1 liveness по jobs.pid через merge_gate.pid_alive:

  1. Окно claim→spawn. claim_next_job ставит running+started_at, но pid остаётся stale; реальный pid пишется только в _spawn после Popen (launcher.py:711). Между этими шагами (или если _spawn упал на ensure_worktree/_materialize_deferred_branch до строки 711) reaper видит running со старым pid.
  2. pid переиспользован (вероятно после рестарта контейнера) → pid_alive(stale)=True → reaper «видит живой процесс» и никогда не реапит действительно застрявшую строку; при max_concurrency=1 (дефолт) этот фантомный running блокирует клейм всей очереди.
  3. pid мёртв → reaper копит dead-тики (reaper_dead_ticks=2) против чужого pid и может отбросить легитимно-стартующий job обратно в queued/failed — «выглядит частично стартовавшим, но фактически не запускается».

Таким образом stale run-ownership искажает сигналы liveness/диагностики (reaper + /metrics get_running_agents), делая клейм/рестарт ненадёжным. Точный путь старвейшна подтверждается добавляемой авто-санацией/диагностикой (см. 04-test-plan).

2. Объём (scope)

В объёме

  • Аудит и фикс гигиены строки jobs вокруг возврата в queued: claim_next_job, requeue_running_jobs, mark_job, mark_job_transient, reap_running_job, окно _spawn.
  • Гарантия: queued-job либо чисто клеймится, либо детерминированно сбрасывается/реквью́ится при рестарте — без удержания stale run-ownership.
  • Авто-санация или явная диагностика «невозможного» queued-состояния (run_id/pid есть, а started_at нет).
  • Тесты на restart/requeue и stale queued-run-метаданные.

Вне объёма

  • Семантика serial-gate (ORCH-088/124), dep-gate (ORCH-026) — НЕ трогаем (баг от них независим).
  • Изменение STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict-ключей / схемы БД (новых колонок не вводим — фикс на существующих).
  • Переписывание reaper'а (ORCH-065/113) и transition-lease (ORCH-114) — лишь не сломать их инварианты.

3. Заинтересованные стороны

  • Оператор (заказчик фикса) — нуждается в надёжном bypass'е: при выключенном serial-gate срочная задача обязана стартовать.
  • Self-hosting orchestrator + все проекты на общем инстансе/очереди — фантомный running при max_concurrency=1 клинит очередь всех проектов.

4. Бизнес-требования (BR)

  • BR-1 — При выключенном serial-gate (ORCH_SERIAL_GATE_ENABLED=false) валидный queued ORCH-job клеймится и переходит в running штатно (без зависания).
  • BR-2 — Job, возвращённый в queued (рестарт / ретрай / transient / reap), не несёт stale run-ownership: после возврата run_id IS NULL и pid IS NULL.
  • BR-3 — Свежеклеймленный, ещё не заспавненный job не несёт stale pid (reaper не судит liveness по чужому процессу).
  • BR-4 — «Невозможные» queued-состояния (run_id/pid при отсутствии started_at) авто-санируются или явно сигнализируются (лог + наблюдаемость в GET /queue).
  • BR-5Регресс-тест: до фикса воспроизводит stale-состояние/старвейшн (красный), после — зелёный.

5. Нефункциональные требования (NFR)

  • NFR-1 (never-raise / never-wedge): правки в горячем пути клейма не должны ронять или заклинивать очередь всех проектов; ошибка диагностики — изолирована и не влияет на клейм.
  • NFR-2 (offline hot-path): claim_next_job остаётся offline (только локальная БД), без сети.
  • NFR-3 (совместимость): схема БД не меняется; поведение для не-stale job'ов байт-в-байт; enduro-trails не затронут.
  • NFR-4 (self-hosting safety): правка не рестартит/не роняет прод-контейнер, не трогает main, без новых процессов; миграция БД не требуется (правки на существующих колонках).
  • NFR-5 (restart-safe / идемпотентность): санация выдерживает повторный рестарт и гонку worker↔reaper↔monitor (атомарные guard'ы по status сохранены).

6. Допущения и ограничения

  • Дефолт max_concurrency=1 (config.py:114) — единственный stuck-running клинит очередь; поэтому корректность liveness reaper'а критична.
  • run_id для queued-job — мёртвая ссылка на прошлую попытку (текущего run'а нет), её сброс безопасен; история живёт в таблице agent_runs, не в jobs.run_id.
  • Env читается на старте процесса: на self-hosting выключение флага требует управляемого рестарта (вне объёма этого фикса; здесь — гарантия корректного клейма после рестарта).

7. Критерии успеха

Queued ORCH-job при выключенном serial-gate стартует штатно; queued-job'ы никогда не удерживают stale run-ownership после рестарта/ретрая; невозможные queued-состояния авто-санируются или явно видны; регресс-тесты покрывают restart/requeue и stale queued-run-метаданные. Полный pytest tests/ -q зелёный. Детальные PASS/FAIL — 03-acceptance-criteria.md.

8. Риски

  • Сброс run_id/pid в неверной точке мог бы стереть идентичность активного run'а → строго на переходе в queued и/или в claim до _spawn (детали — TRZ FR-1..FR-3, решает developer).
  • Взаимодействие с reaper Tier-1/Tier-3 и transition-lease — см. трассировку выше; детальный риск-разбор — 10-tech-risks.md (на укороченном маршруте не обязателен).