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

140 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
work_item: ORCH-126
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-17
model_used: claude-opus-4-8
track: 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` (на укороченном маршруте не обязателен).