Trackers: cache 1h + persistance disque (/data) pour survivre aux redéploiements
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user