Improve ingestion concurrency safety and batch transaction robustness
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
from datetime import timedelta
|
||||
import hashlib
|
||||
from contextlib import contextmanager
|
||||
|
||||
from django.db.models import Q
|
||||
from django.db import connection
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.ingestion.models import IngestionError, IngestionRun
|
||||
@ -62,14 +63,59 @@ def mark_ingestion_run_skipped(*, provider_namespace: str, job_type: str, reason
|
||||
return run
|
||||
|
||||
|
||||
def has_running_ingestion_run(*, provider_namespace: str, job_type: str, within_minutes: int) -> bool:
|
||||
cutoff = timezone.now() - timedelta(minutes=max(within_minutes, 1))
|
||||
return IngestionRun.objects.filter(
|
||||
def _build_ingestion_lock_key(*, provider_namespace: str, job_type: str) -> int:
|
||||
digest = hashlib.blake2b(
|
||||
f"{provider_namespace}:{job_type}".encode("utf-8"),
|
||||
digest_size=8,
|
||||
).digest()
|
||||
return int.from_bytes(digest, byteorder="big", signed=True)
|
||||
|
||||
|
||||
def try_acquire_ingestion_lock(*, provider_namespace: str, job_type: str) -> tuple[bool, int]:
|
||||
lock_key = _build_ingestion_lock_key(provider_namespace=provider_namespace, job_type=job_type)
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT pg_try_advisory_lock(%s);", [lock_key])
|
||||
acquired = bool(cursor.fetchone()[0])
|
||||
return acquired, lock_key
|
||||
|
||||
|
||||
def release_ingestion_lock(*, lock_key: int) -> None:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT pg_advisory_unlock(%s);", [lock_key])
|
||||
|
||||
|
||||
@contextmanager
|
||||
def ingestion_advisory_lock(*, provider_namespace: str, job_type: str):
|
||||
acquired, lock_key = try_acquire_ingestion_lock(
|
||||
provider_namespace=provider_namespace,
|
||||
job_type=job_type,
|
||||
status=IngestionRun.RunStatus.RUNNING,
|
||||
started_at__gte=cutoff,
|
||||
).filter(Q(finished_at__isnull=True) | Q(finished_at__gte=cutoff)).exists()
|
||||
)
|
||||
try:
|
||||
yield acquired
|
||||
finally:
|
||||
if acquired:
|
||||
release_ingestion_lock(lock_key=lock_key)
|
||||
|
||||
|
||||
def update_ingestion_run_progress(
|
||||
*,
|
||||
run: IngestionRun,
|
||||
completed_step: str,
|
||||
step_summary: dict,
|
||||
source_counts: dict | None = None,
|
||||
) -> IngestionRun:
|
||||
context = dict(run.context or {})
|
||||
completed_steps = list(context.get("completed_steps") or [])
|
||||
completed_steps.append(completed_step)
|
||||
context["completed_steps"] = completed_steps
|
||||
step_summaries = dict(context.get("step_summaries") or {})
|
||||
step_summaries[completed_step] = step_summary
|
||||
context["step_summaries"] = step_summaries
|
||||
if source_counts is not None:
|
||||
context["source_counts"] = source_counts
|
||||
run.context = context
|
||||
run.save(update_fields=["context"])
|
||||
return run
|
||||
|
||||
|
||||
def log_ingestion_error(*, run: IngestionRun, message: str, provider_namespace: str, severity: str = IngestionError.Severity.ERROR, entity_type: str = "", external_id: str = "", raw_payload: dict | None = None) -> IngestionError:
|
||||
|
||||
@ -9,7 +9,12 @@ from django.utils.dateparse import parse_date
|
||||
|
||||
from apps.competitions.models import Competition, Season
|
||||
from apps.ingestion.models import IngestionRun
|
||||
from apps.ingestion.services.runs import finish_ingestion_run, log_ingestion_error, start_ingestion_run
|
||||
from apps.ingestion.services.runs import (
|
||||
finish_ingestion_run,
|
||||
log_ingestion_error,
|
||||
start_ingestion_run,
|
||||
update_ingestion_run_progress,
|
||||
)
|
||||
from apps.players.models import Nationality, Player, PlayerAlias, PlayerCareerEntry, Position, Role
|
||||
from apps.players.services.origin import refresh_player_origin
|
||||
from apps.providers.exceptions import ProviderRateLimitError, ProviderTransientError
|
||||
@ -441,14 +446,54 @@ def run_sync_job(
|
||||
if job_type == IngestionRun.JobType.INCREMENTAL
|
||||
else provider.sync_all()
|
||||
)
|
||||
source_counts = {
|
||||
"competitions": len(payload.get("competitions", [])),
|
||||
"teams": len(payload.get("teams", [])),
|
||||
"seasons": len(payload.get("seasons", [])),
|
||||
"players": len(payload.get("players", [])),
|
||||
"player_stats": len(payload.get("player_stats", [])),
|
||||
"player_careers": len(payload.get("player_careers", [])),
|
||||
}
|
||||
|
||||
with transaction.atomic():
|
||||
_sync_competitions(provider_namespace, payload.get("competitions", []), run, summary)
|
||||
_sync_teams(provider_namespace, payload.get("teams", []), run, summary)
|
||||
_sync_seasons(provider_namespace, payload.get("seasons", []), run, summary)
|
||||
_sync_players(provider_namespace, payload.get("players", []), run, summary)
|
||||
_sync_player_stats(provider_namespace, payload.get("player_stats", []), run, summary)
|
||||
_sync_player_careers(provider_namespace, payload.get("player_careers", []), run, summary)
|
||||
steps: list[tuple[str, callable, list[dict]]] = [
|
||||
("competitions", _sync_competitions, payload.get("competitions", [])),
|
||||
("teams", _sync_teams, payload.get("teams", [])),
|
||||
("seasons", _sync_seasons, payload.get("seasons", [])),
|
||||
("players", _sync_players, payload.get("players", [])),
|
||||
("player_stats", _sync_player_stats, payload.get("player_stats", [])),
|
||||
("player_careers", _sync_player_careers, payload.get("player_careers", [])),
|
||||
]
|
||||
|
||||
for step_name, step_fn, step_payload in steps:
|
||||
step_summary = SyncSummary()
|
||||
with transaction.atomic():
|
||||
step_fn(provider_namespace, step_payload, run, step_summary)
|
||||
|
||||
summary.processed += step_summary.processed
|
||||
summary.created += step_summary.created
|
||||
summary.updated += step_summary.updated
|
||||
summary.failed += step_summary.failed
|
||||
|
||||
update_ingestion_run_progress(
|
||||
run=run,
|
||||
completed_step=step_name,
|
||||
step_summary={
|
||||
"processed": step_summary.processed,
|
||||
"created": step_summary.created,
|
||||
"updated": step_summary.updated,
|
||||
"failed": step_summary.failed,
|
||||
},
|
||||
source_counts=source_counts,
|
||||
)
|
||||
logger.info(
|
||||
"Completed ingestion step run_id=%s step=%s processed=%s created=%s updated=%s failed=%s",
|
||||
run.id,
|
||||
step_name,
|
||||
step_summary.processed,
|
||||
step_summary.created,
|
||||
step_summary.updated,
|
||||
step_summary.failed,
|
||||
)
|
||||
|
||||
success_error_summary = ""
|
||||
if summary.failed > 0:
|
||||
|
||||
Reference in New Issue
Block a user