From 885fa9b065e69d15594e925c17de62e612211d85 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 4 Sep 2026 07:52:17 +0200 Subject: [PATCH] perf: bound concurrent image generation --- CHANGELOG.md | 2 ++ TODO.md | 3 ++- routes/conversation.py | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a90892a..dd28bea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi Browser localStorage is no longer used as a credential store. - **Image Job Deduplication.** Consistent portrait/reference generation accepts an `Idempotency-Key` and returns the existing job for repeated submissions; completed job mappings are evicted with the job cache. +- **Image Concurrency Bound.** Consistent image generation is limited to two active GPU jobs; additional requests + remain observable as queued jobs and fail clearly if no slot becomes available within 15 minutes. - **Security Regression Coverage.** Added a Python test gate for character metadata validation, private-download URL rejection and oversized JSON request rejection. - **Navigation Reliability.** Rehearser phase requests now survive the feature-module initialization boundary. diff --git a/TODO.md b/TODO.md index bb36175..2a1b2ae 100644 --- a/TODO.md +++ b/TODO.md @@ -120,7 +120,8 @@ the first block before investing in architecture or polish. - [ ] Give every job an ID, progress, cancellation, timeout, retry, and idempotency key. - [ ] Prevent duplicate generation when a user clicks twice. - [ ] Centralize provider health checks and error handling. -- [ ] Limit concurrency to 1–2 image generations, 1–2 voice generations, and 1 STT request per user. +- [ ] Limit concurrency to 1–2 image generations, 1–2 voice generations, and 1 STT request per user. Consistent + portrait/reference rendering is now capped at two active jobs; extend the cap to voice and STT paths. - [ ] Use bounded server queues. ## P0 — Security diff --git a/routes/conversation.py b/routes/conversation.py index 31ab866..5f37df5 100644 --- a/routes/conversation.py +++ b/routes/conversation.py @@ -61,6 +61,7 @@ _attribution_llm_lock = threading.Lock() _consistent_image_jobs: dict[str, dict] = {} _consistent_image_jobs_lock = threading.Lock() _consistent_image_idempotency: dict[str, str] = {} +_consistent_image_slots = threading.BoundedSemaphore(2) def _cij_new(job_id: str, name: str) -> None: @@ -3085,6 +3086,11 @@ async def character_generate_consistent_images(request: Request): _consistent_image_idempotency[idempotency_key] = job_id def _worker() -> None: + acquired = _consistent_image_slots.acquire(timeout=900) + if not acquired: + _cij_set(job_id, stage="error", detail="Image queue timed out", done=True, + error="No image-generation slot became available within 15 minutes") + return try: identity_image = identity_image_input if identity_image.startswith("/"): @@ -3121,6 +3127,8 @@ async def character_generate_consistent_images(request: Request): _cij_set(job_id, stage="error", detail="Failed", done=True, error=str(exc.detail)) except Exception as exc: _cij_set(job_id, stage="error", detail="Failed", done=True, error=str(exc)) + finally: + _consistent_image_slots.release() threading.Thread(target=_worker, daemon=True, name=f"consistent-images-{job_id}").start() return {"job_id": job_id}