architect(ET): auto-commit from architect run_id=774
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:
@@ -1269,7 +1269,11 @@ ORCH-065 вводит фоновый watchdog, чтобы смерть проц
|
||||
- **Job-reaper** (`src/job_reaper.py`) — daemon-поток по образцу `reconciler`,
|
||||
работает **без рестарта**. Трёхуровневая liveness: Tier-1 мёртвый `jobs.pid`
|
||||
(новая колонка) после `reaper_dead_ticks` подряд тиков (анти-ложноположительность
|
||||
— живой долгий агент не реапится); Tier-2 `agent_runs.exit_code` записан, а job
|
||||
— живой долгий агент не реапится; `pid IS NULL`/живой → streak сбрасывается, не
|
||||
реапит). **Предусловие Tier-1 (ORCH-126, adr-0052):** `jobs.pid` отражает процесс
|
||||
ИМЕННО текущего run'а — обеспечивается инвариантом «`queued ⇒ run_id/pid IS NULL`»
|
||||
(queued-job не несёт stale pid; переиспользованный pid иначе дал бы фантомный
|
||||
«живой» `running`, клинящий очередь). Tier-2 `agent_runs.exit_code` записан, а job
|
||||
ещё `running` — но это окно неоднозначно (живой monitor пишет exit_code ПЕРВЫМ,
|
||||
затем git push/PR/Plane-комментарии), поэтому Tier-2 реапит только после
|
||||
finalization-grace `reaper_finalize_grace_s` (живой финализирующий monitor НЕ
|
||||
@@ -1436,7 +1440,7 @@ Monitoring after Deploy → Done
|
||||
- `events` — входящие вебхуки (дедуп)
|
||||
- `tasks` — задачи и их стадии; колонки `cancelled_at`/`cancel_requested_at` (ORCH-090) — durable-метки STOP-отмены (вторая — отложенная отмена в критичном окне merge/deploy). Терминальная стадия `cancelled` (сток, параллельно `done`); натуральные ключи отменённой строки тумбстонятся суффиксом `#cancelled-<id>` (`plane_id`/`work_item_id`/`plane_issue_id`). Колонка `paused_at` (ORCH-124, adr-0051) — durable per-task park-сигнал serial-gate (NULL = не на паузе): **ортогональная** оси «терминальность» ось «пауза» (`paused_at IS NOT NULL`), читается **только** serial-gate (`task_deps`/`stages.py` её не читают); паузнутый предшественник не держит FIFO, но не обходит `repo_freeze`/`task_deps`
|
||||
- `agent_runs` — запуски агентов (run_id, usage, cost)
|
||||
- `jobs` — очередь задач (ORCH-1); статусы `queued|running|done|failed|cancelled` (ORCH-090: `cancelled` — терминальный исход STOP, нигде не реквью'ится); колонка `pid` (ORCH-065) — pid агентского процесса для liveness-детекции зомби job-reaper'ом
|
||||
- `jobs` — очередь задач (ORCH-1); статусы `queued|running|done|failed|cancelled` (ORCH-090: `cancelled` — терминальный исход STOP, нигде не реквью'ится); колонка `pid` (ORCH-065) — pid агентского процесса для liveness-детекции зомби job-reaper'ом. **Инвариант run-ownership (ORCH-126, adr-0052): `status='queued' ⇒ run_id IS NULL AND pid IS NULL AND started_at IS NULL`** — queued-job никогда не несёт run-ownership (она принадлежит ровно одной активной попытке `running` после стампа в `_spawn`; история — в `agent_runs`). Все пути возврата в `queued` (`requeue_running_jobs`/`mark_job('queued')`/`mark_job_transient`/`reap_running_job('queued')`) сбрасывают `run_id`/`pid`; `claim_next_job` — defense-in-depth-сброс при флипе в `running`. Stale run-ownership искажала бы Tier-1 liveness reaper'а (переиспользованный pid → фантомный `running` клинит `max_concurrency=1`-очередь всех проектов) и `/metrics` (ORCH-099). «Невозможные» строки само-лечатся при старте/реапе + счётчик в `GET /queue`. **Норматив:** новый путь возврата в `queued` обязан соблюсти инвариант (reviewer: нарушение = ≥P1)
|
||||
- `job_deps` — декларативные зависимости задач (ORCH-026, Уровень B): `(task_id, depends_on_task_id)`, аддитивная; источник истины планировщика для гейта «B ждёт A»
|
||||
- `repo_freeze` — durable per-repo rollback-freeze (ORCH-088, FR-5): `(id, repo, frozen_at, reason, work_item_id, cleared_at)`, аддитивная append-only; активный freeze ⇔ строка репо с `cleared_at IS NULL`. Выставляется post-deploy `DEGRADED` (`set_repo_freeze`), снимается вручную (`POST /serial-gate/unfreeze` → `cleared_at=now`). Гейтит serial-claim безусловно (деградировавшая задача уже `done`)
|
||||
- `lessons` — машинный журнал отклонений конвейера (ORCH-098, FR-1): `(id, created_at, updated_at, lesson_type, work_item_id, task_id, stage, agent, repo, root_cause, suggestion, status, related_task, attribution, target_repo, target_domain, source, detail)`, аддитивная идемпотентная (`CREATE TABLE IF NOT EXISTS` + три индекса); колонки атрибуции (`attribution`/`target_repo`/`target_domain`) — нуллабельны и присутствуют сразу (NFR-6), без `enum`-констрейнтов (слаги forward-compatible). Автозапись 4 типов (`gate_failure`/`merge_hold`/`transient_retry`/`deploy_degraded`, `source="auto"`, дедуп в окне `lessons_dedup_window_s`) + ручная (`source="manual"`); observer-only (не участвует в решении гейта). Leaf `src/lessons.py` never-raise, kill-switch `lessons_enabled` (без `*_repos` — журнал не скоупится по репо, репо-разрез на выборке)
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
work_item: ORCH-126
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: accepted
|
||||
created_at: 2026-06-17
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# adr-0052: Инвариант run-ownership строки `jobs` — «queued ⇒ run_id/pid/started_at IS NULL»
|
||||
|
||||
- **Статус:** accepted
|
||||
- **Дата:** 2026-06-17
|
||||
- **Задача:** ORCH-126 (bug-fix контрол-плейна)
|
||||
- **Детальный ADR:** `docs/work-items/ORCH-126/06-adr/ADR-001-queued-job-run-ownership-hygiene.md`
|
||||
|
||||
## Контекст
|
||||
|
||||
Колонки `jobs.run_id` и `jobs.pid` — **общий контракт liveness/идентичности run'а**, на который
|
||||
опираются несколько подсистем контрол-плейна:
|
||||
- **job-reaper (ORCH-065, adr-0011/adr-0043):** Tier-1 судит liveness running-job'а по `jobs.pid`
|
||||
(`merge_gate.pid_alive`);
|
||||
- **`/metrics` (ORCH-099, adr-0030):** `get_running_agents` отдаёт `run_id`/`pid` running-job'ов
|
||||
как «сырьё» для sidecar;
|
||||
- **scheduler/launcher (ORCH-1/ORCH-088):** `_spawn` выставляет `run_id` (после INSERT в `agent_runs`)
|
||||
и `pid` (после `Popen`) **вперёд**.
|
||||
|
||||
Но ни один путь возврата job'а в `queued` (restart-recovery `requeue_running_jobs`,
|
||||
retry `mark_job('queued')`, transient `mark_job_transient`, reaper `reap_running_job('queued')`) не
|
||||
сбрасывал run-ownership — он оставался «протухшим» от прошлой попытки. Возникало физически невозможное
|
||||
состояние `status='queued'` с непустыми `run_id`/`pid` при `started_at IS NULL`. Поскольку pid после
|
||||
рестарта контейнера может быть **переиспользован** ОС, `pid_alive(stale)` ложно возвращает `True`,
|
||||
reaper видит «живой» фантомный `running` и при `max_concurrency=1` (дефолт) клинит клейм **всей**
|
||||
очереди — а это **общий** инстанс/очередь всех проктов (self-hosting). Инцидент ORCH-124/125: queued
|
||||
analyst-job'ы зависали навсегда даже при `ORCH_SERIAL_GATE_ENABLED=false`.
|
||||
|
||||
Корень — **отсутствие именованного, принудительно соблюдаемого инварианта**, связывающего
|
||||
`jobs.status` с его run-ownership-колонками.
|
||||
|
||||
## Решение
|
||||
|
||||
Зафиксировать как **системный инвариант данных контрол-плейна**:
|
||||
|
||||
> **`status='queued' ⇒ run_id IS NULL AND pid IS NULL AND started_at IS NULL`.**
|
||||
|
||||
То есть: **queued-job никогда не несёт run-ownership.** Run-ownership принадлежит ровно одной активной
|
||||
попытке (`running` после стампа в `_spawn`) и история живёт в таблице `agent_runs`, а не в
|
||||
`jobs.run_id`.
|
||||
|
||||
Соблюдение (ORCH-126, без смены схемы БД, на существующих колонках):
|
||||
- **Forward-cleanup:** каждый путь перехода в `queued` выставляет `run_id=NULL, pid=NULL` той же
|
||||
UPDATE-транзакцией, что чистит `started_at`/`finished_at` (атомарные `status`-guard'ы сохранены).
|
||||
- **Clean claim (defense-in-depth):** `claim_next_job` при флипе `queued→running` сбрасывает stale
|
||||
`pid`/`run_id` тем же UPDATE — между claim и стампом `pid` в `_spawn` строка несёт `pid IS NULL`,
|
||||
не чужой pid (offline hot-path не трогается).
|
||||
- **Self-heal + наблюдаемость:** «невозможные» queued-строки санируются идемпотентно при старте/реапе
|
||||
(never-raise) и видны счётчиком в `GET /queue` — защита от рецидива, если будущий путь возврата в
|
||||
`queued` забудет инвариант.
|
||||
|
||||
**Норматив на будущее:** любой новый путь, переводящий job в `queued`, **обязан** соблюсти инвариант
|
||||
(сбросить `run_id`/`pid`). Reviewer ловит нарушение как ≥P1 (фантомный `running` способен заклинить
|
||||
очередь всех проектов).
|
||||
|
||||
`STAGE_TRANSITIONS` / реестр `QG_CHECKS` / `check_*` / machine-verdict-ключи / **схема БД** —
|
||||
байт-в-байт. Это инвариант данных планировщика, **не** Quality Gate и **не** стадия.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **DB-level CHECK/триггер** — отвергнуто: смена схемы; раняющий констрейнт нарушает never-raise и мог
|
||||
бы заклинить очередь всех проектов. Инвариант лучше держать кодом + self-heal, чем раняющим
|
||||
констрейнтом.
|
||||
- **Reaper-side эвристика поверх stale pid** — отвергнуто: лечит симптом у одного читателя, оставляет
|
||||
stale-данные другим (`/metrics`); reaper уже корректно трактует `pid IS NULL`.
|
||||
- **Новая колонка-эпоха run'а** — отвергнуто: смена схемы, избыточно; инвариант выразим на
|
||||
существующих колонках.
|
||||
|
||||
## Последствия
|
||||
|
||||
- Класс «фантомный `running` клинит `max_concurrency=1`-очередь всех проектов» закрыт у корня;
|
||||
восстановлена корректность Tier-1 reaper-liveness; чище `/metrics`.
|
||||
- Инвариант **назван** → перестаёт быть «неявным предположением» reaper'а/metrics и становится
|
||||
проверяемым контрактом (reviewer + self-heal).
|
||||
- Нулевая регрессия для здоровых job'ов и enduro-trails; миграция БД не требуется (аномальные строки
|
||||
санируются при первом старте).
|
||||
- Аддитивно/обратимо: **не** `arch:major-change` (нет новой стадии / QG / таблицы / смены топологии).
|
||||
- **Откат:** ревертом ORCH-126 PR; опц. self-heal/диагностика — своим флагом.
|
||||
|
||||
## Связи
|
||||
|
||||
- adr-0011 / `docs/work-items/ORCH-065/06-adr/` (job-reaper Tier-1 по `jobs.pid` — читатель инварианта;
|
||||
фикс восстанавливает его предусловие).
|
||||
- adr-0043 / `docs/work-items/ORCH-113/06-adr/` (finalizer-liveness — ортогонален: process-local,
|
||||
по `job_id`).
|
||||
- adr-0045 / `docs/work-items/ORCH-114/06-adr/` (transition-lease — ортогонален: своя таблица/колонки,
|
||||
recovery по boot-id).
|
||||
- adr-0030 / `docs/work-items/ORCH-099/06-adr/` (`/metrics` `get_running_agents` — читатель `pid`/
|
||||
`run_id`; уже допускает `pid IS NULL`).
|
||||
- adr-0002 (job-queue ORCH-1 — порождающая модель `jobs`).
|
||||
</content>
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
work_item: ORCH-126
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: accepted
|
||||
created_at: 2026-06-17
|
||||
model_used: claude-opus-4-8
|
||||
track: bug
|
||||
---
|
||||
|
||||
# ADR-001: Гигиена run-ownership строки `jobs` — инвариант «queued ⇒ нет run-ownership»
|
||||
|
||||
Work Item: **ORCH-126** — queued-job хранит протухший `run_id`/`pid` и не клеймится даже при выключенном serial-gate
|
||||
Стадия: **architecture**
|
||||
Сквозная регистрация: **`docs/architecture/adr/adr-0052-queued-job-run-ownership-invariant.md`** (решение кросс-каттинговое — инвариант данных, на который опираются reaper / `/metrics` / scheduler).
|
||||
|
||||
> 🐞 **Контекст маршрута.** Задача классифицирована как **Bug** (трек ORCH-019); аналитик отметил, что
|
||||
> ADR/макет «не требуются». Однако задача дошла до стадии `architecture` (bug-fast-track не сработал —
|
||||
> метка `Bug` не была проставлена в Plane на момент `start_pipeline`, либо флаг выключен), а
|
||||
> детерминированный exit-гейт `check_architecture_done` требует артефакт `06-adr/` **или**
|
||||
> `07-infra-requirements.md`. Поэтому фиксирую **минимальный, но настоящий** архитектурный артефакт:
|
||||
> правка трогает **4 маркированных инварианта** (ORCH-065 / ORCH-113 / ORCH-114 / ORCH-099), и их
|
||||
> валидация (CLAUDE.md §9) — это именно архитектурная работа. **Не** `arch:major-change` (нет новой
|
||||
> стадии / компонента / QG / смены БД).
|
||||
|
||||
## Статус
|
||||
Accepted
|
||||
|
||||
## Контекст
|
||||
|
||||
Корневая проблема — **отсутствие принудительного инварианта**, связывающего `jobs.status` с
|
||||
колонками run-ownership `jobs.run_id` / `jobs.pid` / `jobs.started_at`. Run-ownership выставляется
|
||||
**вперёд** в `launcher._spawn` (`run_id` после INSERT в `agent_runs`, `pid` после `Popen`,
|
||||
`launcher.py:711`), но **ни один** из путей возврата job'а в `queued` его не сбрасывает. Сверено по коду:
|
||||
|
||||
| # | Путь (`src/db.py`) | Что чистит | `run_id`/`pid` |
|
||||
|---|--------------------|-----------|----------------|
|
||||
| 1 | `requeue_running_jobs()` (`:1483`, restart-recovery) | `started_at` | **НЕ чистит** |
|
||||
| 2 | `mark_job(status='queued')` (`:1264`) | `started_at`/`finished_at` | **НЕ чистит** |
|
||||
| 3 | `mark_job_transient()` (`:1226`) | `started_at`/`finished_at` | **НЕ чистит** |
|
||||
| 4 | `reap_running_job(status='queued')` (`:1648`) | `started_at`/`finished_at` | **НЕ чистит** |
|
||||
| 5 | `claim_next_job()` (`:1196`, флип `queued→running`) | ставит `started_at`, `attempts++` | **НЕ сбрасывает** stale `pid` |
|
||||
|
||||
Итог — `queued`-строка может нести «протухшую» run-ownership: физически невозможное состояние
|
||||
(run-ownership выставлен, но запуск не состоялся: `run_id`/`pid` ≠ NULL при `started_at IS NULL` —
|
||||
ровно наблюдаемое в инциденте: job 2286 `queued + run_id=759/760 + pid=35/42`).
|
||||
|
||||
Колонки `run_id`/`pid` — **общий контракт liveness**, который читают два подсистемы:
|
||||
- **job-reaper (ORCH-065):** Tier-1 судит liveness по `jobs.pid` через `merge_gate.pid_alive`
|
||||
(`job_reaper.py:245`). Stale-**но-переиспользованный** pid → `pid_alive(stale)=True` → reaper
|
||||
«видит живой процесс» → **никогда не реапит** фантомный `running`; при `max_concurrency=1` (дефолт)
|
||||
это клинит клейм **всей** очереди (общий инстанс/очередь всех проектов).
|
||||
- **`/metrics` (ORCH-099):** `get_running_agents` отдаёт `run_id`/`pid` running-job'ов.
|
||||
|
||||
«Как есть» не годится: без сброса run-ownership на возврате в `queued` сигналы liveness/диагностики
|
||||
искажены, и при выключенном serial-gate срочная задача всё равно не стартует.
|
||||
|
||||
## Решение
|
||||
|
||||
### Сводка
|
||||
Ввести и **повсеместно соблюсти** инвариант жизненного цикла строки `jobs`:
|
||||
|
||||
> **`status='queued' ⇒ run_id IS NULL AND pid IS NULL AND started_at IS NULL`.**
|
||||
|
||||
Сбрасывать run-ownership (`run_id=NULL, pid=NULL`) той же UPDATE-транзакцией, что уже чистит
|
||||
`started_at`, на **всех** путях возврата в `queued`; в `claim` — defense-in-depth-сброс stale `pid`/
|
||||
`run_id` при флипе в `running` (до стампа в `_spawn`). Плюс — детект/санация «невозможного» состояния
|
||||
(старт + реап-тик) и наблюдаемость. **Схема БД, `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`,
|
||||
machine-verdict-ключи — байт-в-байт.** Это исправление инварианта данных планировщика, **не** новая
|
||||
фича и **не** Quality Gate.
|
||||
|
||||
### D1 — Forward-cleanup на всех путях возврата в `queued` (FR-1 / AC-1)
|
||||
В `requeue_running_jobs` / `mark_job('queued')` / `mark_job_transient` / `reap_running_job('queued')`
|
||||
добавить `run_id = NULL, pid = NULL` в тот же `SET`, что чистит `started_at`/`finished_at`. Атомарные
|
||||
guard'ы по `status` (`reap_running_job ... WHERE status='running'`, rowcount-проверка) — **сохранить
|
||||
байт-в-байт** (restart-safe, гонка worker↔reaper↔monitor). Точка сброса — **строго на переходе В
|
||||
`queued`**: сброс run-ownership активного `running`-job стёр бы идентичность живого run'а (ключевое
|
||||
ограничение корректности — см. TR-1).
|
||||
|
||||
### D2 — Чистый claim (FR-2 / AC-3)
|
||||
`claim_next_job` при флипе `queued→running` сбрасывает `pid=NULL, run_id=NULL` тем же UPDATE
|
||||
(defense-in-depth поверх D1) — между claim и стампом `pid` в `_spawn` строка несёт `pid IS NULL`, а
|
||||
не чужой pid. SELECT-гейт (`status='queued' AND available_at<=now` + dep-/serial-gate) — **не трогать**
|
||||
(offline hot-path, NFR-2). Сброс — часть уже существующего `UPDATE … SET status='running', …` (без
|
||||
нового SELECT/сети).
|
||||
|
||||
### D3 — Окно `_spawn` (FR-3 / AC-6)
|
||||
При провале `_spawn` **до** стампа `pid` (`ensure_worktree` / `_materialize_deferred_branch` /
|
||||
`_write_task_file`) обработчик `queue_worker._drain_once` возвращает job через `mark_job('queued'|
|
||||
'failed')` → по D1 строка остаётся без stale `pid`/`run_id`. Новый код в launcher не нужен —
|
||||
устойчивость обеспечивается D1; в development подтвердить, что повторный claim после такого провала
|
||||
стартует штатно.
|
||||
|
||||
### D4 — Детект и санация «невозможного» состояния (FR-4 / AC-5)
|
||||
«Невозможное» = `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` (по D1), never-raise. Закрывает уже-существующие
|
||||
аномальные строки на проблемной БД без миграции.
|
||||
- **Наблюдаемость** — структурный WARNING (`job_id`/`run_id`/`pid`) + read-only счётчик в блоке
|
||||
очереди `GET /queue` (поле/`queue.impossible_queued`); существующие поля снимка не переименовывать.
|
||||
|
||||
### D5 — Корректность reaper-liveness (FR-5 / AC-4) — валидация, не правка
|
||||
После D1–D3 reaper на свежеклеймленном `running` видит `pid IS NULL` → Tier-1 (`job_reaper.py:245`:
|
||||
`if pid is not None and not pid_alive(pid)`) **пропускает** строку, сбрасывает streak
|
||||
(`:257-258` «Alive / no pid → reset»); Tier-3 backstop (`reaper_max_running_s`) — без изменений
|
||||
ловит реально застрявший claim→spawn в ограниченное время. **Правка reaper'а не требуется** — фикс
|
||||
**восстанавливает предусловие**, на котором reaper уже спроектирован («`pid` отражает процесс ЭТОГО
|
||||
run'а»).
|
||||
|
||||
### D6 — Без kill-switch для базового сброса (D1–D3); опц. флаг для D4
|
||||
Базовый сброс run-ownership (D1–D3) — **безусловен** (исправление инварианта данных, для здоровых
|
||||
job'ов байт-в-байт). Опциональную авто-санацию/диагностику D4 допустимо закрыть флагом (дефолт =
|
||||
включено) на усмотрение developer. Отдельный фичефлаг для D1–D3 не вводится (NFR-3).
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **DB-level enforcement (CHECK-констрейнт / триггер `status='queued' ⇒ run_id/pid IS NULL`)** —
|
||||
отвергнуто: правка **схемы БД** (вне объёма, NFR-3); раняющий констрейнт нарушает never-raise и мог
|
||||
бы заклинить очередь всех проектов; самолечение на старте (D4) безопаснее жёсткого констрейнта.
|
||||
- **Только reaper-side эвристика (игнорировать `pid`, если `started_at` подозрителен)** — отвергнуто:
|
||||
не лечит корень — другие читатели (`/metrics`) по-прежнему видят stale-данные; reaper уже корректно
|
||||
обрабатывает `pid IS NULL` — правильнее **гарантировать** NULL, а не плодить эвристики в reaper'е.
|
||||
- **Новая колонка (`run_epoch`/`claim_token`)** — отвергнуто: смена схемы (вне объёма), избыточно —
|
||||
инвариант выразим на существующих колонках.
|
||||
- **Сброс run-ownership где угодно (в т.ч. у активного `running`)** — отвергнуто: стёр бы идентичность
|
||||
живого run'а; сброс строго на переходе В `queued` и в claim ДО `_spawn`.
|
||||
|
||||
## Последствия
|
||||
|
||||
- **+** Закрыт класс «фантомный `running` клинит `max_concurrency=1` очередь всех проектов»;
|
||||
восстановлена корректность Tier-1 liveness reaper'а; чище `/metrics`.
|
||||
- **+** Инвариант «queued = без run-ownership» **назван и зафиксирован** (этот ADR + сквозной
|
||||
adr-0052) → защита от рецидива (новый 6-й путь возврата в `queued` обязан его соблюсти; D4
|
||||
само-лечит пропуск).
|
||||
- **+** Для не-stale job'ов поведение байт-в-байт (NFR-3); enduro-trails не затронут;
|
||||
миграция БД не требуется.
|
||||
- **−** 4–5 точек правки → риск забыть будущий путь возврата (митигейшн: D4 startup/reap self-heal +
|
||||
счётчик в `/queue` + named-инвариант) — **TR-2**.
|
||||
- **−** Точка сброса критична: ошибочный сброс у активного `running` стёр бы идентичность live-run'а
|
||||
(митигейшн: строго на переходе В `queued` / в claim ДО `_spawn`) — **TR-1**.
|
||||
- **Откат:** изменения локальны (наборы `SET` в `src/db.py` + опц. startup-хук) → ревертом PR;
|
||||
опц. D4 — выключением его флага.
|
||||
|
||||
### Трассировка маркированных инвариантов (CLAUDE.md §9) — все сохранены
|
||||
- **ORCH-065 (reaper Tier-1):** сохранён и **восстановлен** (`job_reaper.py:245/257`); Tier-3 backstop
|
||||
без изменений.
|
||||
- **ORCH-113 (finalizer-liveness):** ортогонален — process-local маркер по `job_id`, только для
|
||||
`running`+exit_code на ребре `deploy-staging`; у queued-job финализатора нет.
|
||||
- **ORCH-114 (transition-lease / `recover_on_startup`):** ортогонален — своя таблица
|
||||
`transition_lease`, свои `owner_pid`/`owner_boot_id`; recovery по boot-id, не по `jobs.pid`/`run_id`.
|
||||
- **ORCH-099 (`/metrics`):** улучшен — `get_running_agents` фильтрует `status='running'` и уже
|
||||
допускает `pid IS NULL`; фикс убирает утечку stale-pid, контракт держится.
|
||||
|
||||
## Ссылки
|
||||
- BRD: `docs/work-items/ORCH-126/01-brd.md`
|
||||
- TRZ: `docs/work-items/ORCH-126/02-trz.md`
|
||||
- Acceptance: `docs/work-items/ORCH-126/03-acceptance-criteria.md`
|
||||
- Test-plan: `docs/work-items/ORCH-126/04-test-plan.yaml`
|
||||
- Технические риски: `docs/work-items/ORCH-126/10-tech-risks.md`
|
||||
- Сквозной ADR: `docs/architecture/adr/adr-0052-queued-job-run-ownership-invariant.md`
|
||||
- Сверено по коду: `src/db.py` (`claim_next_job`/`requeue_running_jobs`/`mark_job`/`mark_job_transient`/`reap_running_job`/`get_running_agents`), `src/job_reaper.py` (`:245`/`:257`), `src/agents/launcher.py` (`_spawn` `:711`), `src/transition_lease.py`
|
||||
- Маркированные инварианты: `docs/work-items/ORCH-065/06-adr/`, `ORCH-113/06-adr/`, `ORCH-114/06-adr/`, `ORCH-099/06-adr/`
|
||||
</content>
|
||||
</invoke>
|
||||
41
docs/work-items/ORCH-126/10-tech-risks.md
Normal file
41
docs/work-items/ORCH-126/10-tech-risks.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
work_item: ORCH-126
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: accepted
|
||||
created_at: 2026-06-17
|
||||
model_used: claude-opus-4-8
|
||||
track: bug
|
||||
---
|
||||
|
||||
# 10 — Технические риски: ORCH-126 — гигиена run-ownership queued-job
|
||||
|
||||
Work Item: **ORCH-126** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
> Информационный (гейтом не парсится). Риски реализации сброса `run_id`/`pid` на путях возврата в
|
||||
> `queued` и их митигейшн. На bug-маршруте необязателен, но включён: правка — горячий путь клейма,
|
||||
> затрагивает 4 маркированных инварианта (ORCH-065/113/114/099) на **общей** очереди всех проектов.
|
||||
|
||||
## Реестр рисков
|
||||
|
||||
| ID | Риск | Вер. | Влия. | Митигейшн |
|
||||
|----|------|------|-------|-----------|
|
||||
| TR-1 | **Сброс run-ownership не в той точке** — обнуление `run_id`/`pid` у **активного** `running`-job стёрло бы идентичность живого run'а (reaper/`/metrics` потеряли бы pid живого процесса; зомби-детекция сломалась бы). | Низ. | Выс. | Сброс **строго на переходе В `queued`** (D1) и в `claim` **до** `_spawn` (D2). Активный `running` не трогается. TC-10 (анти-регресс здорового job'а) + TC-04 (атомарный `WHERE status='running'`-guard сохранён). |
|
||||
| TR-2 | **Забытый будущий путь возврата в `queued`** (6-й путь мимо инварианта) воскрешает класс «stale run-ownership». | Сред. | Сред. | Named-инвариант (adr-0052) + D4 startup/reap self-heal (идемпотентно лечит пропуск) + счётчик `impossible_queued` в `GET /queue` + reviewer-норматив «нарушение = ≥P1». |
|
||||
| TR-3 | **Регресс reaper Tier-1** — неверная трактовка `pid IS NULL` как «dead → reap» реапнула бы легитимный старт. | Низ. | Выс. | Правка reaper НЕ требуется: `job_reaper.py:245` реапит лишь `pid is not None and not pid_alive(pid)`, `:257` сбрасывает streak при «no pid». Фикс **восстанавливает** предусловие. Покрыто TC-07. |
|
||||
| TR-4 | **Гонка worker↔reaper↔monitor** на возврате в `queued` (двойная обработка строки). | Низ. | Сред. | Атомарные `status`-guard'ы (`reap_running_job ... WHERE status='running'`, rowcount) сохранены байт-в-байт (FR-1). Restart-safe (TC-04 повторный вызов → rowcount 0). |
|
||||
| TR-5 | **Окно claim→spawn без pid** — job, чей `_spawn` упал до стампа pid и не был реквью́ен, висит `running` с `pid IS NULL` до Tier-3 backstop. | Низ. | Низ. | Штатный путь — немедленный реквью через `_drain_once`+D1 (TC-09). Worst-case ловит Tier-3 `reaper_max_running_s` (без изменений). Поведение не хуже текущего. |
|
||||
| TR-6 | **Ошибка в горячем пути клейма роняет/клинит очередь всех проектов** (NFR-1). | Низ. | Выс. | Сброс — часть существующего `UPDATE` (без нового SELECT/сети, offline NFR-2); D4-диагностика изолирована (never-raise) от клейма. Полный `pytest tests/ -q` зелёный (AC-7). |
|
||||
| TR-7 | **Невозможные строки на проде на момент апгрейда** (job 2286/2303) не санируются. | Низ. | Сред. | D4 авто-санация при первом старте `main.lifespan` (миграция не требуется); идемпотентно, выдерживает повторный рестарт (NFR-5). Покрыто TC-08. |
|
||||
|
||||
## Сводный вывод
|
||||
|
||||
Доминирующий класс — **точечная гигиена данных в горячем пути** при сохранении атомарных guard'ов и
|
||||
4 маркированных инвариантов. Все риски — низкой вероятности; высокое влияние (TR-1/TR-3/TR-6) полностью
|
||||
снимается тем, что (а) сброс ограничен переходом В `queued`/claim-до-`_spawn`, (б) reaper не правится и
|
||||
его предусловие лишь восстанавливается, (в) изменения — внутри существующих UPDATE без сети.
|
||||
**Эскалация не требуется** (`arch:major-change` — нет; возврат в анализ — нет): схема БД / `STAGE_TRANSITIONS`
|
||||
/ `QG_CHECKS` / `check_*` / machine-verdict — байт-в-байт, решение реализуемо без нарушения принципов.
|
||||
Остаточный риск для прод-конвейера (self-hosting) — **низкий**; обязательный регресс-тест (TC-01,
|
||||
red→green) + анти-регресс здорового job'а (TC-10) фиксируют корректность.
|
||||
</content>
|
||||
Reference in New Issue
Block a user