Trackers: cache 1h + persistance disque (/data) pour survivre aux redéploiements

This commit is contained in:
jerem
2026-06-17 12:06:02 +02:00
parent 7f5bbd6b08
commit b6e8aa7225
4 changed files with 56 additions and 4 deletions

View File

@@ -98,10 +98,15 @@ class Config:
)
# --- Trackers torrent privés (ratio du compte) ---
# Le ratio change lentement et le login est coûteux (CSRF + session) : on cache
# le résultat plus longtemps que le reste (défaut 30 min).
# Le ratio change lentement (~30 min/1 h côté trackers) et le login est coûteux
# (CSRF + session) : on cache le résultat plus longtemps que le reste (défaut 1 h).
tracker_ttl_seconds: int = field(
default_factory=lambda: int(_get("MONITORINK_TRACKER_TTL", "1800"))
default_factory=lambda: int(_get("MONITORINK_TRACKER_TTL", "3600"))
)
# Cache persistant sur disque : survit aux redéploiements du conteneur, évite de
# reloguer les 4 trackers d'un coup après chaque rebuild. Monté via le volume /data.
tracker_cache_file: str = field(
default_factory=lambda: _get("MONITORINK_TRACKER_CACHE_FILE", "/data/trackers.json")
)
# --- Cache / rafraîchissement serveur ---

View File

@@ -18,9 +18,11 @@ renvoie un `TrackerStat`, et l'enregistrer dans `_FETCHERS` sous sa clé de `typ
from __future__ import annotations
import asyncio
import json
import os
import re
import time
from dataclasses import dataclass
from dataclasses import asdict, dataclass
import httpx
@@ -80,6 +82,42 @@ class TrackerStat:
# Sessions réutilisées (cookies httpx) et derniers résultats connus, par tracker.
_sessions: dict[str, httpx.Cookies] = {}
_cache: dict[str, dict] = {}
_loaded = False # le cache disque n'est lu qu'une fois par process
def _load_cache() -> None:
"""Hydrate `_cache` depuis le fichier JSON persistant (une fois par process).
Permet de survivre à un redéploiement sans reloguer les 4 trackers d'un coup.
Tout fichier absent/illisible/corrompu est ignoré silencieusement."""
global _loaded
if _loaded:
return
_loaded = True
try:
with open(config.tracker_cache_file, encoding="utf-8") as fh:
data = json.load(fh)
for key, entry in data.items():
_cache[key] = {"value": TrackerStat(**entry["value"]), "ts": float(entry["ts"])}
except (OSError, ValueError, KeyError, TypeError):
pass # cache absent/corrompu -> on repart d'un cache vide, re-fetch propre
def _save_cache() -> None:
"""Réécrit tout `_cache` sur disque de façon atomique (tmp + os.replace), comme le
refresh du token Claude. Une erreur d'écriture ne doit jamais casser le rendu."""
path = config.tracker_cache_file
payload = {
key: {"value": asdict(entry["value"]), "ts": entry["ts"]}
for key, entry in _cache.items()
}
try:
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
tmp = f"{path}.tmp"
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(payload, fh)
os.replace(tmp, path)
except OSError:
pass
class _AuthError(Exception):
@@ -275,6 +313,7 @@ async def _fetch_one(spec: TrackerSpec) -> TrackerStat:
return TrackerStat(spec.key, spec.label, ok=False, error=str(exc) or "injoignable")
_cache[spec.key] = {"value": value, "ts": now}
_save_cache() # persiste la nouvelle valeur (survit aux redéploiements)
return value
@@ -282,4 +321,5 @@ async def fetch_all() -> list[TrackerStat]:
specs = config.trackers
if not specs:
return []
_load_cache() # hydrate le cache depuis le disque au premier appel
return list(await asyncio.gather(*(_fetch_one(s) for s in specs)))