coinsentry / app /core /cache.py
heisbuba's picture
Upload 3 files
ee43f70 verified
Raw
History Blame Contribute Delete
4.15 kB
"""
app/core/cache.py
─────────────────
Two independent market-data caches:
PUBLIC_CACHE β€” uses the keyless CoinGecko v3 public endpoint.
Auto-refreshes every 120 s via a background thread.
Used by all guest visitors; logged-in users can opt in.
Fetches 5 pages (1 250 coins).
V3_CACHE β€” uses the v3 endpoint with a demo API key.
Refreshed on-demand: stale after 300 s or on manual
/dashboard/refresh. Fetches 6 pages (1 500 coins).
Only used when a logged-in user has endpoint_mode="v3".
"""
import time
import threading
import os
from app.core.scanner import MarketScanner
# ── Cache stores ─────────────────────────────────────────────────────────────
PUBLIC_CACHE: dict = {"data": None, "timestamp": 0}
V3_CACHE: dict = {"data": None, "timestamp": 0}
_PUBLIC_TTL = 120 # seconds between auto-refreshes for public endpoint
_V3_TTL = 300 # seconds before v3 cache is considered stale
_public_lock = threading.Lock()
_v3_lock = threading.Lock()
_bg_started = False
_bg_lock = threading.Lock()
# ── Public endpoint helpers ───────────────────────────────────────────────────
def _refresh_public() -> None:
"""Fetch fresh data from the keyless public endpoint and store it."""
try:
data = MarketScanner(api_key="", use_public=True).run_scan(pages=4)
with _public_lock:
PUBLIC_CACHE["data"] = data
PUBLIC_CACHE["timestamp"] = time.time()
except Exception as exc:
print(f"[cache] public refresh error: {exc}")
def _bg_loop() -> None:
"""Background thread: refresh public cache every _PUBLIC_TTL seconds."""
while True:
_refresh_public()
time.sleep(_PUBLIC_TTL)
def start_public_autorefresh() -> None:
"""Start the background auto-refresh thread (idempotent)."""
global _bg_started
with _bg_lock:
if _bg_started:
return
t = threading.Thread(target=_bg_loop, daemon=True)
t.start()
_bg_started = True
print("[cache] Public auto-refresh thread started (every 120 s).")
def get_public_data() -> list:
"""Return cached public data; triggers a blocking refresh if cache is empty."""
with _public_lock:
if PUBLIC_CACHE["data"]:
return PUBLIC_CACHE["data"]
# First-ever call: populate synchronously so the page doesn't return empty.
# NOTE: We intentionally release the lock before the slow network call to
# avoid blocking concurrent requests. Multiple callers may race here on a
# cold start; only the last writer wins which is safe (idempotent data).
_refresh_public()
with _public_lock:
return PUBLIC_CACHE["data"] or []
# ── V3 (demo-key) endpoint helpers ────────────────────────────────────────────
def get_v3_data(api_key: str) -> list:
"""
Return cached v3 data, refreshing if stale (> 300 s) or empty.
Uses whichever api_key is passed (personal > env fallback).
"""
with _v3_lock:
now = time.time()
if V3_CACHE["data"] and (now - V3_CACHE["timestamp"] <= _V3_TTL):
return V3_CACHE["data"]
# Outside the lock for the slow network call
try:
data = MarketScanner(api_key=api_key, use_public=False).run_scan(pages=6)
except Exception as exc:
print(f"[cache] v3 refresh error: {exc}")
data = V3_CACHE.get("data") or []
with _v3_lock:
V3_CACHE["data"] = data
V3_CACHE["timestamp"] = time.time()
return data
def invalidate_v3() -> None:
"""Force the next v3 access to re-fetch (used by /dashboard/refresh)."""
with _v3_lock:
V3_CACHE["data"] = None
V3_CACHE["timestamp"] = 0