Add more workers and update structure #46
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Scale TTS Processing Across Multiple Workers and TTS Nodes
Current state
Ready for scaling:
backend/db/migrations/009_create_tts_jobs.up.sqlwith index(status, created_at)ClaimNextQueuedTTSJobusesFOR UPDATE SKIP LOCKED(job_id, segment_index)is idempotentBottlenecks today:
backend/cmd/web/main.gobackend/internal/tts/worker.go_synthesis_lockintts/main.pyArchitecture
Separate the API (enqueue + poll) from workers (claim + synthesize + store). Scale each layer independently.
Throughput:
Design principles
Job queue: Postgres + LISTEN/NOTIFY
Postgres remains the durable job queue and source of truth. Workers claim jobs via
ClaimNextQueuedTTSJob(SKIP LOCKED). No gRPC dispatch from the API — workers stay independent of API uptime.Workers wake via Postgres
LISTEN/NOTIFYinstead of polling:CreateTTSJob/ requeue paths emitNOTIFY tts_job_queuedon commitMigrations: API only
backend/cmd/web/main.gorunsmigrate.Up()on startupbackend/cmd/worker/main.goconnects to DB but does not migrateCode layout
internal/workerinternal/ttscmd/workerDocker: separate images
webspeaker-back:latestbackend/Dockerfilecmd/webwebspeaker-worker:latestbackend/Dockerfile.workercmd/workerBoth build from the same Go module. Share builder-stage pattern to avoid drift.
TTS nodes: one model per container, one synthesis at a time
Each TTS container/node loads one model instance and processes one synthesis at a time. Spare VRAM (e.g. on a 16GB GPU with a ~5–8GB 1.7B bf16 model) is activation headroom, not used for duplicate model copies.
CUDA_VISIBLE_DEVICES; load-balance viaTTS_SERVICE_URLFuture optimization (optional): profile GPU utilization; if consistently low, investigate segment batching within a single model before any architectural changes.
Phase 1 — Worker package and binary split
Config (
internal/worker/config.go)TTS_SERVICE_URLhttp://tts:8000/tts/streamTTS_WORKER_CONCURRENCY1TTS_NOTIFY_CHANNELtts_job_queuedTTS_NOTIFY_FALLBACK30sTTS_STALE_JOB_AFTER30mrunningjobsTTS_BUSY_RETRY_MAX5TTS_BUSY_RETRY_BACKOFF2scmd/worker/main.goSIGTERMcmd/web/main.goCompose
backend-workerservice usingwebspeaker-worker:latestPhase 2 — Worker pool with LISTEN/NOTIFY
In
internal/worker/pool.go:LISTEN tts_job_queuedon startupClaimNextQueuedTTSJobinternal/tts.Processor.ProcessJob(ctx, job)In
tts_job_store.go, emit on enqueue/requeue:Phase 3 — TTS internal queue
In
tts/main.py:_synthesis_lockbusy-reject with an internal FIFO queue/tts/streamenqueues the request; one consumer callsprovider.synthesize()at a time/healthfor observabilityMulti-GPU: deploy one TTS replica per GPU, each with its own queue and model, behind
TTS_SERVICE_URL.Phase 4 — Resilience
ReleaseTTSJob:running → queuedon transient TTS connection failure so another worker/node can retryrunningpastTTS_STALE_JOB_AFTER; delete partial segmentsPhase 5 — Observability
job_id,worker_id,queue_wait,synthesis_durationtts_jobs_queued,tts_jobs_running,tts_queue_depth/healthRollout order
internal/worker+cmd/worker+Dockerfile.worker(concurrency=1, poll-based initially)Unchanged
frontend/src/pages/AudioTest/index.tsx)backend/internal/handlers/tts_jobs.go)FOR UPDATE SKIP LOCKED)backend/internal/tts/sse.go)Testing
ClaimNextQueuedTTSJobfrom N goroutines → distinct jobs/tts/streamconnections → queued, not rejectedRisks
runningafter crash