fix(queue): enforce queued ⇒ no run-ownership invariant (ORCH-126) #145

Merged
admin merged 8 commits from feature/ORCH-126-bug-queued-job-can-keep-stale- into main 2026-06-17 11:56:28 +03:00
Owner

ORCH-126 — гигиена run-ownership строки jobs (трек Bug)

Багфикс контрол-плейна (инцидент ORCH-124/125): при ORCH_SERIAL_GATE_ENABLED=false queued analyst-job'ы зависали навсегда. Job 2286 наблюдался в физически невозможном состоянии status=queued + run_id=759/760 + pid=35/42 + started_at=NULL.

Причина

Ни один путь возврата job в queued (requeue_running_jobs / mark_job('queued') / mark_job_transient / reap_running_job('queued')) не сбрасывал run-ownership (run_id/pid). После рестарта контейнера pid мог быть переиспользован ОС → pid_alive(stale)=True → job-reaper (ORCH-065) Tier-1 «видел живой» фантомный running и при max_concurrency=1 клинил клейм всей общей очереди всех проектов.

Инвариант (adr-0052)

status='queued' ⇒ run_id IS NULL AND pid IS NULL AND started_at IS NULL

Queued-job никогда не несёт run-ownership (история run'а — в agent_runs). Фикс на существующих колонках — схема БД не меняется.

Реализация

  • D1 forward-cleanup — все 4 пути возврата в queued сбрасывают run_id=NULL, pid=NULL той же UPDATE-транзакцией; атомарные status-guard'ы сохранены байт-в-байт.
  • D2 чистый claimclaim_next_job сбрасывает pid/run_id на флипе queued→running (defense-in-depth) → строка несёт pid IS NULL до стампа в _spawn. SELECT-гейт не тронут (offline hot-path).
  • D3 окно _spawn — провал до стампа pid → _drain_once mark_job('queued') → по D1 строка чистая, повторный claim штатный.
  • D4 self-heal + наблюдаемостьdb.sanitize_impossible_queued() при старте (main.lifespan) и на реап-тике (never-raise) санирует «невозможные» queued-строки без миграции; счётчик impossible_queued_total в блоке reaper GET /queue. Kill-switch ORCH_IMPOSSIBLE_QUEUED_SANITIZE_ENABLED (дефолт on; гейтит только D4-sweep, D1-D3 безусловны).
  • D5 — reaper не правится; фикс восстанавливает его предусловие (pid IS NULL → Tier-1 skip). Маркированные инварианты ORCH-065/113/114/099 сохранены.

Инвариант проекта

STAGE_TRANSITIONS / реестр QG_CHECKS / check_* / machine-verdict-ключи / схема БД — байт-в-байт. Для здоровых job'ов и enduro поведение байт-в-байт. Это исправление инварианта данных планировщика, не Quality Gate.

Тесты

tests/test_orch126_queued_stale_run.py — TC-01 обязательный регресс (красный до фикса → зелёный после), TC-02…TC-10. Полный pytest tests/ -q2189 passed.

Доки

docs/architecture/internals.md (раздел инварианта run-ownership), .env.example, CHANGELOG.md; сквозной ADR docs/architecture/adr/adr-0052-queued-job-run-ownership-invariant.md.

🤖 Generated with Claude Code

## ORCH-126 — гигиена run-ownership строки `jobs` (трек Bug) Багфикс контрол-плейна (инцидент **ORCH-124/125**): при `ORCH_SERIAL_GATE_ENABLED=false` queued analyst-job'ы зависали навсегда. Job 2286 наблюдался в **физически невозможном** состоянии `status=queued + run_id=759/760 + pid=35/42 + started_at=NULL`. ### Причина Ни один путь возврата job в `queued` (`requeue_running_jobs` / `mark_job('queued')` / `mark_job_transient` / `reap_running_job('queued')`) **не сбрасывал run-ownership** (`run_id`/`pid`). После рестарта контейнера pid мог быть переиспользован ОС → `pid_alive(stale)=True` → job-reaper (ORCH-065) Tier-1 «видел живой» фантомный `running` и при `max_concurrency=1` клинил клейм **всей** общей очереди всех проектов. ### Инвариант (adr-0052) > `status='queued' ⇒ run_id IS NULL AND pid IS NULL AND started_at IS NULL` Queued-job никогда не несёт run-ownership (история run'а — в `agent_runs`). Фикс на **существующих колонках** — схема БД не меняется. ### Реализация - **D1 forward-cleanup** — все 4 пути возврата в `queued` сбрасывают `run_id=NULL, pid=NULL` той же UPDATE-транзакцией; атомарные `status`-guard'ы сохранены байт-в-байт. - **D2 чистый claim** — `claim_next_job` сбрасывает `pid/run_id` на флипе `queued→running` (defense-in-depth) → строка несёт `pid IS NULL` до стампа в `_spawn`. SELECT-гейт не тронут (offline hot-path). - **D3 окно `_spawn`** — провал до стампа pid → `_drain_once` `mark_job('queued')` → по D1 строка чистая, повторный claim штатный. - **D4 self-heal + наблюдаемость** — `db.sanitize_impossible_queued()` при старте (`main.lifespan`) и на реап-тике (never-raise) санирует «невозможные» queued-строки **без миграции**; счётчик `impossible_queued_total` в блоке `reaper` `GET /queue`. Kill-switch `ORCH_IMPOSSIBLE_QUEUED_SANITIZE_ENABLED` (дефолт on; гейтит только D4-sweep, D1-D3 безусловны). - **D5** — reaper не правится; фикс восстанавливает его предусловие (`pid IS NULL` → Tier-1 skip). Маркированные инварианты ORCH-065/113/114/099 сохранены. ### Инвариант проекта `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / `check_*` / machine-verdict-ключи / **схема БД** — байт-в-байт. Для здоровых job'ов и enduro поведение байт-в-байт. Это исправление инварианта данных планировщика, **не** Quality Gate. ### Тесты `tests/test_orch126_queued_stale_run.py` — TC-01 обязательный регресс (красный до фикса → зелёный после), TC-02…TC-10. Полный `pytest tests/ -q` — **2189 passed**. ### Доки `docs/architecture/internals.md` (раздел инварианта run-ownership), `.env.example`, `CHANGELOG.md`; сквозной ADR `docs/architecture/adr/adr-0052-queued-job-run-ownership-invariant.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
admin added 4 commits 2026-06-17 11:40:13 +03:00
docs: init ORCH-126 business request
All checks were successful
CI / test (push) Successful in 1m17s
a5f691fc96
analyst(ET): auto-commit from analyst run_id=773
All checks were successful
CI / test (push) Successful in 1m12s
453c5b7d04
architect(ET): auto-commit from architect run_id=774
All checks were successful
CI / test (push) Successful in 1m12s
3fb7bd6e4c
fix(queue): enforce queued ⇒ no run-ownership invariant (ORCH-126)
All checks were successful
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m15s
d7e7a4d817
Queued analyst-jobs hung forever even with ORCH_SERIAL_GATE_ENABLED=false
(incident ORCH-124/125, job 2286: queued + run_id=759/760 + pid=35/42 +
started_at=NULL — physically impossible). No path returning a job to
'queued' reset its run-ownership (run_id/pid); after a container restart a
reused pid made pid_alive(stale)=True, so the job-reaper Tier-1 saw a phantom
'running' and at max_concurrency=1 wedged the claim of the whole shared queue.

Enforce the invariant `status='queued' ⇒ run_id IS NULL AND pid IS NULL AND
started_at IS NULL` on existing columns (no schema change):

- D1 forward-cleanup: requeue_running_jobs / mark_job('queued') /
  mark_job_transient / reap_running_job('queued') reset run_id=NULL, pid=NULL
  in the same UPDATE that clears started_at; atomic status-guards preserved.
- D2 clean claim: claim_next_job resets pid/run_id on the queued->running flip
  (defense-in-depth) so the row carries pid IS NULL until _spawn stamps it.
- D4 self-heal + observability: db.find_impossible_queued_jobs /
  sanitize_impossible_queued run at startup (main.lifespan) and on each reaper
  tick (JobReaper.sanitize_impossible_queued_once, never-raise); counter
  impossible_queued_total in the GET /queue reaper block. Kill-switch
  ORCH_IMPOSSIBLE_QUEUED_SANITIZE_ENABLED (default on; gates only the D4 sweep).
- D5: reaper Tier-1 unchanged — the fix restores its precondition (pid reflects
  THIS run). Marked invariants ORCH-065/113/114/099 preserved.

Tests: tests/test_orch126_queued_stale_run.py (TC-01 mandatory regression
red->green; TC-02..TC-10). Full pytest tests/ -q green (2189 passed).
Docs: internals.md (run-ownership invariant section), .env.example, CHANGELOG;
cross-cutting adr-0052.

Refs: ORCH-126
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
admin added 1 commit 2026-06-17 11:47:06 +03:00
reviewer(ET): auto-commit from reviewer run_id=776
All checks were successful
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m14s
c8632f4b48
admin added 1 commit 2026-06-17 11:48:45 +03:00
test(ORCH-116): test gate PASS for ORCH-126
Some checks failed
CI / test (push) Has been cancelled
CI / test (pull_request) Successful in 1m18s
3b8aca03ee
admin added 1 commit 2026-06-17 11:50:20 +03:00
staging(ORCH-115): staging gate SUCCESS for ORCH-126
All checks were successful
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m12s
aca0466162
admin added 1 commit 2026-06-17 11:56:28 +03:00
deploy(ORCH-036): finalize SUCCESS for ORCH-126
All checks were successful
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m13s
ab157324a7
admin merged commit 8c74430b13 into main 2026-06-17 11:56:28 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/orchestrator#145