Dieser Commit ist enthalten in:
Claude Project Manager
2025-08-01 23:50:28 +02:00
Commit 04585e95b6
290 geänderte Dateien mit 64086 neuen und 0 gelöschten Zeilen

Datei anzeigen

@ -0,0 +1,330 @@
"""
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()