analyst(ET): auto-commit from analyst run_id=773
All checks were successful
CI / test (push) Successful in 1m12s
All checks were successful
CI / test (push) Successful in 1m12s
This commit is contained in:
139
docs/work-items/ORCH-126/01-brd.md
Normal file
139
docs/work-items/ORCH-126/01-brd.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
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` (на укороченном маршруте не обязателен).
|
||||
97
docs/work-items/ORCH-126/02-trz.md
Normal file
97
docs/work-items/ORCH-126/02-trz.md
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
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`); не сломать их инварианты.
|
||||
107
docs/work-items/ORCH-126/03-acceptance-criteria.md
Normal file
107
docs/work-items/ORCH-126/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-126 — гигиена run-ownership queued-job
|
||||
|
||||
Work Item: **ORCH-126** · Repo: **orchestrator** · Стадия: analysis · Трек: **Bug**
|
||||
|
||||
Формат: каждый критерий имеет **PASS** / **FAIL**. Reviewer проверяет буквально по файлам репозитория
|
||||
и прогону тестов.
|
||||
|
||||
---
|
||||
|
||||
## AC-1 — Инвариант чистого queued после возврата
|
||||
|
||||
**Условие:** job возвращён в `queued` любым путём — `requeue_running_jobs` (restart),
|
||||
`mark_job('queued')` (retry), `mark_job_transient` (transient), `reap_running_job('queued')` (reaper).
|
||||
- **PASS:** после операции строка имеет `run_id IS NULL` **И** `pid IS NULL` **И** `started_at IS NULL`.
|
||||
- **FAIL:** хотя бы один из `run_id`/`pid`/`started_at` остаётся непустым у `status='queued'`.
|
||||
|
||||
---
|
||||
|
||||
## AC-2 — Клейм при выключенном serial-gate стартует штатно
|
||||
|
||||
**Условие:** `ORCH_SERIAL_GATE_ENABLED=false`, есть один валидный queued ORCH-job, `running=0`,
|
||||
`max_concurrency≥1`.
|
||||
- **PASS:** `claim_next_job` выбирает job, флипает в `running`, `_spawn` стартует; задача не зависает.
|
||||
- **FAIL:** job остаётся `queued` при свободном слоте (повтор `queued=1, running=0`).
|
||||
|
||||
---
|
||||
|
||||
## AC-3 — Свежеклеймленный job не несёт чужой pid
|
||||
|
||||
**Условие:** job клеймится из `queued`, который ранее нёс stale `pid`/`run_id`; до выполнения стампа
|
||||
`pid` в `_spawn`.
|
||||
- **PASS:** сразу после `claim_next_job` (до `_spawn` Popen-стампа) `jobs.pid IS NULL` (не stale).
|
||||
- **FAIL:** после claim строка несёт `pid` от прошлой попытки.
|
||||
|
||||
---
|
||||
|
||||
## AC-4 — Reaper не реапит легитимный старт по чужому pid
|
||||
|
||||
**Условие:** job только что склеймлен (`running`, `started_at=now`, `pid IS NULL`), job-reaper
|
||||
делает тик.
|
||||
- **PASS:** reaper не считает строку мёртвой (Tier-1 пропускает `pid IS NULL`); не возвращает её в
|
||||
`queued`/`failed`; легитимный старт не сбивается. Фантомный «живой» stale-pid больше не блокирует
|
||||
клейм очереди.
|
||||
- **FAIL:** reaper реапит стартующий job по чужому pid, либо вечно держит фантомный `running`.
|
||||
|
||||
---
|
||||
|
||||
## AC-5 — Невозможное queued-состояние авто-санируется / явно видно
|
||||
|
||||
**Условие:** в БД присутствует строка `status='queued'` с непустым `run_id`/`pid`/`started_at`
|
||||
(например после апгрейда на проблемной БД или гонки).
|
||||
- **PASS:** состояние приводится к чистому `queued` (авто-санация при старте/реапе, идемпотентно,
|
||||
never-raise) **и/или** явно сигнализируется (структурный WARNING с `job_id`/`run_id`/`pid` +
|
||||
счётчик в `GET /queue`).
|
||||
- **FAIL:** аномальная строка остаётся незамеченной и неисправленной.
|
||||
|
||||
---
|
||||
|
||||
## AC-6 — Окно `_spawn` устойчиво к провалу до стампа pid
|
||||
|
||||
**Условие:** `_spawn` падает до стампа `pid` (`ensure_worktree`/материализация ветки/запись task-файла);
|
||||
job уходит назад в `queued` обработчиком `_drain_once`.
|
||||
- **PASS:** строка в `queued` чистая (AC-1); повторный claim стартует штатно; нет «частично
|
||||
стартовавшего» зависания.
|
||||
- **FAIL:** после провала job несёт stale `pid`/`run_id` или не клеймится повторно.
|
||||
|
||||
---
|
||||
|
||||
## AC-7 — Без регрессов контрактов и схемы
|
||||
|
||||
**Условие:** прогон полного `pytest tests/ -q`; проверка диффа.
|
||||
- **PASS:** все тесты зелёные; `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / `check_*` /
|
||||
machine-verdict-ключи / схема БД (`jobs`) — без изменений; новых колонок/таблиц нет;
|
||||
поведение для не-stale job'ов неизменно; enduro не затронут.
|
||||
- **FAIL:** падение тестов, изменён контракт гейта/схема БД, или регресс для здоровых job'ов.
|
||||
|
||||
---
|
||||
|
||||
## AC-8 — Обязательный регресс-тест красный→зелёный
|
||||
|
||||
**Условие:** тест из `04-test-plan.yaml`, фиксирующий stale-состояние и/или старвейшн.
|
||||
- **PASS:** тест красный на коде ДО фикса, зелёный ПОСЛЕ.
|
||||
- **FAIL:** тест зелёный до фикса (не воспроизводит баг) или красный после.
|
||||
|
||||
---
|
||||
|
||||
## Сводная матрица AC ↔ FR/BR
|
||||
| AC | Покрывает |
|
||||
|----|-----------|
|
||||
| AC-1 | BR-2 / FR-1 |
|
||||
| AC-2 | BR-1 / FR-2 |
|
||||
| AC-3 | BR-3 / FR-2 |
|
||||
| AC-4 | BR-1, BR-3 / FR-5 |
|
||||
| AC-5 | BR-4 / FR-4 |
|
||||
| AC-6 | BR-3 / FR-3 |
|
||||
| AC-7 | NFR-1..NFR-4 |
|
||||
| AC-8 | BR-5 / FR-1..FR-4 |
|
||||
102
docs/work-items/ORCH-126/04-test-plan.yaml
Normal file
102
docs/work-items/ORCH-126/04-test-plan.yaml
Normal file
@@ -0,0 +1,102 @@
|
||||
work_item: ORCH-126
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: ready-for-review
|
||||
created_at: 2026-06-17
|
||||
model_used: claude-opus-4-8
|
||||
title: "ORCH-126 — гигиена run-ownership queued-job + диагностика невозможных состояний"
|
||||
framework: pytest
|
||||
track: bug
|
||||
scope: >
|
||||
Покрывает: сброс run_id/pid на всех путях возврата job в queued (restart/retry/transient/reap),
|
||||
чистый claim без stale pid, устойчивость окна _spawn, детект/санацию невозможных queued-состояний,
|
||||
корректность Tier-1 reaper-liveness. Вне покрытия: семантика serial-gate (ORCH-088/124) и dep-gate
|
||||
(ORCH-026), переписывание reaper/transition-lease, изменение схемы БД.
|
||||
notes: >
|
||||
TC-01 — ОБЯЗАТЕЛЬНЫЙ регресс-тест бага: красный на коде ДО фикса, зелёный ПОСЛЕ (BR-5 / AC-8).
|
||||
Тесты используют изолированную временную SQLite-БД (db.init_db во временной директории), без сети,
|
||||
без Claude CLI, без реального Popen (spawn мокается/подменяется). Полный регресс pytest tests/ -q
|
||||
должен оставаться зелёным (AC-7).
|
||||
|
||||
tests:
|
||||
- id: TC-01
|
||||
type: integration
|
||||
description: >
|
||||
РЕГРЕСС (red→green): job в running со stale run_id+pid (started_at set) проходит
|
||||
requeue_running_jobs() (имитация рестарта); ассерт — строка queued с run_id IS NULL,
|
||||
pid IS NULL, started_at IS NULL. До фикса pid/run_id остаются (красный), после — чисто.
|
||||
module: tests/test_orch126_queued_stale_run.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: >
|
||||
mark_job(job_id, 'queued') сбрасывает run_id и pid (а не только started_at/finished_at).
|
||||
module: tests/test_orch126_queued_stale_run.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: >
|
||||
mark_job_transient(...) на job со stale run_id/pid возвращает чистый queued (run_id/pid NULL),
|
||||
сохраняя инкремент transient_attempts и backoff available_at.
|
||||
module: tests/test_orch126_queued_stale_run.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: >
|
||||
reap_running_job(job_id, 'queued', ...) сбрасывает run_id/pid; атомарный guard
|
||||
WHERE status='running' сохранён (повторный вызов на уже-queued строке -> rowcount 0).
|
||||
module: tests/test_orch126_queued_stale_run.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: integration
|
||||
description: >
|
||||
claim_next_job() флипает queued->running и не оставляет stale pid: сразу после claim
|
||||
jobs.pid IS NULL (до _spawn-стампа). Подтверждает чистый старт (AC-3).
|
||||
module: tests/test_orch126_queued_stale_run.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: integration
|
||||
description: >
|
||||
При выключенном serial-gate (ORCH_SERIAL_GATE_ENABLED=false) валидный queued ORCH-job
|
||||
выбирается claim_next_job при running=0 и не зависает (AC-2).
|
||||
module: tests/test_orch126_queued_stale_run.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: integration
|
||||
description: >
|
||||
Reaper-тик на свежеклеймленном running с pid IS NULL не реапит строку (Tier-1 пропускает
|
||||
pid IS NULL), легитимный старт сохраняется (AC-4).
|
||||
module: tests/test_orch126_queued_stale_run.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: integration
|
||||
description: >
|
||||
Детект/санация невозможного состояния: предзаписанная строка queued с непустым pid/run_id/
|
||||
started_at приводится к чистому queued (авто-санация при старте/реапе) И/ИЛИ фиксируется
|
||||
счётчиком в снимке очереди; операция идемпотентна и never-raise (AC-5).
|
||||
module: tests/test_orch126_queued_stale_run.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: integration
|
||||
description: >
|
||||
Окно _spawn: при провале launch (launch_job/_spawn бросает до стампа pid) обработчик
|
||||
_drain_once возвращает job в queued чистым (pid/run_id NULL); повторный claim стартует
|
||||
штатно (AC-6).
|
||||
module: tests/test_orch126_queued_stale_run.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: >
|
||||
Анти-регресс: для здорового job (никогда не стартовал) поведение mark_job/claim байт-в-байт;
|
||||
терминальные исходы (done/failed/cancelled) и стамп finished_at не затронуты сбросом ownership.
|
||||
module: tests/test_orch126_queued_stale_run.py
|
||||
expected: PASS
|
||||
Reference in New Issue
Block a user