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

@@ -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). # 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). # Le ratio n'est PAS lisible au token API -> login requis (username/password du compte).
# Laisser MONITORINK_TRACKERS vide pour masquer la section. # 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_TRACKERS=c411,torr9
#MONITORINK_TRACKER_C411_LABEL=c411 #MONITORINK_TRACKER_C411_LABEL=c411
#MONITORINK_TRACKER_C411_TYPE=unit3d_nuxt #MONITORINK_TRACKER_C411_TYPE=unit3d_nuxt

View File

@@ -98,10 +98,15 @@ class Config:
) )
# --- Trackers torrent privés (ratio du compte) --- # --- Trackers torrent privés (ratio du compte) ---
# Le ratio change lentement et le login est coûteux (CSRF + session) : on cache # Le ratio change lentement (~30 min/1 h côté trackers) et le login est coûteux
# le résultat plus longtemps que le reste (défaut 30 min). # (CSRF + session) : on cache le résultat plus longtemps que le reste (défaut 1 h).
tracker_ttl_seconds: int = field( 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 --- # --- 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 from __future__ import annotations
import asyncio import asyncio
import json
import os
import re import re
import time import time
from dataclasses import dataclass from dataclasses import asdict, dataclass
import httpx import httpx
@@ -80,6 +82,42 @@ class TrackerStat:
# Sessions réutilisées (cookies httpx) et derniers résultats connus, par tracker. # Sessions réutilisées (cookies httpx) et derniers résultats connus, par tracker.
_sessions: dict[str, httpx.Cookies] = {} _sessions: dict[str, httpx.Cookies] = {}
_cache: dict[str, dict] = {} _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): 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") return TrackerStat(spec.key, spec.label, ok=False, error=str(exc) or "injoignable")
_cache[spec.key] = {"value": value, "ts": now} _cache[spec.key] = {"value": value, "ts": now}
_save_cache() # persiste la nouvelle valeur (survit aux redéploiements)
return value return value
@@ -282,4 +321,5 @@ async def fetch_all() -> list[TrackerStat]:
specs = config.trackers specs = config.trackers
if not specs: if not specs:
return [] return []
_load_cache() # hydrate le cache depuis le disque au premier appel
return list(await asyncio.gather(*(_fetch_one(s) for s in specs))) return list(await asyncio.gather(*(_fetch_one(s) for s in specs)))

View File

@@ -18,6 +18,9 @@ services:
# l'inode capturé au démarrage -> le conteneur servirait un token périmé (401). # 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(). # Monter le dossier fait re-résoudre /hermes/auth.json à chaque open().
- /home/jerem/.hermes:/hermes:ro - /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). # Optionnel : burn rate via ccusage (lecture seule des logs Claude Code principaux).
# Décommenter + MONITORINK_CCUSAGE=1. # Décommenter + MONITORINK_CCUSAGE=1.
# - /home/jerem/.claude/projects:/root/.claude/projects:ro # - /home/jerem/.claude/projects:/root/.claude/projects:ro