[Backend] Provider-aware concurrency caps in the Celery queue #356

Closed
opened 2026-08-25 20:39:18 +00:00 by claude-bot · 1 comment
Contributor

Found in the August 2026 session lifecycle review (#319).

Why

Session processing is bursty and highly correlated — most groups play Friday and Saturday evening, in a couple of timezone clusters. A single Friday night can deliver a large number of tracks into a narrow window.

Every provider has a limit, and they differ: managed APIs publish concurrency or rate limits, a self-hosted llama.cpp instance has a fixed slot count (the current deployment runs --parallel 2), and a CPU deployment is effectively serial. Exceeding any of them produces rate-limit errors or queue collapse rather than graceful slowdown.

Proposed fix

Give each provider adapter a declared concurrency limit and enforce it in the Celery layer, so work queues rather than fails. Prefer a shape where a burst costs latency, not failures — this is asynchronous batch work and nobody needs their recap within 30 seconds.

Surface queue depth and estimated wait so a GM whose session lands in a busy window sees "queued, about 20 minutes" rather than silence. That connects to the async-feedback work in v4.3.0.

Acceptance criteria

  • Each adapter declares a concurrency limit
  • The Celery layer enforces it per provider
  • Exceeding capacity queues rather than erroring
  • Queue depth and estimated wait are observable and surfaced to the user
  • A simulated burst of concurrent sessions completes without provider rate-limit failures
Found in the August 2026 session lifecycle review (#319). ## Why Session processing is bursty and highly correlated — most groups play Friday and Saturday evening, in a couple of timezone clusters. A single Friday night can deliver a large number of tracks into a narrow window. Every provider has a limit, and they differ: managed APIs publish concurrency or rate limits, a self-hosted llama.cpp instance has a fixed slot count (the current deployment runs `--parallel 2`), and a CPU deployment is effectively serial. Exceeding any of them produces rate-limit errors or queue collapse rather than graceful slowdown. ## Proposed fix Give each provider adapter a declared concurrency limit and enforce it in the Celery layer, so work queues rather than fails. Prefer a shape where a burst costs latency, not failures — this is asynchronous batch work and nobody needs their recap within 30 seconds. Surface queue depth and estimated wait so a GM whose session lands in a busy window sees "queued, about 20 minutes" rather than silence. That connects to the async-feedback work in v4.3.0. ## Acceptance criteria - [ ] Each adapter declares a concurrency limit - [ ] The Celery layer enforces it per provider - [ ] Exceeding capacity queues rather than erroring - [ ] Queue depth and estimated wait are observable and surfaced to the user - [ ] A simulated burst of concurrent sessions completes without provider rate-limit failures
Author
Contributor

Landed in PR #495 (merged 2026-09-05, CI green; 1929 backend / 489 frontend tests).

  • Declared limits per adapter (Ollama 1, llama.cpp 2, bundled WhisperX 1, managed None) with .env overrides QB_LLM_CONCURRENCY_LIMIT / QB_ASR_CONCURRENCY_LIMIT.
  • app/services/provider_slots.py: a Redis lease semaphore (DB 0) with TTL leases, admission-order ranking, rolling hold durations for the estimate, and fail-open if Redis is unreachable.
  • process_audio acquires the ASR slot and re-queues itself (self.retry, 20–40 s jittered, budget ~2 h) when none is free; every LLM path goes through one run_with_slot at the transport dispatch with a bounded in-task wait; request-path callers never wait (a request-scope contextvar set by middleware, so a browser cannot hang on the queue).
  • 429s release the slot and back off honouring Retry-After (clamped to 10 min). An ASR 429 re-queues the session rather than failing it, and writes no usage row. An LLM 429 after transcription still fails after three attempts, deliberately: re-running would repeat the paid-for ASR.
  • GET /api/admin/ai/queue, processing_wait on the session response, and a "queued behind N, about M minutes" note on the session page.
  • Unified with #359: one typed rate-limit error; the placeholder conformance case is now real and asserts peak == limit for every adapter. The burst test (8 sessions, limit 2) shows peak exactly 2 and zero failures.

Not done: no separate retry counter for queue waits versus transport retries — they share Celery's budget, documented at the call site.

Landed in **PR #495** (merged 2026-09-05, CI green; 1929 backend / 489 frontend tests). - Declared limits per adapter (Ollama 1, llama.cpp 2, bundled WhisperX 1, managed `None`) with `.env` overrides `QB_LLM_CONCURRENCY_LIMIT` / `QB_ASR_CONCURRENCY_LIMIT`. - `app/services/provider_slots.py`: a Redis lease semaphore (DB 0) with TTL leases, admission-order ranking, rolling hold durations for the estimate, and fail-open if Redis is unreachable. - `process_audio` acquires the ASR slot and re-queues itself (`self.retry`, 20–40 s jittered, budget ~2 h) when none is free; every LLM path goes through one `run_with_slot` at the transport dispatch with a bounded in-task wait; request-path callers never wait (a request-scope contextvar set by middleware, so a browser cannot hang on the queue). - 429s release the slot and back off honouring `Retry-After` (clamped to 10 min). An ASR 429 re-queues the session rather than failing it, and writes no usage row. An LLM 429 after transcription still fails after three attempts, deliberately: re-running would repeat the paid-for ASR. - `GET /api/admin/ai/queue`, `processing_wait` on the session response, and a "queued behind N, about M minutes" note on the session page. - Unified with #359: one typed rate-limit error; the placeholder conformance case is now real and asserts **peak == limit** for every adapter. The burst test (8 sessions, limit 2) shows peak exactly 2 and zero failures. Not done: no separate retry counter for queue waits versus transport retries — they share Celery's budget, documented at the call site.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#356
No description provided.