""" 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()