fix(api): cap list_scenes filter sizes to prevent DB OOM (Fixes GOON-1M)
Some checks are pending
Backend tests / test (push) Waiting to run

A single request with 194 studio_slugs + 23 tag filters (each tag = a correlated
EXISTS) plus an ILIKE search built a query heavy enough that the OOM killer killed the
Postgres backend, triggering a full crash-recovery (~1s prod-wide outage, all in-flight
connections dropped). Any user could do this with a big enough filter. Cap studios to
50, tags to 15, performers to 15 (far above any real UI usage) and return 422 instead
of executing — bounding query complexity regardless of the planner's choice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-06-26 16:25:29 +02:00
parent 813bf741b9
commit 05a35955ad

View file

@ -167,9 +167,18 @@ def list_scenes(
if q:
base = base.where(Scene.title_normalized.ilike(f"%{q.lower()}%"))
# Cap rozmiarów filtrów. Bez tego pojedynczy request z setkami studio_slugs +
# dziesiątkami tagów (każdy tag = osobny correlated EXISTS) + ILIKE budował zapytanie,
# które OOM-killer ubijał → PG crash-recovery = ~1s globalnej przerwy (GOON-1M,
# 2026-06-26: 194 studios + 23 tagi). Realny UI nigdy nie wysyła tylu. 422 zamiast
# wywalania bazy. Limity hojne (>> normalne użycie), ale ograniczają złożoność query.
_MAX_STUDIOS, _MAX_TAGS, _MAX_PERFORMERS = 50, 15, 15
studio_slug_list = _split_csv(studio_slugs)
if studio_slug:
studio_slug_list.append(studio_slug)
if len(studio_slug_list) > _MAX_STUDIOS:
raise HTTPException(status_code=422, detail=f"too many studio filters (max {_MAX_STUDIOS})")
if studio_slug_list:
base = base.where(
Scene.studio_id.in_(
@ -178,6 +187,8 @@ def list_scenes(
)
tag_slug_list = _split_csv(tags)
if len(tag_slug_list) > _MAX_TAGS:
raise HTTPException(status_code=422, detail=f"too many tag filters (max {_MAX_TAGS})")
# AND między tagami: scena musi mieć WSZYSTKIE zaznaczone tagi. Każdy slug → osobny
# exists() — zaznaczanie kolejnych filtrów zawęża wyniki, jak intuicja użytkownika.
#
@ -207,6 +218,8 @@ def list_scenes(
)
perf_id_strings = _split_csv(performer_ids)
if len(perf_id_strings) > _MAX_PERFORMERS:
raise HTTPException(status_code=422, detail=f"too many performer filters (max {_MAX_PERFORMERS})")
if perf_id_strings:
try:
perf_ids = [uuid.UUID(s) for s in perf_id_strings]