""" Thread Safety Mixins - Non-intrusive thread safety for existing classes Opt-in thread safety without changing existing logic """ import threading import functools import time import weakref from typing import Any, Dict, Optional, Callable, Set from collections import defaultdict import logging logger = logging.getLogger(__name__) class ThreadSafetyMixin: """ Mixin-Klasse die zu bestehenden Klassen hinzugefügt werden kann für thread-sichere Operationen ohne Änderung der bestehenden Logik """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._operation_locks: Dict[str, threading.RLock] = {} self._lock_manager = threading.RLock() self._active_operations: Dict[str, Set[int]] = defaultdict(set) self._operation_stats = { 'total_operations': 0, 'concurrent_operations': 0, 'lock_acquisitions': 0, 'lock_contentions': 0 } self._stats_lock = threading.RLock() def _thread_safe_operation(self, operation_key: str, operation_func: Callable, *args, timeout: Optional[float] = None, **kwargs) -> Any: """ Wrapper für thread-sichere Operationen Args: operation_key: Eindeutiger Schlüssel für die Operation operation_func: Die auszuführende Funktion timeout: Optional timeout für Lock-Akquisition """ thread_id = threading.current_thread().ident start_time = time.time() # Operation-spezifischen Lock holen/erstellen with self._lock_manager: if operation_key not in self._operation_locks: self._operation_locks[operation_key] = threading.RLock() logger.debug(f"Created lock for operation: {operation_key}") operation_lock = self._operation_locks[operation_key] # Prüfen ob bereits aktive Operationen vorhanden active_count = len(self._active_operations[operation_key]) if active_count > 0: with self._stats_lock: self._operation_stats['lock_contentions'] += 1 logger.debug(f"Lock contention detected for {operation_key}: {active_count} active operations") # Lock akquirieren lock_acquired = False try: if timeout: lock_acquired = operation_lock.acquire(timeout=timeout) if not lock_acquired: raise TimeoutError(f"Failed to acquire lock for {operation_key} within {timeout}s") else: operation_lock.acquire() lock_acquired = True with self._stats_lock: self._operation_stats['lock_acquisitions'] += 1 self._operation_stats['total_operations'] += 1 # Thread zu aktiven Operationen hinzufügen with self._lock_manager: self._active_operations[operation_key].add(thread_id) concurrent_ops = len(self._active_operations[operation_key]) if concurrent_ops > 1: with self._stats_lock: self._operation_stats['concurrent_operations'] += 1 # Operation ausführen logger.debug(f"Executing thread-safe operation {operation_key} (thread: {thread_id})") result = operation_func(*args, **kwargs) execution_time = time.time() - start_time logger.debug(f"Completed operation {operation_key} in {execution_time:.3f}s") return result finally: # Thread aus aktiven Operationen entfernen with self._lock_manager: self._active_operations[operation_key].discard(thread_id) # Lock freigeben if lock_acquired: operation_lock.release() def _get_operation_stats(self) -> Dict[str, Any]: """Gibt Thread-Safety-Statistiken zurück""" with self._stats_lock: stats = self._operation_stats.copy() with self._lock_manager: active_ops = {key: len(threads) for key, threads in self._active_operations.items() if threads} return { **stats, 'active_operations': active_ops, 'total_locks': len(self._operation_locks), 'current_thread': threading.current_thread().ident } def _cleanup_inactive_locks(self) -> int: """Bereinigt Locks für inaktive Operationen""" cleaned_count = 0 with self._lock_manager: # Locks ohne aktive Operationen identifizieren inactive_operations = [ key for key, threads in self._active_operations.items() if not threads and key in self._operation_locks ] # Bereinigen for key in inactive_operations: if key in self._operation_locks: del self._operation_locks[key] cleaned_count += 1 if key in self._active_operations: del self._active_operations[key] if cleaned_count > 0: logger.debug(f"Cleaned up {cleaned_count} inactive operation locks") return cleaned_count def thread_safe_method(operation_key: Optional[str] = None, timeout: Optional[float] = None): """ Decorator für thread-sichere Methoden Args: operation_key: Eindeutiger Schlüssel (default: Methodenname) timeout: Timeout für Lock-Akquisition """ def decorator(func: Callable) -> Callable: @functools.wraps(func) def wrapper(self, *args, **kwargs): # Prüfen ob Objekt ThreadSafetyMixin hat if not hasattr(self, '_thread_safe_operation'): logger.warning(f"Object {type(self).__name__} does not have ThreadSafetyMixin") return func(self, *args, **kwargs) key = operation_key or f"{type(self).__name__}.{func.__name__}" return self._thread_safe_operation(key, func, self, *args, timeout=timeout, **kwargs) # Original-Methode verfügbar machen wrapper.original = func wrapper.is_thread_safe = True return wrapper return decorator class ResourcePoolMixin: """ Mixin für Pool-basierte Resource-Verwaltung (z.B. Browser-Sessions) """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._resource_pool: Dict[str, Any] = {} self._resource_locks: Dict[str, threading.RLock] = {} self._resource_usage: Dict[str, Dict[str, Any]] = {} self._pool_lock = threading.RLock() def _acquire_resource(self, resource_id: str, timeout: Optional[float] = None) -> bool: """ Akquiriert eine Resource aus dem Pool """ with self._pool_lock: if resource_id not in self._resource_locks: self._resource_locks[resource_id] = threading.RLock() resource_lock = self._resource_locks[resource_id] # Resource-Lock akquirieren if timeout: acquired = resource_lock.acquire(timeout=timeout) else: resource_lock.acquire() acquired = True if acquired: # Usage tracking thread_id = threading.current_thread().ident with self._pool_lock: if resource_id not in self._resource_usage: self._resource_usage[resource_id] = { 'acquired_by': thread_id, 'acquired_at': time.time(), 'usage_count': 0 } self._resource_usage[resource_id]['usage_count'] += 1 logger.debug(f"Acquired resource {resource_id} by thread {thread_id}") return acquired def _release_resource(self, resource_id: str) -> None: """ Gibt eine Resource zurück in den Pool """ thread_id = threading.current_thread().ident with self._pool_lock: if resource_id in self._resource_locks: self._resource_locks[resource_id].release() # Usage tracking aktualisieren if resource_id in self._resource_usage: usage_info = self._resource_usage[resource_id] usage_info['released_at'] = time.time() usage_duration = usage_info['released_at'] - usage_info['acquired_at'] logger.debug(f"Released resource {resource_id} by thread {thread_id} " f"(used for {usage_duration:.3f}s)") def _get_resource_stats(self) -> Dict[str, Any]: """Gibt Resource-Pool-Statistiken zurück""" with self._pool_lock: return { 'total_resources': len(self._resource_pool), 'active_locks': len(self._resource_locks), 'resource_usage': dict(self._resource_usage), 'available_resources': list(self._resource_pool.keys()) } class ConcurrencyControlMixin: """ Mixin für erweiterte Concurrency-Kontrolle """ def __init__(self, max_concurrent_operations: int = 10, *args, **kwargs): super().__init__(*args, **kwargs) self.max_concurrent_operations = max_concurrent_operations self._operation_semaphore = threading.Semaphore(max_concurrent_operations) self._active_operation_count = 0 self._operation_queue = [] self._concurrency_lock = threading.RLock() def _controlled_operation(self, operation_func: Callable, *args, priority: int = 5, **kwargs) -> Any: """ Führt Operation mit Concurrency-Kontrolle aus Args: operation_func: Auszuführende Funktion priority: Priorität (1=höchste, 10=niedrigste) """ thread_id = threading.current_thread().ident # Semaphore akquirieren (begrenzt gleichzeitige Operationen) logger.debug(f"Thread {thread_id} waiting for operation slot (priority: {priority})") acquired = self._operation_semaphore.acquire(timeout=30) # 30s timeout if not acquired: raise TimeoutError("Failed to acquire operation slot within timeout") try: with self._concurrency_lock: self._active_operation_count += 1 current_count = self._active_operation_count logger.debug(f"Thread {thread_id} executing operation " f"({current_count}/{self.max_concurrent_operations} slots used)") # Operation ausführen result = operation_func(*args, **kwargs) return result finally: with self._concurrency_lock: self._active_operation_count -= 1 self._operation_semaphore.release() logger.debug(f"Thread {thread_id} released operation slot") def _get_concurrency_stats(self) -> Dict[str, Any]: """Gibt Concurrency-Statistiken zurück""" with self._concurrency_lock: return { 'max_concurrent_operations': self.max_concurrent_operations, 'active_operations': self._active_operation_count, 'available_slots': self.max_concurrent_operations - self._active_operation_count, 'queue_length': len(self._operation_queue) } # Kombiniertes Mixin für vollständige Thread-Safety class FullThreadSafetyMixin(ThreadSafetyMixin, ResourcePoolMixin, ConcurrencyControlMixin): """ Vollständiges Thread-Safety-Mixin mit allen Features """ def __init__(self, max_concurrent_operations: int = 5, *args, **kwargs): super().__init__(max_concurrent_operations=max_concurrent_operations, *args, **kwargs) def get_complete_stats(self) -> Dict[str, Any]: """Gibt vollständige Thread-Safety-Statistiken zurück""" return { 'thread_safety': self._get_operation_stats(), 'resource_pool': self._get_resource_stats(), 'concurrency_control': self._get_concurrency_stats(), 'current_thread': { 'id': threading.current_thread().ident, 'name': threading.current_thread().name, 'is_daemon': threading.current_thread().daemon } } # Utility-Funktionen für bestehende Klassen def make_thread_safe(cls: type, method_names: list = None, operation_timeout: Optional[float] = None) -> type: """ Macht eine bestehende Klasse thread-safe durch dynamisches Mixin Args: cls: Klasse die thread-safe gemacht werden soll method_names: Liste der Methoden die geschützt werden sollen operation_timeout: Timeout für Lock-Akquisition """ # Neue Klasse mit ThreadSafetyMixin erstellen class ThreadSafeVersion(ThreadSafetyMixin, cls): pass ThreadSafeVersion.__name__ = f"ThreadSafe{cls.__name__}" ThreadSafeVersion.__qualname__ = f"ThreadSafe{cls.__qualname__}" # Methoden mit thread_safe_method decorator versehen if method_names: for method_name in method_names: if hasattr(ThreadSafeVersion, method_name): original_method = getattr(ThreadSafeVersion, method_name) decorated_method = thread_safe_method( operation_key=f"{cls.__name__}.{method_name}", timeout=operation_timeout )(original_method) setattr(ThreadSafeVersion, method_name, decorated_method) return ThreadSafeVersion