feat: serialize official record imports
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s

This commit is contained in:
ik
2026-09-07 07:54:07 +07:00
parent 687b4c9cb5
commit c45b4511b7
9 changed files with 115 additions and 13 deletions
+41 -1
View File
@@ -3,11 +3,12 @@ from __future__ import annotations
import hashlib
import re
import time as time_module
from contextlib import contextmanager
from dataclasses import asdict, dataclass
from datetime import date, datetime, time, timezone
import httpx
from sqlalchemy import select
from sqlalchemy import select, text
from sqlalchemy.orm import Session
from rf4_research.official_parser import RecordsContractError, parse_official_records
@@ -24,6 +25,10 @@ class ImportSourceError(ValueError):
pass
class ImportAlreadyRunning(RuntimeError):
pass
@dataclass(frozen=True, slots=True)
class RawRecord:
region: str
@@ -105,7 +110,42 @@ def fetch_records(
raise AssertionError("unreachable")
def _lock_key(url: str, region: str, category: str) -> int:
digest = hashlib.sha256(f"{url}|{region.upper()}|{category}".encode()).digest()
return int.from_bytes(digest[:8], byteorder="big", signed=True)
@contextmanager
def _official_import_lock(session: Session, *, url: str, region: str, category: str):
bind = session.get_bind()
if bind.dialect.name != "postgresql":
yield
return
connection = bind.connect()
key = _lock_key(url, region, category)
try:
acquired = bool(connection.scalar(text("SELECT pg_try_advisory_lock(:key)"), {"key": key}))
except Exception:
connection.close()
raise
if not acquired:
connection.close()
raise ImportAlreadyRunning("official import is already running for this source and category")
try:
yield
finally:
try:
connection.execute(text("SELECT pg_advisory_unlock(:key)"), {"key": key})
finally:
connection.close()
def import_records(session: Session, *, url: str, region: str, category: str, html: str | None = None) -> OfficialRecordImport:
with _official_import_lock(session, url=url, region=region, category=category):
return _import_records_locked(session, url=url, region=region, category=category, html=html)
def _import_records_locked(session: Session, *, url: str, region: str, category: str, html: str | None = None) -> OfficialRecordImport:
run = OfficialRecordImport(started_at=datetime.now(timezone.utc), status=ImportStatus.running, source_url=url, rows_seen=0, rows_created=0, rows_updated=0)
session.add(run)
session.commit()