| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630 |
- # -*- coding: utf-8 -*-
- """
- Worker-Pool für Event-Verarbeitung.
- Verwaltet Worker für parallele Event-Verarbeitung.
- """
- from __future__ import annotations
- import asyncio
- from dataclasses import dataclass, field
- from datetime import datetime
- from enum import Enum, auto
- from typing import TYPE_CHECKING, Any, Callable, Coroutine
- import uuid
- if TYPE_CHECKING:
- from trixy_core.events.event_data.base import EventData
- from trixy_core.events.async_queue.queue import AsyncEventQueue, QueuedEvent
- class WorkerState(Enum):
- """
- Zustand eines Workers.
- """
- IDLE = auto()
- """Worker wartet auf Arbeit."""
- BUSY = auto()
- """Worker verarbeitet Event."""
- STOPPED = auto()
- """Worker gestoppt."""
- FAILED = auto()
- """Worker fehlerhaft."""
- @dataclass
- class WorkerConfig:
- """
- Konfiguration für Worker.
- """
- idle_timeout_ms: float = 30000.0
- """Timeout für Idle-Worker."""
- max_consecutive_errors: int = 5
- """Maximale aufeinanderfolgende Fehler."""
- error_cooldown_ms: float = 1000.0
- """Wartezeit nach Fehler."""
- enable_metrics: bool = True
- """Metriken aktivieren."""
- @dataclass
- class WorkerStats:
- """
- Statistiken eines Workers.
- """
- worker_id: str
- """ID des Workers."""
- state: WorkerState = WorkerState.IDLE
- """Aktueller Zustand."""
- events_processed: int = 0
- """Verarbeitete Events."""
- events_failed: int = 0
- """Fehlgeschlagene Events."""
- total_processing_ms: float = 0.0
- """Gesamte Verarbeitungszeit."""
- last_event_at: datetime | None = None
- """Zeitpunkt des letzten Events."""
- consecutive_errors: int = 0
- """Aufeinanderfolgende Fehler."""
- started_at: datetime | None = None
- """Startzeit."""
- @property
- def avg_processing_ms(self) -> float:
- """Durchschnittliche Verarbeitungszeit."""
- if self.events_processed == 0:
- return 0.0
- return self.total_processing_ms / self.events_processed
- @property
- def uptime_seconds(self) -> float:
- """Laufzeit in Sekunden."""
- if self.started_at is None:
- return 0.0
- return (datetime.now() - self.started_at).total_seconds()
- def to_dict(self) -> dict[str, Any]:
- """Konvertiert in ein Dictionary."""
- return {
- "worker_id": self.worker_id,
- "state": self.state.name,
- "events_processed": self.events_processed,
- "events_failed": self.events_failed,
- "avg_processing_ms": self.avg_processing_ms,
- "uptime_seconds": self.uptime_seconds,
- "consecutive_errors": self.consecutive_errors,
- }
- class Worker:
- """
- Ein einzelner Event-Worker.
- Verarbeitet Events aus einer Queue.
- Example:
- worker = Worker(
- worker_id="worker-1",
- queue=event_queue,
- handler=process_event,
- )
- await worker.start()
- # ...
- await worker.stop()
- """
- def __init__(
- self,
- worker_id: str,
- queue: "AsyncEventQueue",
- handler: Callable[[str, "EventData"], Coroutine[Any, Any, None]],
- config: WorkerConfig | None = None,
- ) -> None:
- """
- Initialisiert den Worker.
- Args:
- worker_id: ID des Workers.
- queue: Die Event-Queue.
- handler: Handler-Funktion.
- config: Konfiguration.
- """
- self._id = worker_id
- self._queue = queue
- self._handler = handler
- self._config = config or WorkerConfig()
- self._task: asyncio.Task | None = None
- self._running = False
- self._stats = WorkerStats(worker_id=worker_id)
- @property
- def id(self) -> str:
- """Worker-ID."""
- return self._id
- @property
- def state(self) -> WorkerState:
- """Aktueller Zustand."""
- return self._stats.state
- @property
- def is_running(self) -> bool:
- """Ob Worker läuft."""
- return self._running
- @property
- def stats(self) -> WorkerStats:
- """Statistiken."""
- return self._stats
- async def start(self) -> None:
- """Startet den Worker."""
- if self._running:
- return
- self._running = True
- self._stats.state = WorkerState.IDLE
- self._stats.started_at = datetime.now()
- self._task = asyncio.create_task(self._run())
- async def stop(self, timeout: float = 5.0) -> None:
- """
- Stoppt den Worker.
- Args:
- timeout: Timeout in Sekunden.
- """
- self._running = False
- self._stats.state = WorkerState.STOPPED
- if self._task:
- try:
- await asyncio.wait_for(self._task, timeout=timeout)
- except asyncio.TimeoutError:
- self._task.cancel()
- self._task = None
- async def _run(self) -> None:
- """Worker-Hauptschleife."""
- while self._running:
- try:
- # Event holen
- queued = await self._queue.dequeue(
- timeout=self._config.idle_timeout_ms / 1000
- )
- if queued is None:
- # Timeout - weiter warten
- continue
- # Event verarbeiten
- self._stats.state = WorkerState.BUSY
- start_time = datetime.now()
- try:
- await self._handler(queued.event_name, queued.event_data)
- # Erfolg
- self._stats.events_processed += 1
- self._stats.consecutive_errors = 0
- except Exception as e:
- # Fehler
- self._stats.events_failed += 1
- self._stats.consecutive_errors += 1
- if self._stats.consecutive_errors >= self._config.max_consecutive_errors:
- self._stats.state = WorkerState.FAILED
- await asyncio.sleep(self._config.error_cooldown_ms / 1000)
- self._stats.state = WorkerState.IDLE
- finally:
- # Zeiterfassung
- duration_ms = (datetime.now() - start_time).total_seconds() * 1000
- self._stats.total_processing_ms += duration_ms
- self._stats.last_event_at = datetime.now()
- self._stats.state = WorkerState.IDLE
- except asyncio.CancelledError:
- break
- except Exception:
- await asyncio.sleep(0.1)
- @dataclass
- class PoolStats:
- """
- Statistiken des Worker-Pools.
- """
- total_workers: int = 0
- """Gesamtzahl Worker."""
- active_workers: int = 0
- """Aktive Worker."""
- idle_workers: int = 0
- """Idle Worker."""
- failed_workers: int = 0
- """Fehlerhafte Worker."""
- total_events_processed: int = 0
- """Gesamtzahl verarbeiteter Events."""
- total_events_failed: int = 0
- """Gesamtzahl fehlgeschlagener Events."""
- avg_processing_ms: float = 0.0
- """Durchschnittliche Verarbeitungszeit."""
- workers: list[dict[str, Any]] = field(default_factory=list)
- """Statistiken aller Worker."""
- class WorkerPool:
- """
- Pool von Event-Workern.
- Verwaltet mehrere Worker für parallele Verarbeitung.
- Example:
- pool = WorkerPool(
- queue=event_queue,
- handler=process_event,
- min_workers=2,
- max_workers=10,
- )
- await pool.start()
- # Dynamisch skalieren
- await pool.scale_up(2)
- await pool.scale_down(1)
- # Statistiken
- stats = pool.get_stats()
- print(f"Active workers: {stats.active_workers}")
- await pool.stop()
- """
- def __init__(
- self,
- queue: "AsyncEventQueue",
- handler: Callable[[str, "EventData"], Coroutine[Any, Any, None]],
- min_workers: int = 1,
- max_workers: int = 10,
- config: WorkerConfig | None = None,
- ) -> None:
- """
- Initialisiert den Pool.
- Args:
- queue: Die Event-Queue.
- handler: Handler-Funktion.
- min_workers: Minimale Anzahl Worker.
- max_workers: Maximale Anzahl Worker.
- config: Worker-Konfiguration.
- """
- self._queue = queue
- self._handler = handler
- self._min_workers = min_workers
- self._max_workers = max_workers
- self._config = config or WorkerConfig()
- self._workers: dict[str, Worker] = {}
- self._running = False
- self._worker_counter = 0
- self._lock = asyncio.Lock()
- @property
- def worker_count(self) -> int:
- """Anzahl der Worker."""
- return len(self._workers)
- @property
- def is_running(self) -> bool:
- """Ob Pool läuft."""
- return self._running
- def _create_worker_id(self) -> str:
- """Erstellt eine neue Worker-ID."""
- self._worker_counter += 1
- return f"worker-{self._worker_counter}"
- async def _create_worker(self) -> Worker:
- """Erstellt und startet einen neuen Worker."""
- worker_id = self._create_worker_id()
- worker = Worker(
- worker_id=worker_id,
- queue=self._queue,
- handler=self._handler,
- config=self._config,
- )
- await worker.start()
- self._workers[worker_id] = worker
- return worker
- async def _remove_worker(self, worker_id: str) -> bool:
- """Entfernt einen Worker."""
- worker = self._workers.pop(worker_id, None)
- if worker:
- await worker.stop()
- return True
- return False
- async def start(self) -> None:
- """Startet den Pool mit minimaler Worker-Anzahl."""
- if self._running:
- return
- self._running = True
- async with self._lock:
- for _ in range(self._min_workers):
- await self._create_worker()
- async def stop(self, timeout: float = 10.0) -> None:
- """
- Stoppt den Pool.
- Args:
- timeout: Timeout pro Worker.
- """
- self._running = False
- async with self._lock:
- for worker in list(self._workers.values()):
- await worker.stop(timeout=timeout)
- self._workers.clear()
- async def scale_up(self, count: int = 1) -> int:
- """
- Fügt Worker hinzu.
- Args:
- count: Anzahl neuer Worker.
- Returns:
- Anzahl tatsächlich hinzugefügter Worker.
- """
- async with self._lock:
- added = 0
- for _ in range(count):
- if len(self._workers) >= self._max_workers:
- break
- await self._create_worker()
- added += 1
- return added
- async def scale_down(self, count: int = 1) -> int:
- """
- Entfernt Worker.
- Args:
- count: Anzahl zu entfernender Worker.
- Returns:
- Anzahl tatsächlich entfernter Worker.
- """
- async with self._lock:
- removed = 0
- # Idle Worker bevorzugt entfernen
- idle_workers = [
- w for w in self._workers.values()
- if w.state == WorkerState.IDLE
- ]
- for worker in idle_workers[:count]:
- if len(self._workers) <= self._min_workers:
- break
- await self._remove_worker(worker.id)
- removed += 1
- return removed
- async def scale_to(self, target: int) -> int:
- """
- Skaliert auf eine bestimmte Worker-Anzahl.
- Args:
- target: Ziel-Anzahl.
- Returns:
- Differenz (positiv = hinzugefügt, negativ = entfernt).
- """
- target = max(self._min_workers, min(self._max_workers, target))
- current = len(self._workers)
- if target > current:
- return await self.scale_up(target - current)
- elif target < current:
- return -(await self.scale_down(current - target))
- return 0
- def get_worker(self, worker_id: str) -> Worker | None:
- """
- Gibt einen Worker zurück.
- Args:
- worker_id: ID des Workers.
- Returns:
- Worker oder None.
- """
- return self._workers.get(worker_id)
- def get_stats(self) -> PoolStats:
- """
- Gibt Pool-Statistiken zurück.
- Returns:
- PoolStats.
- """
- stats = PoolStats(total_workers=len(self._workers))
- total_processed = 0
- total_failed = 0
- total_processing_ms = 0.0
- for worker in self._workers.values():
- worker_stats = worker.stats
- if worker_stats.state == WorkerState.BUSY:
- stats.active_workers += 1
- elif worker_stats.state == WorkerState.IDLE:
- stats.idle_workers += 1
- elif worker_stats.state == WorkerState.FAILED:
- stats.failed_workers += 1
- total_processed += worker_stats.events_processed
- total_failed += worker_stats.events_failed
- total_processing_ms += worker_stats.total_processing_ms
- stats.workers.append(worker_stats.to_dict())
- stats.total_events_processed = total_processed
- stats.total_events_failed = total_failed
- if total_processed > 0:
- stats.avg_processing_ms = total_processing_ms / total_processed
- return stats
- async def restart_failed_workers(self) -> int:
- """
- Startet fehlerhafte Worker neu.
- Returns:
- Anzahl neu gestarteter Worker.
- """
- restarted = 0
- async with self._lock:
- failed_workers = [
- w for w in self._workers.values()
- if w.state == WorkerState.FAILED
- ]
- for worker in failed_workers:
- worker_id = worker.id
- await self._remove_worker(worker_id)
- await self._create_worker()
- restarted += 1
- return restarted
- class AutoScalingPool(WorkerPool):
- """
- Worker-Pool mit automatischer Skalierung.
- Passt die Worker-Anzahl basierend auf Queue-Größe an.
- Example:
- pool = AutoScalingPool(
- queue=event_queue,
- handler=process_event,
- min_workers=2,
- max_workers=20,
- scale_up_threshold=100,
- scale_down_threshold=10,
- )
- await pool.start()
- # Pool skaliert automatisch
- """
- def __init__(
- self,
- queue: "AsyncEventQueue",
- handler: Callable[[str, "EventData"], Coroutine[Any, Any, None]],
- min_workers: int = 1,
- max_workers: int = 10,
- config: WorkerConfig | None = None,
- scale_up_threshold: int = 100,
- scale_down_threshold: int = 10,
- check_interval_ms: float = 5000.0,
- ) -> None:
- """
- Initialisiert den Auto-Scaling-Pool.
- Args:
- queue: Die Event-Queue.
- handler: Handler-Funktion.
- min_workers: Minimale Anzahl Worker.
- max_workers: Maximale Anzahl Worker.
- config: Worker-Konfiguration.
- scale_up_threshold: Queue-Größe für Scale-Up.
- scale_down_threshold: Queue-Größe für Scale-Down.
- check_interval_ms: Prüfintervall in Millisekunden.
- """
- super().__init__(queue, handler, min_workers, max_workers, config)
- self._scale_up_threshold = scale_up_threshold
- self._scale_down_threshold = scale_down_threshold
- self._check_interval_ms = check_interval_ms
- self._scaler_task: asyncio.Task | None = None
- async def start(self) -> None:
- """Startet den Pool mit Auto-Scaler."""
- await super().start()
- async def scaler():
- while self._running:
- await asyncio.sleep(self._check_interval_ms / 1000)
- await self._auto_scale()
- self._scaler_task = asyncio.create_task(scaler())
- async def stop(self, timeout: float = 10.0) -> None:
- """Stoppt den Pool."""
- if self._scaler_task:
- self._scaler_task.cancel()
- try:
- await self._scaler_task
- except asyncio.CancelledError:
- pass
- self._scaler_task = None
- await super().stop(timeout)
- async def _auto_scale(self) -> None:
- """Führt automatische Skalierung durch."""
- queue_size = self._queue.size
- current_workers = len(self._workers)
- if queue_size >= self._scale_up_threshold:
- # Mehr Worker benötigt
- workers_needed = min(
- queue_size // self._scale_up_threshold,
- self._max_workers - current_workers,
- )
- if workers_needed > 0:
- await self.scale_up(workers_needed)
- elif queue_size <= self._scale_down_threshold:
- # Weniger Worker benötigt
- if current_workers > self._min_workers:
- await self.scale_down(1)
|