Files
AccountForger-neuerUpload/utils/thread_safety_mixins.py
Claude Project Manager 04585e95b6 Initial commit
2025-08-01 23:50:28 +02:00

362 Zeilen
14 KiB
Python

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