fix(scenes): use ON CONFLICT for tag slug upsert in enrich_tags_from_tube

Replace SAVEPOINT + IntegrityError fallback in resolve_tag with
postgres INSERT ... ON CONFLICT (slug) DO NOTHING + re-SELECT.
Postgres serializes on the unique index, so concurrent inserts of
the same slug no longer race on lookup→insert and the second caller
no longer raises uq_tags_slug. Mirrors the on_conflict pattern
already used for SceneTag/MovieTag inserts.
This commit is contained in:
jtrzupek 2026-05-27 15:03:58 +02:00
parent aac6b10d77
commit 49bb65d707

View file

@ -4,7 +4,7 @@ from __future__ import annotations
import uuid import uuid
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.exc import IntegrityError from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.tag import Tag from app.models.tag import Tag
@ -27,22 +27,18 @@ def resolve_tag(session: Session, *, norm: NormalizedTag) -> Tag | None:
name = (norm.name or "").strip() or slug.replace("-", " ").title() name = (norm.name or "").strip() or slug.replace("-", " ").title()
if len(name) > 120: if len(name) > 120:
name = name[:120] name = name[:120]
tag = Tag(name=name, slug=slug) # Concurrent insert race: worker scraper + API enrich_tags_from_tube oba
# SAVEPOINT — chroni outer transaction przed concurrent insert race: # robią lookup→insert na tym samym slug ('hardcore-sex'), oba widzą NULL,
# gdy worker scrapuje sceny + API endpoint enrich_tags_from_tube robią # oba próbują INSERT, drugi pada UniqueViolation uq_tags_slug (Sentry GOON-H,
# `resolve_tag('hardcore-sex')` jednocześnie, jeden INSERT się uda, # 5 events od 2026-05-12). ON CONFLICT DO NOTHING + re-SELECT po slug
# drugi → UniqueViolation slug. Bez savepoint cała transakcja API # załatwia atomowo — Postgres serializuje na unique index.
# rzucała 500 (Sentry GOON-H, 5x od 2026-05-12). Z savepoint rollback stmt = (
# na savepoint + re-SELECT zwraca już-istniejący tag. pg_insert(Tag.__table__)
sp = session.begin_nested() .values(name=name, slug=slug)
try: .on_conflict_do_nothing(index_elements=["slug"])
session.add(tag) )
session.flush() session.execute(stmt)
sp.commit() return session.execute(select(Tag).where(Tag.slug == slug)).scalar_one_or_none()
return tag
except IntegrityError:
sp.rollback()
return session.execute(select(Tag).where(Tag.slug == slug)).scalar_one_or_none()
def resolve_tag_by_id(session: Session, tag_id: uuid.UUID) -> Tag | None: def resolve_tag_by_id(session: Session, tag_id: uuid.UUID) -> Tag | None: