docs(resilience): document preflight/429/backoff/breaker + env vars (ORCH-1)
This commit is contained in:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user