Files
orchestrator/docs/work-items/ORCH-126/02-trz.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

98 lines
8.9 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
---
# 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`); не сломать их инварианты.