362 Zeilen
14 KiB
Python
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 |