fix(queue): enforce queued ⇒ no run-ownership invariant (ORCH-126) #145
Reference in New Issue
Block a user
Delete Branch "feature/ORCH-126-bug-queued-job-can-keep-stale-"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
ORCH-126 — гигиена run-ownership строки
jobs(трек Bug)Багфикс контрол-плейна (инцидент ORCH-124/125): при
ORCH_SERIAL_GATE_ENABLED=falsequeued 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)
Queued-job никогда не несёт run-ownership (история run'а — в
agent_runs). Фикс на существующих колонках — схема БД не меняется.Реализация
queuedсбрасываютrun_id=NULL, pid=NULLтой же UPDATE-транзакцией; атомарныеstatus-guard'ы сохранены байт-в-байт.claim_next_jobсбрасываетpid/run_idна флипеqueued→running(defense-in-depth) → строка несётpid IS NULLдо стампа в_spawn. SELECT-гейт не тронут (offline hot-path)._spawn— провал до стампа pid →_drain_oncemark_job('queued')→ по D1 строка чистая, повторный claim штатный.db.sanitize_impossible_queued()при старте (main.lifespan) и на реап-тике (never-raise) санирует «невозможные» queued-строки без миграции; счётчикimpossible_queued_totalв блокеreaperGET /queue. Kill-switchORCH_IMPOSSIBLE_QUEUED_SANITIZE_ENABLED(дефолт on; гейтит только D4-sweep, D1-D3 безусловны).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; сквозной ADRdocs/architecture/adr/adr-0052-queued-job-run-ownership-invariant.md.🤖 Generated with Claude Code
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>