140 lines
12 KiB
Markdown
140 lines
12 KiB
Markdown
---
|
||
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`
|
||
(пути 1–4: задача требовала рестарта/ретрая/реапа).
|
||
|
||
### Механизм «никогда не клеймится» (гипотеза к подтверждению в 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` (на укороченном маршруте не обязателен).
|