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