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