From b6e8aa7225ed01a492ed116bf69ed5bcb247e0dc Mon Sep 17 00:00:00 2001 From: jerem Date: Wed, 17 Jun 2026 12:06:02 +0200 Subject: [PATCH] =?UTF-8?q?Trackers:=20cache=201h=20+=20persistance=20disq?= =?UTF-8?q?ue=20(/data)=20pour=20survivre=20aux=20red=C3=A9ploiements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 4 +++ backend/config.py | 11 ++++++--- backend/integrations/trackers.py | 42 +++++++++++++++++++++++++++++++- docker-compose.yml | 3 +++ 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index aa21662..8db5902 100644 --- a/.env.example +++ b/.env.example @@ -41,6 +41,10 @@ MONITORINK_CODEX_TOKEN_FILE=/hermes/auth.json # Liste des clés actives, puis UN bloc par tracker. v1 : type "unit3d_nuxt" (ex. c411). # Le ratio n'est PAS lisible au token API -> login requis (username/password du compte). # Laisser MONITORINK_TRACKERS vide pour masquer la section. +# Cache : le ratio bouge lentement, on le garde 1 h (TTL) et on le persiste sur disque +# (volume /data) pour ne pas reloguer les 4 trackers après chaque redéploiement. +#MONITORINK_TRACKER_TTL=3600 +#MONITORINK_TRACKER_CACHE_FILE=/data/trackers.json #MONITORINK_TRACKERS=c411,torr9 #MONITORINK_TRACKER_C411_LABEL=c411 #MONITORINK_TRACKER_C411_TYPE=unit3d_nuxt diff --git a/backend/config.py b/backend/config.py index b05d803..95a9c82 100644 --- a/backend/config.py +++ b/backend/config.py @@ -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 --- diff --git a/backend/integrations/trackers.py b/backend/integrations/trackers.py index a3dfb94..ac04f39 100644 --- a/backend/integrations/trackers.py +++ b/backend/integrations/trackers.py @@ -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))) diff --git a/docker-compose.yml b/docker-compose.yml index 8d51a2b..526a30f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,9 @@ services: # l'inode capturé au démarrage -> le conteneur servirait un token périmé (401). # Monter le dossier fait re-résoudre /hermes/auth.json à chaque open(). - /home/jerem/.hermes:/hermes:ro + # Cache persistant des ratios trackers (survit aux redéploiements -> pas de + # re-login massif des 4 trackers après chaque rebuild). + - /home/jerem/.monitorink-data:/data:rw # Optionnel : burn rate via ccusage (lecture seule des logs Claude Code principaux). # Décommenter + MONITORINK_CCUSAGE=1. # - /home/jerem/.claude/projects:/root/.claude/projects:ro