Spaces:
Running
Running
| """ | |
| 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 | |