Files
orchestrator/docs/ORCH-1_JOB_QUEUE.md
Dev Agent 4be168c0ec docs(queue): document job queue, /queue, env vars (ORCH-1)
ARCHITECTURE job-queue section + flow diagram, README /queue endpoint and
ORCH_MAX_CONCURRENCY/ORCH_QUEUE_POLL_INTERVAL, new docs/ORCH-1_JOB_QUEUE.md.
2026-06-02 23:58:44 +03:00

4.9 KiB
Raw Blame History

ORCH-1 (F-2b): Persistent Job Queue

Дата: 2026-06-02 Ветка: feature/ORCH-1-job-queue Источник: AUDIT_2026-06-02 (B-2 / F-2b)

Проблема

Агенты запускались in-process: launcher.launch() синхронно спавнил subprocess.Popen + 2 daemon-thread (_watchdog, _monitor_agent) прямо в процессе uvicorn, из 8 webhook-точек. Последствия:

  • Рестарт = катастрофа. daemon-threads умирают, claude-процессы → сироты, работа теряется (M-1 лишь помечал exit=-1 и звал человека).
  • Нет лимита параллелизма — N webhook'ов = N одновременных claude.
  • Нет ретраев — упавший агент просто мёртв.

Решение

Персистентная очередь задач (SQLite-таблица jobs) + фоновый воркер:

  1. Webhook-хэндлер кладёт job (enqueue_job) → мгновенный ответ 200.
  2. Фоновый воркер (src/queue_worker.py, отдельный daemon-thread) забирает jobs с учётом max_concurrency (claim_next_job, атомарно) и спавнит агента (launcher.launch_job, та же Popen-логика).
  3. По завершении _monitor_agent_finalize_job:
    • exit 0done;
    • exit != 0 & attempts < max_attempts → requeue (queued);
    • exit != 0 & attempts >= max_attemptsfailed + Telegram.

Что изменено

Файл Изменение
src/db.py Таблица jobs + индекс; хелперы enqueue_job, claim_next_job (атомарный), mark_job, count_running_jobs, requeue_running_jobs, get_job, job_status_counts, recent_jobs
src/config.py max_concurrency (env ORCH_MAX_CONCURRENCY, default 1), queue_poll_interval (env ORCH_QUEUE_POLL_INTERVAL, default 2.0)
src/agents/launcher.py launch() → тонкая обёртка над _spawn(); новый launch_job(job); _spawn() (общий, job_id опционально); monitor/watchdog принимают job_id; новый _finalize_job() (статусы + ретраи). 4 внутренних advance-вызова self.launchenqueue_job
src/webhooks/plane.py 4 точки launcher.launchenqueue_job
src/webhooks/gitea.py 4 точки launcher.launchenqueue_job
src/queue_worker.py НОВЫЙQueueWorker (drain loop + max_concurrency + graceful stop)
src/main.py lifespan: queue-recovery (requeue_running_jobs) после M-1, старт/останов воркера; новый GET /queue
tests/test_queue.py НОВЫЙ — 19 тестов (lifecycle, атомарность claim, ретраи, requeue, observability, worker max_concurrency; Popen полностью замокан)

Атомарность claim

SELECT id FROM jobs WHERE status='queued' ORDER BY id LIMIT 1;
UPDATE jobs SET status='running', attempts=attempts+1, started_at=datetime('now')
  WHERE id=? AND status='queued';   -- rowcount==1 => claimed, ==0 => проиграл гонку

Гарантия: один job не выдаётся дважды даже при параллельных тиках воркера (проверено test_concurrent_claims_no_duplicate — 8 потоков, 20 jobs).

Сохранённые фиксы (НЕ сломаны)

  • B-1 task-file write (direct open() в worktree) — без изменений.
  • B-2 Popen → log_fh (no PIPE), monitor reap — без изменений, только обёрнут.
  • M-1 orphan-recovery в main.py — оставлен, queue-recovery добавлен ПОСЛЕ него.
  • ORCH-2 worktree per task — без изменений.
  • ORCH-6 project registry/filter — без изменений.

Acceptance

# Проверка Статус
1 webhook кладёт job (queued) enqueue_job
2 воркер исполняет queued→running→done worker + _finalize_job
3 running ≤ max_concurrency test_worker_respects_max_concurrency
4 ретрай fail→queued→failed+notify test_finalize_job_requeue_then_fail
5 рестарт-safe (running→requeue) requeue_running_jobs + lifespan
6 M-1 не сломан оставлен в lifespan
7 тесты (new green, 9 pre-existing) 76 passed / 9 pre-existing
8 /queue counts + recent

Тесты

IMG=$(docker inspect orchestrator --format '{{.Config.Image}}')
docker run --rm -v /home/slin/repos/orchestrator:/code -w /code \
  --entrypoint python3 $IMG -m pytest tests/ -q
# 76 passed, 9 failed (pre-existing test_webhooks 401/signature/TypeError)