diff --git a/app/resolve/tag_resolver.py b/app/resolve/tag_resolver.py index 421aa43..7ad123a 100644 --- a/app/resolve/tag_resolver.py +++ b/app/resolve/tag_resolver.py @@ -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,22 +27,18 @@ 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() - return session.execute(select(Tag).where(Tag.slug == slug)).scalar_one_or_none() + # 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() def resolve_tag_by_id(session: Session, tag_id: uuid.UUID) -> Tag | None: