diff --git a/README.md b/README.md index 9b47539..f3c2f4b 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,12 @@ uvicorn src.main:app --reload --port 8500 | `ORCH_DB_PATH` | SQLite path | `/app/data/orchestrator.db` | | `ORCH_MAX_CONCURRENCY` | Сколько jobs воркер запускает параллельно (ORCH-1) | `1` | | `ORCH_QUEUE_POLL_INTERVAL` | Период опроса очереди воркером, сек (ORCH-1) | `2.0` | +| `ORCH_PREFLIGHT_CACHE_TTL` | Кэш preflight (CLI/net), сек (ORCH-1 resilience) | `45` | +| `ORCH_BACKOFF_BASE_SECONDS` | База exp-backoff для transient (429) | `10` | +| `ORCH_BACKOFF_MAX_SECONDS` | Потолок backoff | `600` | +| `ORCH_TRANSIENT_MAX_ATTEMPTS` | Ретраи для 429/недоступности | `5` | +| `ORCH_BREAKER_THRESHOLD` | transient подряд до открытия breaker | `3` | +| `ORCH_BREAKER_PAUSE_SECONDS` | Пауза при открытом breaker | `300` | ## Очередь задач (ORCH-1 / F-2b) @@ -128,7 +134,11 @@ Webhook-хэндлеры больше не спавнят claude-агентов потом `failed` + Telegram-нотификация. Статусы job: `queued → running → done | failed`. Наблюдаемость — через `GET /queue`. -Подробности: `docs/ORCH-1_JOB_QUEUE.md`. + +**Resilience-слой:** дешёвый preflight (CLI/net, кэш, без токенов) гейтит claim; +429/overload детектится по логу (transient vs permanent), transient ретраится с +exp-backoff (`available_at`, Retry-After); circuit breaker паузит воркер после N +transient подряд. Подробности: `docs/ORCH-1_JOB_QUEUE.md`. ## Multi-repo: реестр проектов (ORCH-6) diff --git a/docs/ORCH-1_JOB_QUEUE.md b/docs/ORCH-1_JOB_QUEUE.md index eeed34c..629751d 100644 --- a/docs/ORCH-1_JOB_QUEUE.md +++ b/docs/ORCH-1_JOB_QUEUE.md @@ -79,5 +79,49 @@ UPDATE jobs SET status='running', attempts=attempts+1, started_at=datetime('now' 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) +# 110 passed, 9 failed (pre-existing test_webhooks 401/signature/TypeError) ``` + +--- + +## Resilience-слой (ДОПОЛНЕНИЕ: preflight + 429 + backoff + circuit breaker) + +Надёжность очереди против недоступности CLI и rate-limit. Два РАЗНЫХ класса +проблем лечатся по-разному. + +### A. Дешёвый preflight (`src/preflight.py`) — не жжёт токены +Перед claim воркер проверяет: `os.path.exists(CLAUDE_BIN)` + `claude --version` +(timeout 5с, токены НЕ тратит). Результат кэшируется `preflight_cache_ttl` (45с). +FAIL → воркер НЕ claim’ит (job остаётся `queued`), ждёт. 🚫 НЕТ prompt-ping. + +### B. 429 — детект НА ВЫХОДЕ (`src/error_classifier.py`) +rate-limit нельзя предсказать — классифицируем по логу прогона. `classify_log_file` +читает хвост лога (16KB), ищет `429/rate limit/overloaded/quota/503/529/timeout/...` +→ `transient` или `permanent`. Извлекает `Retry-After`. + +- **transient** (429/сеть) → backoff-ретрай с ОТДЕЛЬНЫМ `transient_attempts` + (лимит `transient_max_attempts=5`) — не жжёт code-fault бюджет. +- **permanent** (code-fault) → обычные `attempts < max_attempts` (2), потом `failed`. + +### C. Backoff + `available_at` +Колонки `jobs.available_at TEXT` + `jobs.transient_attempts INTEGER` (миграция +`_ensure_column`). `claim_next_job`: `WHERE status='queued' AND (available_at IS NULL +OR available_at <= datetime('now'))`. При transient: `available_at = now + +min(2^n * base, max)` (base=10с, max=600с), `Retry-After` уважается (берёмся max). + +### D. Circuit breaker (`CircuitBreaker` в queue_worker) +N=3 transient подряд → **open**: воркер паузит `breaker_pause_seconds=300`, ВООБЩЕ +не дёргает CLI, Telegram-алерт. Через паузу → **half-open** (пробует 1 job); +ожил (exit 0) → **closed**; снова transient → опять open. Состояние в памяти +воркера, отражается в `/queue.resilience`. +Связь launcher→breaker — через callback `launcher.on_outcome` (без import-цикла). + +### Конфиг (config.py) +`preflight_cache_ttl=45`, `backoff_base_seconds=10`, `backoff_max_seconds=600`, +`transient_max_attempts=5`, `breaker_threshold=3`, `breaker_pause_seconds=300`. + +### Тесты +`tests/test_resilience.py` — 34 теста: preflight (FAIL→queued, кэш, force), +классификатор (transient/permanent/Retry-After), backoff (рост/cap/Retry-After, +`available_at` гейтинг), launcher transient/permanent finalize, breaker +(open/half-open/closed/re-open, блок claim).