| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- # -*- coding: utf-8 -*-
- """
- Request-Response Korrelation.
- Verwaltet die Zuordnung von Anfragen zu Antworten
- über eindeutige Korrelations-IDs.
- """
- from __future__ import annotations
- import asyncio
- import logging
- import uuid
- from dataclasses import dataclass, field
- from datetime import datetime, timedelta
- from enum import Enum, auto
- from typing import Any, Awaitable, Callable, Generic, TypeVar
- T = TypeVar("T")
- class RequestState(Enum):
- """Zustand einer ausstehenden Anfrage."""
- PENDING = auto() # Wartet auf Antwort
- COMPLETED = auto() # Antwort erhalten
- TIMEOUT = auto() # Timeout
- CANCELLED = auto() # Abgebrochen
- ERROR = auto() # Fehler
- @dataclass
- class CorrelationConfig:
- """Konfiguration für Korrelations-Manager."""
- default_timeout: float = 30.0 # Standard-Timeout in Sekunden
- cleanup_interval: float = 60.0 # Interval für Cleanup alter Requests
- max_pending: int = 10000 # Maximale Anzahl ausstehender Requests
- id_generator: Callable[[], str] | None = None # Custom ID-Generator
- @dataclass
- class PendingRequest(Generic[T]):
- """
- Repräsentiert eine ausstehende Anfrage.
- Enthält alle Informationen über den Request und
- ermöglicht das Warten auf die Antwort.
- """
- correlation_id: str
- created_at: datetime = field(default_factory=datetime.now)
- timeout: float = 30.0
- state: RequestState = RequestState.PENDING
- result: T | None = None
- error: Exception | None = None
- metadata: dict[str, Any] = field(default_factory=dict)
- _future: asyncio.Future | None = field(default=None, repr=False)
- @property
- def is_complete(self) -> bool:
- """Prüft ob Request abgeschlossen ist."""
- return self.state in (
- RequestState.COMPLETED,
- RequestState.TIMEOUT,
- RequestState.CANCELLED,
- RequestState.ERROR,
- )
- @property
- def elapsed(self) -> timedelta:
- """Vergangene Zeit seit Erstellung."""
- return datetime.now() - self.created_at
- @property
- def elapsed_seconds(self) -> float:
- """Vergangene Zeit in Sekunden."""
- return self.elapsed.total_seconds()
- def is_expired(self) -> bool:
- """Prüft ob Request abgelaufen ist."""
- return self.elapsed_seconds > self.timeout
- class CorrelationManager(Generic[T]):
- """
- Manager für Request-Response Korrelation.
- Verwaltet ausstehende Anfragen und ordnet
- eingehende Antworten den richtigen Requests zu.
- Beispiel:
- manager = CorrelationManager[bytes]()
- # Request erstellen
- correlation_id = await manager.create_request()
- # Nachricht senden mit correlation_id
- await send_message(data, correlation_id)
- # Auf Antwort warten
- try:
- response = await manager.wait_for_response(correlation_id)
- except TimeoutError:
- print("Timeout")
- # Antwort empfangen (von anderem Task)
- manager.complete(correlation_id, response_data)
- """
- def __init__(
- self,
- config: CorrelationConfig | None = None,
- logger: logging.Logger | None = None,
- ) -> None:
- """
- Initialisiert den Korrelations-Manager.
- Args:
- config: Konfiguration
- logger: Logger-Instanz
- """
- self.config = config or CorrelationConfig()
- self.logger = logger or logging.getLogger(__name__)
- self._pending: dict[str, PendingRequest[T]] = {}
- self._lock = asyncio.Lock()
- self._cleanup_task: asyncio.Task | None = None
- # ID-Generator
- self._generate_id = (
- self.config.id_generator
- or self._default_id_generator
- )
- @staticmethod
- def _default_id_generator() -> str:
- """Standard-ID-Generator."""
- return str(uuid.uuid4())
- @property
- def pending_count(self) -> int:
- """Anzahl ausstehender Requests."""
- return len(self._pending)
- async def start_cleanup(self) -> None:
- """Startet den Cleanup-Task."""
- if self._cleanup_task is None:
- self._cleanup_task = asyncio.create_task(self._cleanup_loop())
- async def stop_cleanup(self) -> None:
- """Stoppt den Cleanup-Task."""
- if self._cleanup_task:
- self._cleanup_task.cancel()
- try:
- await self._cleanup_task
- except asyncio.CancelledError:
- pass
- self._cleanup_task = None
- async def _cleanup_loop(self) -> None:
- """Periodisches Cleanup abgelaufener Requests."""
- while True:
- try:
- await asyncio.sleep(self.config.cleanup_interval)
- await self.cleanup_expired()
- except asyncio.CancelledError:
- break
- except Exception as e:
- self.logger.error(f"Cleanup-Fehler: {e}")
- async def create_request(
- self,
- timeout: float | None = None,
- metadata: dict[str, Any] | None = None,
- ) -> str:
- """
- Erstellt einen neuen ausstehenden Request.
- Args:
- timeout: Timeout in Sekunden
- metadata: Zusätzliche Metadaten
- Returns:
- Korrelations-ID
- Raises:
- RuntimeError: Wenn Maximum erreicht
- """
- async with self._lock:
- if len(self._pending) >= self.config.max_pending:
- raise RuntimeError(
- f"Maximum ausstehende Requests erreicht: "
- f"{self.config.max_pending}"
- )
- correlation_id = self._generate_id()
- # Stelle sicher dass ID eindeutig ist
- while correlation_id in self._pending:
- correlation_id = self._generate_id()
- request = PendingRequest[T](
- correlation_id=correlation_id,
- timeout=timeout or self.config.default_timeout,
- metadata=metadata or {},
- _future=asyncio.get_event_loop().create_future(),
- )
- self._pending[correlation_id] = request
- return correlation_id
- async def wait_for_response(
- self,
- correlation_id: str,
- timeout: float | None = None,
- ) -> T:
- """
- Wartet auf Antwort für einen Request.
- Args:
- correlation_id: Korrelations-ID
- timeout: Optional: Override Timeout
- Returns:
- Response-Daten
- Raises:
- KeyError: Request nicht gefunden
- TimeoutError: Timeout erreicht
- Exception: Bei Fehler im Request
- """
- request = self._pending.get(correlation_id)
- if not request:
- raise KeyError(f"Request nicht gefunden: {correlation_id}")
- effective_timeout = timeout or request.timeout
- try:
- result = await asyncio.wait_for(
- request._future,
- timeout=effective_timeout,
- )
- return result
- except asyncio.TimeoutError:
- async with self._lock:
- if correlation_id in self._pending:
- request = self._pending[correlation_id]
- request.state = RequestState.TIMEOUT
- if request._future and not request._future.done():
- request._future.cancel()
- raise TimeoutError(
- f"Timeout für Request {correlation_id} "
- f"nach {effective_timeout}s"
- )
- def complete(
- self,
- correlation_id: str,
- result: T,
- ) -> bool:
- """
- Markiert Request als abgeschlossen mit Ergebnis.
- Args:
- correlation_id: Korrelations-ID
- result: Ergebnis/Antwort
- Returns:
- True wenn Request gefunden und abgeschlossen
- """
- request = self._pending.get(correlation_id)
- if not request:
- self.logger.warning(
- f"Unbekannte Korrelations-ID: {correlation_id}"
- )
- return False
- if request.is_complete:
- self.logger.warning(
- f"Request bereits abgeschlossen: {correlation_id}"
- )
- return False
- request.state = RequestState.COMPLETED
- request.result = result
- if request._future and not request._future.done():
- request._future.set_result(result)
- return True
- def fail(
- self,
- correlation_id: str,
- error: Exception,
- ) -> bool:
- """
- Markiert Request als fehlgeschlagen.
- Args:
- correlation_id: Korrelations-ID
- error: Fehler
- Returns:
- True wenn Request gefunden
- """
- request = self._pending.get(correlation_id)
- if not request:
- return False
- if request.is_complete:
- return False
- request.state = RequestState.ERROR
- request.error = error
- if request._future and not request._future.done():
- request._future.set_exception(error)
- return True
- def cancel(self, correlation_id: str) -> bool:
- """
- Bricht einen Request ab.
- Args:
- correlation_id: Korrelations-ID
- Returns:
- True wenn abgebrochen
- """
- request = self._pending.get(correlation_id)
- if not request:
- return False
- if request.is_complete:
- return False
- request.state = RequestState.CANCELLED
- if request._future and not request._future.done():
- request._future.cancel()
- return True
- async def remove(self, correlation_id: str) -> PendingRequest[T] | None:
- """
- Entfernt einen Request.
- Args:
- correlation_id: Korrelations-ID
- Returns:
- Entfernter Request oder None
- """
- async with self._lock:
- return self._pending.pop(correlation_id, None)
- def get(self, correlation_id: str) -> PendingRequest[T] | None:
- """Holt Request nach ID."""
- return self._pending.get(correlation_id)
- def has_pending(self, correlation_id: str) -> bool:
- """Prüft ob Request existiert."""
- return correlation_id in self._pending
- async def cleanup_expired(self) -> int:
- """
- Entfernt abgelaufene Requests.
- Returns:
- Anzahl entfernter Requests
- """
- to_remove = []
- async with self._lock:
- for cid, request in self._pending.items():
- if request.is_expired():
- to_remove.append(cid)
- if request._future and not request._future.done():
- request._future.cancel()
- for cid in to_remove:
- self._pending.pop(cid, None)
- if to_remove:
- self.logger.debug(f"Cleanup: {len(to_remove)} abgelaufene Requests entfernt")
- return len(to_remove)
- async def cancel_all(self) -> int:
- """
- Bricht alle ausstehenden Requests ab.
- Returns:
- Anzahl abgebrochener Requests
- """
- count = 0
- async with self._lock:
- for cid, request in self._pending.items():
- if not request.is_complete:
- request.state = RequestState.CANCELLED
- if request._future and not request._future.done():
- request._future.cancel()
- count += 1
- return count
- def get_pending_ids(self) -> list[str]:
- """Listet alle ausstehenden Request-IDs."""
- return [
- cid for cid, req in self._pending.items()
- if req.state == RequestState.PENDING
- ]
- def get_stats(self) -> dict[str, Any]:
- """Liefert Statistiken."""
- states = {}
- for request in self._pending.values():
- state_name = request.state.name
- states[state_name] = states.get(state_name, 0) + 1
- return {
- "total_pending": self.pending_count,
- "states": states,
- "oldest_request_age": max(
- (r.elapsed_seconds for r in self._pending.values()),
- default=0.0,
- ),
- }
- def __contains__(self, correlation_id: str) -> bool:
- return correlation_id in self._pending
- def __len__(self) -> int:
- return self.pending_count
|