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)
103 lines
3.4 KiB
Python
103 lines
3.4 KiB
Python
"""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
|