diff --git a/app/api/sources.py b/app/api/sources.py index c273680..f70d9c6 100644 --- a/app/api/sources.py +++ b/app/api/sources.py @@ -16,6 +16,7 @@ from __future__ import annotations import logging import os import re +import threading import time from datetime import datetime from pathlib import Path @@ -27,7 +28,7 @@ from sqlalchemy import func, select from sqlalchemy.orm import Session from app.auth import require_api_key -from app.db import get_session +from app.db import get_session, session_scope from app.models.playback_source import PlaybackSource from app.models.source_stats import SourceStats @@ -86,8 +87,10 @@ class SourceOut(BaseModel): _ICON_DIR = Path(__file__).resolve().parent.parent / "static" / "site-icons" -_SOURCES_TTL = 300.0 +#: Po tylu sekundach wynik uznajemy za stary i przeliczamy W TLE (oddajac stary). +_SOURCES_TTL = 900.0 _SOURCES_CACHE: dict = {"at": 0.0, "rows": None} +_SOURCES_LOCK = threading.Lock() def _icon_url(sitetag: str) -> str | None: @@ -134,6 +137,50 @@ def _sitetag_to_display(sitetag: str) -> str: return sitetag +def _source_rows(session: Session): + return session.execute( + select( + PlaybackSource.origin, + func.count(PlaybackSource.id).label("scene_count"), + func.max(PlaybackSource.last_seen_at).label("last_scraped_at"), + ) + .where(PlaybackSource.dead_at.is_(None)) + .where(PlaybackSource.origin.like("tube:%")) + .group_by(PlaybackSource.origin) + ).all() + + +def _refresh_source_rows_bg() -> None: + if not _SOURCES_LOCK.acquire(blocking=False): + return # ktos juz liczy + + def run() -> None: + try: + with session_scope() as s: + rows = _source_rows(s) + _SOURCES_CACHE["rows"] = rows + _SOURCES_CACHE["at"] = time.monotonic() + except Exception: # noqa: BLE001 + log.exception("sources: odswiezenie listy stron w tle nie wyszlo") + finally: + _SOURCES_LOCK.release() + + threading.Thread(target=run, name="sources-refresh", daemon=True).start() + + +def _cached_source_rows(session: Session): + rows = _SOURCES_CACHE["rows"] + if rows is None: + with _SOURCES_LOCK: + if _SOURCES_CACHE["rows"] is None: + _SOURCES_CACHE["rows"] = _source_rows(session) + _SOURCES_CACHE["at"] = time.monotonic() + return _SOURCES_CACHE["rows"] + if time.monotonic() - _SOURCES_CACHE["at"] > _SOURCES_TTL: + _refresh_source_rows_bg() + return rows + + @router.get("", response_model=SourceListOut) def list_sources( session: Annotated[Session, Depends(get_session)], @@ -142,23 +189,11 @@ def list_sources( Filter: `origin LIKE 'tube:%'` (drop canonical:* TPDB trailery to inna semantyka). """ - # Agregat idzie po 4,1 mln zywych zrodel (600 ms na kazde wejscie w Sites, Sentry: - # Slow DB Query list_sources). Liczby zmieniaja sie o promile na godzine, wiec - # wynik zyje w pamieci procesu przez _SOURCES_TTL sekund. - now = time.monotonic() - if _SOURCES_CACHE["rows"] is None or now - _SOURCES_CACHE["at"] > _SOURCES_TTL: - _SOURCES_CACHE["rows"] = session.execute( - select( - PlaybackSource.origin, - func.count(PlaybackSource.id).label("scene_count"), - func.max(PlaybackSource.last_seen_at).label("last_scraped_at"), - ) - .where(PlaybackSource.dead_at.is_(None)) - .where(PlaybackSource.origin.like("tube:%")) - .group_by(PlaybackSource.origin) - ).all() - _SOURCES_CACHE["at"] = now - rows = _SOURCES_CACHE["rows"] + # Agregat idzie po 4,1 mln zywych zrodel (0,6-1,6 s na przeliczenie, Sentry: Slow DB + # Query list_sources, 186 zdarzen). Liczby zmieniaja sie o promile na godzine, wiec + # wynik zyje w pamieci procesu, a po wygasnieciu oddajemy STARY i przeliczamy w tle. + # Wczesniej co wygasniecie cache ktos placil pelnym czasem zapytania (2026-09-17). + rows = _cached_source_rows(session) # Oceny z source_stats (policzone offline przez run_source_stats). Origin → row. stats = {s.origin: s for s in session.execute(select(SourceStats)).scalars().all()}