Initial commit
Dieser Commit ist enthalten in:
362
utils/thread_safety_mixins.py
Normale Datei
362
utils/thread_safety_mixins.py
Normale Datei
@ -0,0 +1,362 @@
|
||||
"""
|
||||
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
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren