330 Zeilen
12 KiB
Python
330 Zeilen
12 KiB
Python
"""
|
|
Fingerprint Cache Service - Thread-safe caching for race condition prevention
|
|
Non-intrusive caching layer that enhances existing fingerprint logic
|
|
"""
|
|
|
|
import threading
|
|
import time
|
|
import weakref
|
|
from typing import Optional, Dict, Any, Callable, TypeVar, Generic
|
|
from datetime import datetime, timedelta
|
|
import logging
|
|
from domain.entities.browser_fingerprint import BrowserFingerprint
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
T = TypeVar('T')
|
|
|
|
|
|
class ThreadSafeCache(Generic[T]):
|
|
"""
|
|
Thread-sichere Cache-Implementierung mit TTL und LRU-Features
|
|
"""
|
|
|
|
def __init__(self, max_size: int = 1000, default_ttl: timedelta = timedelta(hours=1)):
|
|
self.max_size = max_size
|
|
self.default_ttl = default_ttl
|
|
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
self._access_times: Dict[str, datetime] = {}
|
|
self._locks: Dict[str, threading.RLock] = {}
|
|
self._main_lock = threading.RLock()
|
|
|
|
# Weak references für automatische Cleanup
|
|
self._cleanup_callbacks: Dict[str, Callable] = {}
|
|
|
|
def get(self, key: str) -> Optional[T]:
|
|
"""Holt einen Wert aus dem Cache"""
|
|
with self._main_lock:
|
|
if key not in self._cache:
|
|
return None
|
|
|
|
cache_entry = self._cache[key]
|
|
|
|
# TTL prüfen
|
|
if self._is_expired(cache_entry):
|
|
self._remove_key(key)
|
|
return None
|
|
|
|
# Access time aktualisieren für LRU
|
|
self._access_times[key] = datetime.now()
|
|
return cache_entry['value']
|
|
|
|
def put(self, key: str, value: T, ttl: Optional[timedelta] = None) -> None:
|
|
"""Speichert einen Wert im Cache"""
|
|
with self._main_lock:
|
|
# Cache-Größe prüfen und ggf. LRU entfernen
|
|
if len(self._cache) >= self.max_size and key not in self._cache:
|
|
self._evict_lru()
|
|
|
|
expiry = datetime.now() + (ttl or self.default_ttl)
|
|
|
|
self._cache[key] = {
|
|
'value': value,
|
|
'created_at': datetime.now(),
|
|
'expires_at': expiry
|
|
}
|
|
self._access_times[key] = datetime.now()
|
|
|
|
def get_or_compute(self, key: str, compute_func: Callable[[], T],
|
|
ttl: Optional[timedelta] = None) -> T:
|
|
"""
|
|
Holt Wert aus Cache oder berechnet ihn thread-safe
|
|
"""
|
|
# Fast path - Cache hit ohne Locks
|
|
cached_value = self.get(key)
|
|
if cached_value is not None:
|
|
return cached_value
|
|
|
|
# Slow path - mit per-key Lock
|
|
with self._main_lock:
|
|
# Per-key Lock erstellen falls nicht vorhanden
|
|
if key not in self._locks:
|
|
self._locks[key] = threading.RLock()
|
|
key_lock = self._locks[key]
|
|
|
|
# Außerhalb des Main-Locks arbeiten für bessere Parallelität
|
|
with key_lock:
|
|
# Double-checked locking - könnte zwischenzeitlich gesetzt worden sein
|
|
cached_value = self.get(key)
|
|
if cached_value is not None:
|
|
return cached_value
|
|
|
|
# Wert berechnen
|
|
logger.debug(f"Computing value for cache key: {key}")
|
|
start_time = time.time()
|
|
|
|
try:
|
|
computed_value = compute_func()
|
|
computation_time = time.time() - start_time
|
|
|
|
# Nur cachen wenn Berechnung erfolgreich
|
|
if computed_value is not None:
|
|
self.put(key, computed_value, ttl)
|
|
logger.debug(f"Cached value for key {key} (computation took {computation_time:.3f}s)")
|
|
|
|
return computed_value
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to compute value for cache key {key}: {e}")
|
|
raise
|
|
|
|
def invalidate(self, key: str) -> bool:
|
|
"""Entfernt einen Schlüssel aus dem Cache"""
|
|
with self._main_lock:
|
|
if key in self._cache:
|
|
self._remove_key(key)
|
|
return True
|
|
return False
|
|
|
|
def clear(self) -> None:
|
|
"""Leert den gesamten Cache"""
|
|
with self._main_lock:
|
|
self._cache.clear()
|
|
self._access_times.clear()
|
|
self._locks.clear()
|
|
|
|
def get_stats(self) -> Dict[str, Any]:
|
|
"""Gibt Cache-Statistiken zurück"""
|
|
with self._main_lock:
|
|
total_entries = len(self._cache)
|
|
expired_entries = sum(1 for entry in self._cache.values() if self._is_expired(entry))
|
|
|
|
return {
|
|
'total_entries': total_entries,
|
|
'active_entries': total_entries - expired_entries,
|
|
'expired_entries': expired_entries,
|
|
'max_size': self.max_size,
|
|
'active_locks': len(self._locks),
|
|
'cache_keys': list(self._cache.keys())
|
|
}
|
|
|
|
def _is_expired(self, cache_entry: Dict[str, Any]) -> bool:
|
|
"""Prüft ob ein Cache-Eintrag abgelaufen ist"""
|
|
return datetime.now() > cache_entry['expires_at']
|
|
|
|
def _evict_lru(self) -> None:
|
|
"""Entfernt den am längsten nicht verwendeten Eintrag"""
|
|
if not self._access_times:
|
|
return
|
|
|
|
lru_key = min(self._access_times.keys(), key=lambda k: self._access_times[k])
|
|
self._remove_key(lru_key)
|
|
logger.debug(f"Evicted LRU cache entry: {lru_key}")
|
|
|
|
def _remove_key(self, key: str) -> None:
|
|
"""Entfernt einen Schlüssel und alle zugehörigen Daten"""
|
|
self._cache.pop(key, None)
|
|
self._access_times.pop(key, None)
|
|
|
|
# Lock nicht sofort entfernen - könnte noch in Verwendung sein
|
|
# Wird durch schwache Referenzen automatisch bereinigt
|
|
|
|
|
|
class FingerprintCache:
|
|
"""
|
|
Spezialisierter Cache für Browser-Fingerprints mit Account-Binding
|
|
"""
|
|
|
|
def __init__(self, max_size: int = 500, fingerprint_ttl: timedelta = timedelta(hours=24)):
|
|
self.cache = ThreadSafeCache[BrowserFingerprint](max_size, fingerprint_ttl)
|
|
self.generation_stats = {
|
|
'cache_hits': 0,
|
|
'cache_misses': 0,
|
|
'generations': 0,
|
|
'race_conditions_prevented': 0
|
|
}
|
|
self._stats_lock = threading.RLock()
|
|
|
|
def get_account_fingerprint(self, account_id: str,
|
|
generator_func: Callable[[], BrowserFingerprint],
|
|
ttl: Optional[timedelta] = None) -> BrowserFingerprint:
|
|
"""
|
|
Holt oder erstellt Account-gebundenen Fingerprint thread-safe
|
|
"""
|
|
cache_key = f"account_{account_id}"
|
|
|
|
def generate_and_track():
|
|
with self._stats_lock:
|
|
self.generation_stats['generations'] += 1
|
|
self.generation_stats['cache_misses'] += 1
|
|
|
|
logger.info(f"Generating new fingerprint for account {account_id}")
|
|
return generator_func()
|
|
|
|
# Cache-Zugriff mit Statistik-Tracking
|
|
fingerprint = self.cache.get(cache_key)
|
|
if fingerprint is not None:
|
|
with self._stats_lock:
|
|
self.generation_stats['cache_hits'] += 1
|
|
logger.debug(f"Cache hit for account fingerprint: {account_id}")
|
|
return fingerprint
|
|
|
|
# Thread-safe generation
|
|
return self.cache.get_or_compute(cache_key, generate_and_track, ttl)
|
|
|
|
def get_anonymous_fingerprint(self, session_id: str,
|
|
generator_func: Callable[[], BrowserFingerprint],
|
|
ttl: Optional[timedelta] = None) -> BrowserFingerprint:
|
|
"""
|
|
Holt oder erstellt Session-gebundenen anonymen Fingerprint
|
|
"""
|
|
cache_key = f"session_{session_id}"
|
|
return self.cache.get_or_compute(cache_key, generator_func, ttl)
|
|
|
|
def get_platform_fingerprint(self, platform: str, profile_type: str,
|
|
generator_func: Callable[[], BrowserFingerprint],
|
|
ttl: Optional[timedelta] = None) -> BrowserFingerprint:
|
|
"""
|
|
Holt oder erstellt platform-spezifischen Fingerprint
|
|
"""
|
|
cache_key = f"platform_{platform}_{profile_type}"
|
|
return self.cache.get_or_compute(cache_key, generator_func, ttl)
|
|
|
|
def invalidate_account_fingerprint(self, account_id: str) -> bool:
|
|
"""Invalidiert Account-Fingerprint im Cache"""
|
|
return self.cache.invalidate(f"account_{account_id}")
|
|
|
|
def invalidate_session_fingerprint(self, session_id: str) -> bool:
|
|
"""Invalidiert Session-Fingerprint im Cache"""
|
|
return self.cache.invalidate(f"session_{session_id}")
|
|
|
|
def get_cache_stats(self) -> Dict[str, Any]:
|
|
"""Gibt detaillierte Cache-Statistiken zurück"""
|
|
with self._stats_lock:
|
|
stats = self.generation_stats.copy()
|
|
|
|
cache_stats = self.cache.get_stats()
|
|
|
|
# Hit rate berechnen
|
|
total_requests = stats['cache_hits'] + stats['cache_misses']
|
|
hit_rate = stats['cache_hits'] / total_requests if total_requests > 0 else 0
|
|
|
|
return {
|
|
**stats,
|
|
**cache_stats,
|
|
'hit_rate': hit_rate,
|
|
'total_requests': total_requests
|
|
}
|
|
|
|
def cleanup_expired(self) -> int:
|
|
"""Manuelle Bereinigung abgelaufener Einträge"""
|
|
initial_size = len(self.cache._cache)
|
|
|
|
with self.cache._main_lock:
|
|
expired_keys = [
|
|
key for key, entry in self.cache._cache.items()
|
|
if self.cache._is_expired(entry)
|
|
]
|
|
|
|
for key in expired_keys:
|
|
self.cache._remove_key(key)
|
|
|
|
removed_count = len(expired_keys)
|
|
if removed_count > 0:
|
|
logger.info(f"Cleaned up {removed_count} expired fingerprint cache entries")
|
|
|
|
return removed_count
|
|
|
|
|
|
# Global Cache Instance - Singleton Pattern
|
|
_global_fingerprint_cache: Optional[FingerprintCache] = None
|
|
_cache_init_lock = threading.RLock()
|
|
|
|
|
|
def get_fingerprint_cache() -> FingerprintCache:
|
|
"""
|
|
Holt die globale Fingerprint-Cache-Instanz (Singleton)
|
|
"""
|
|
global _global_fingerprint_cache
|
|
|
|
if _global_fingerprint_cache is None:
|
|
with _cache_init_lock:
|
|
if _global_fingerprint_cache is None:
|
|
_global_fingerprint_cache = FingerprintCache()
|
|
logger.info("Initialized global fingerprint cache")
|
|
|
|
return _global_fingerprint_cache
|
|
|
|
|
|
def reset_fingerprint_cache() -> None:
|
|
"""
|
|
Setzt den globalen Cache zurück (für Tests)
|
|
"""
|
|
global _global_fingerprint_cache
|
|
with _cache_init_lock:
|
|
if _global_fingerprint_cache is not None:
|
|
_global_fingerprint_cache.cache.clear()
|
|
_global_fingerprint_cache = None
|
|
logger.info("Reset global fingerprint cache")
|
|
|
|
|
|
class CachedFingerprintMixin:
|
|
"""
|
|
Mixin-Klasse die zu bestehenden Fingerprint-Services hinzugefügt werden kann
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._cache = get_fingerprint_cache()
|
|
|
|
def get_cached_account_fingerprint(self, account_id: str) -> Optional[BrowserFingerprint]:
|
|
"""Holt Account-Fingerprint aus Cache ohne Generation"""
|
|
return self._cache.cache.get(f"account_{account_id}")
|
|
|
|
def create_cached_account_fingerprint(self, account_id: str) -> BrowserFingerprint:
|
|
"""
|
|
Erstellt Account-Fingerprint mit Cache-Integration
|
|
Muss von Subklassen implementiert werden
|
|
"""
|
|
def generator():
|
|
# Delegiert an Original-Implementierung
|
|
if hasattr(super(), 'create_account_fingerprint'):
|
|
return super().create_account_fingerprint(account_id)
|
|
else:
|
|
raise NotImplementedError("Subclass must implement create_account_fingerprint")
|
|
|
|
return self._cache.get_account_fingerprint(account_id, generator)
|
|
|
|
def get_cache_statistics(self) -> Dict[str, Any]:
|
|
"""Gibt Cache-Statistiken zurück"""
|
|
return self._cache.get_cache_stats() |