banyaro/backend/cache.py
rene c03884cb81 Perf: 9 Performance-Fixes — SW by-v1072
Backend:
- DB: 3 neue Indizes (forum_posts thread+user, routes user) — Forum/Routen-Queries
- Caching: cache.py (TTL-Cache ohne neue Dependency) für 5 statische Listen
  (training_exercises, pflege_tipps, wiki_stats, wiki_gruppen, help_articles)
- diary.py + breeder_photos.py: Bildverarbeitung (ffmpeg/PIL/EXIF) per
  run_in_executor → blockiert Event-Loop nicht mehr
- scheduler.py: 11 kollidierende Jobs auf 5-Min-Intervalle gestaggert, coalesce=True
- social.py: ORDER BY RANDOM() ohne LIMIT in 2 Stellen gefixt
- alerts.py: Haversine-Loop bekommt SQL-Bounding-Box-Vorfilter

Frontend:
- sw.js: Tile-Cache mit LRU-Eviction (max 500 Einträge)
- admin.js: Event-Listener-Leak — Tab-Klicks per Delegation statt N Listener
- api.js: compressImage() Helper — Client-seitiges Resize auf max 2000px
  (HEIC/Videos/<500KB unverändert), integriert in 8 Upload-Stellen
  (diary, dog-profile×2, walks, poison, lost, health×2)

Bump APP_VER 1071 → 1072 (sw.js, app.js, main.py, index.html)
2026-05-26 06:30:36 +02:00

103 lines
3.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""BAN YARO — In-Memory TTL-Cache für statische DB-Daten.
Hintergrund:
Routes wie /api/training/exercises, /api/help, /api/wiki/stats laden bei
jedem Request statische Daten aus der DB. Das ist verschwendete Energie.
Diese Daten ändern sich nur durch Admin-Aktionen.
Verwendung:
from cache import ttl_cache
@ttl_cache(ttl=3600)
def my_func(arg1, arg2):
...
API:
@ttl_cache(ttl=3600) Decorator cached pro Argumenten-Signatur
my_func.cache_clear() Komplett leeren (z.B. nach Admin-Update)
Hinweis:
Diese Implementierung ist absichtlich klein und ohne externe Dependency
(kein cachetools nötig). Thread-safe via Lock. Reicht für Read-Only-
Listen, die sich selten ändern. Niemals für user-spezifische Daten
verwenden!
"""
from __future__ import annotations
import functools
import threading
import time
from typing import Any, Callable
def ttl_cache(ttl: int = 3600, maxsize: int = 128) -> Callable:
"""Decorator: cached Rückgabewert pro Argumenten-Signatur für `ttl` Sek.
- ttl: Time-to-live in Sekunden (Default: 1 Stunde)
- maxsize: max. Anzahl Einträge im Cache (FIFO-Eviction bei Überlauf)
Die dekorierte Funktion bekommt zusätzlich:
.cache_clear() leert den gesamten Cache
.cache_info() {hits, misses, size, ttl, maxsize}
"""
def decorator(func: Callable) -> Callable:
store: dict[tuple, tuple[float, Any]] = {}
lock = threading.Lock()
stats = {"hits": 0, "misses": 0}
def _make_key(args: tuple, kwargs: dict) -> tuple:
# kwargs als sortiertes Tuple in den Key packen
if kwargs:
return args + tuple(sorted(kwargs.items()))
return args
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = _make_key(args, kwargs)
now = time.monotonic()
with lock:
cached = store.get(key)
if cached is not None:
expires_at, value = cached
if expires_at > now:
stats["hits"] += 1
return value
# abgelaufen → raus
del store[key]
stats["misses"] += 1
# Außerhalb des Locks ausführen (kann DB-Calls machen)
value = func(*args, **kwargs)
with lock:
# FIFO-Eviction, wenn maxsize überschritten
if len(store) >= maxsize:
try:
oldest_key = next(iter(store))
del store[oldest_key]
except StopIteration:
pass
store[key] = (now + ttl, value)
return value
def cache_clear() -> None:
with lock:
store.clear()
stats["hits"] = 0
stats["misses"] = 0
def cache_info() -> dict:
with lock:
return {
"hits": stats["hits"],
"misses": stats["misses"],
"size": len(store),
"ttl": ttl,
"maxsize": maxsize,
}
wrapper.cache_clear = cache_clear # type: ignore[attr-defined]
wrapper.cache_info = cache_info # type: ignore[attr-defined]
return wrapper
return decorator