perf: bound concurrent image generation

This commit is contained in:
mARTin-B78 2026-09-04 07:52:17 +02:00
parent ebf7b8baa1
commit 885fa9b065
3 changed files with 12 additions and 1 deletions

View File

@ -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.

View File

@ -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

View File

@ -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}