8.9 KiB
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 |
02 — ТЗ (TRZ): ORCH-126 — гигиена run-ownership queued-job + диагностика невозможных состояний
Work Item: ORCH-126 · Repo: orchestrator · Стадия: analysis · Трек: Bug
ТЗ описывает конкретные изменения к реализации, выведенные из BRD и фактического кода. «Как именно» (точка сброса, форма диагностики) — в пределах FR; финальный выбор — за developer. Архитектурного решения/ADR не требуется (укороченный маршрут ORCH-019).
1. Сводка изменения
Сбрасывать run-ownership строки jobs (run_id, pid) во всех путях возврата job'а в queued
и/или в момент claim до _spawn, чтобы (а) queued-job никогда не нёс протухший run_id/pid,
(б) свежеклеймленный-ещё-не-заспавненный job не нёс stale pid, который job-reaper примет за чужой
живой/мёртвый процесс. Плюс — детектор «невозможного» queued-состояния: авто-санация при старте/реапе
и наблюдаемость (лог + счётчик в GET /queue). Схема БД и контракты гейтов не меняются.
2. Задействованные модули / пути
| Путь | Действие |
|---|---|
src/db.py |
изменить: requeue_running_jobs / mark_job('queued') / mark_job_transient / reap_running_job('queued') — добавить сброс run_id=NULL, pid=NULL; claim_next_job — сброс stale pid (и run_id) при флипе в running |
src/agents/launcher.py |
проверить окно _spawn (claim→run_id/started_at→Popen→pid): убедиться, что при провале _spawn до строки 711 job не остаётся со stale pid (опирается на сброс в claim/mark_job) |
src/job_reaper.py |
(опц., по выбору developer) Tier-1 анти-false-positive: pid IS NULL у свежего running уже трактуется как «нет pid → не реапить»; добавить авто-санацию/счётчик невозможных queued-строк, если фикс на стороне reaper |
src/main.py |
(опц.) при старте после requeue_running_jobs — лог/санация обнаруженных невозможных queued-состояний (наблюдаемость BR-4) |
tests/test_orch126_queued_stale_run.py |
создать: регресс + покрытие FR-1..FR-4 |
3. Функциональные требования
FR-1 — Сброс run-ownership на всех путях возврата в queued (BR-2)
Каждый путь, переводящий job в queued, обязан выставить run_id=NULL и pid=NULL той же
UPDATE-транзакцией, что уже чистит started_at/finished_at:
db.requeue_running_jobs() (restart-recovery), db.mark_job(status='queued'),
db.mark_job_transient(), db.reap_running_job(status='queued').
Инвариант: status='queued' ⇒ run_id IS NULL AND pid IS NULL AND started_at IS NULL.
Атомарные guard'ы по status (reap_running_job ... WHERE status='running') — сохранить байт-в-байт.
FR-2 — Чистый claim (BR-1, BR-3)
db.claim_next_job при флипе queued→running не должен оставлять stale pid (и run_id) от прошлой
попытки: либо сбросить их в том же UPDATE (pid=NULL, run_id=NULL), либо опираться на FR-1 (тогда
queued-job их уже не несёт). Defense-in-depth (оба) — предпочтительно. SELECT-гейт
(status='queued' AND available_at<=now + dep/serial-gate) — не трогать (NFR-2 offline).
Результат: между claim и стампом pid в _spawn job имеет pid IS NULL (не чужой pid).
FR-3 — Безопасность окна _spawn (BR-3)
Если _spawn падает до стампа pid (launcher.py:711) — ensure_worktree/
_materialize_deferred_branch/_write_task_file, — обработчик queue_worker._drain_once
(mark_job('queued'|'failed')) обязан, по FR-1, оставить job без stale pid. Проверить, что
повторный claim после такого провала стартует штатно (а не оседает «частично стартовавшим»).
FR-4 — Детект и обработка невозможного состояния (BR-4)
«Невозможное» queued-состояние = status='queued' AND (run_id IS NOT NULL OR pid IS NOT NULL OR started_at IS NOT NULL). Поведение:
- Авто-санация при старте (
main.lifespanпослеrequeue_running_jobs) и/или при реап-тике — привести такие строки к чистомуqueued(FR-1) идемпотентно, never-raise. - Наблюдаемость — структурный WARNING с
job_id/run_id/pid+ read-only счётчик в блоке очередиGET /queue(напримерqueue.impossible_queuedили поле в существующем снимке worker'а).
FR-5 — Корректность reaper-liveness (BR-1, NFR-5)
После FR-1..FR-3 job-reaper (ORCH-065) на свежеклеймленном running видит pid IS NULL → Tier-1
не копит dead-тики против чужого pid и не реапит легитимный старт; фантомный «живой» pid не блокирует
очередь. Инварианты ORCH-065/113/114 (Tier-2 finalize-grace, finalizer-liveness, transition-lease) —
не нарушать.
4. Изменения API
Нет новых эндпоинтов. Расширение наблюдаемости read-only снимка GET /queue — добавить
счётчик/индикатор обнаруженных и санированных невозможных queued-состояний (BR-4); существующие поля
снимка не переименовывать.
5. Изменения схемы БД
Нет. Колонки jobs.run_id / jobs.pid / jobs.started_at уже существуют; фикс — корректное
заполнение/сброс. Никаких _ensure_column/новых таблиц/индексов.
6. Требования к новым/изменённым QG checks
Нет. STAGE_TRANSITIONS / реестр QG_CHECKS / check_* / machine-verdict-ключи — байт-в-байт.
Дефект — свойство гигиены данных планировщика, не Quality Gate.
7. Совместимость / регресс
- Обратная совместимость: для не-stale job'ов поведение байт-в-байт (они и так не несут
run_id/pidвqueued); фикс лишь нормализует аномальные строки. - Область: общий планировщик/очередь (не self-hosting-scoped leaf) — затрагивает все проекты, но семантически нейтрально (приведение к уже-документированному инварианту «queued = без run-ownership»).
- Kill-switch: правка — исправление инварианта данных, не новая фича; отдельный флаг не требуется. Опциональную авто-санацию/диагностику (FR-4) допустимо закрыть под флаг, если developer сочтёт нужным (дефолт = включено), но базовый сброс FR-1..FR-3 — безусловен.
- Обратимость: изменения локальны (UPDATE-наборы в
db.py); откат — ревертом PR. - Миграция: не требуется; существующие аномальные строки санируются при первом старте (FR-4).
- Трассировка (CLAUDE.md §9): перед правкой
pid/run_id-логики прочитать ADR ORCH-065 (reaper Tier-1), ORCH-113 (finalizer-liveness), ORCH-114 (transition-lease/recover_on_startup), ORCH-099 (/metrics); не сломать их инварианты.