Terminal / api /health_manager.py
Baida-A
Initial clean deploy (Reverse Proxy removed)
28a08e7
Raw
History Blame
3.86 kB
import asyncio
import logging
import time
from enum import Enum
from typing import Dict, List, Any, Optional
from pydantic import BaseModel
_logger = logging.getLogger("api.health_manager")
class HealthStatus(str, Enum):
HEALTHY = "healthy"
DEGRADED = "degraded"
DOWN = "down"
class ComponentHealth(BaseModel):
id: str
type: str # "worker" | "provider" | "service"
status: HealthStatus = HealthStatus.HEALTHY
failure_count: int = 0
last_check: float = 0.0
latency: float = 0.0
error_message: Optional[str] = None
class HealthManager:
"""
ARCH-P5.1: Health Manager
Gestisce il monitoraggio, il Circuit Breaker e il Traffic Management.
"""
def __init__(self):
self.components: Dict[str, ComponentHealth] = {}
self._lock = asyncio.Lock()
self.failure_threshold = 5 # Numero di errori prima di aprire il circuit
self.recovery_timeout = 60 # Secondi prima di riprovare un componente DOWN
async def record_success(self, component_id: str, latency: float = 0.0, component_type: str = "worker"):
"""Registra un'operazione riuscita per un componente."""
async with self._lock:
if component_id not in self.components:
self.components[component_id] = ComponentHealth(id=component_id, type=component_type)
c = self.components[component_id]
c.status = HealthStatus.HEALTHY
c.failure_count = 0
c.last_check = time.time()
c.latency = latency
c.error_message = None
async def record_failure(self, component_id: str, error: str, component_type: str = "worker"):
"""Registra un fallimento e attiva il circuit breaker se necessario."""
async with self._lock:
if component_id not in self.components:
self.components[component_id] = ComponentHealth(id=component_id, type=component_type)
c = self.components[component_id]
c.failure_count += 1
c.last_check = time.time()
c.error_message = error
if c.failure_count >= self.failure_threshold:
if c.status != HealthStatus.DOWN:
_logger.warning(f"Circuit Breaker APERTO per {component_id}: {error}")
c.status = HealthStatus.DOWN
elif c.failure_count >= 2:
c.status = HealthStatus.DEGRADED
async def is_healthy(self, component_id: str) -> bool:
"""Verifica se un componente è sano (o se è tempo di riprovare)."""
async with self._lock:
if component_id not in self.components:
return True
c = self.components[component_id]
if c.status == HealthStatus.DOWN:
# Half-open state: riprova dopo il timeout
if time.time() - c.last_check > self.recovery_timeout:
_logger.info(f"Circuit Breaker HALF-OPEN per {component_id} (tentativo di recovery)")
return True
return False
return True
async def get_status(self) -> Dict[str, Any]:
"""Ritorna lo stato aggregato di salute del sistema."""
async with self._lock:
return {
"ts": time.time(),
"components": {k: v.dict() for k, v in self.components.items()},
"summary": {
"healthy": sum(1 for c in self.components.values() if c.status == HealthStatus.HEALTHY),
"degraded": sum(1 for c in self.components.values() if c.status == HealthStatus.DEGRADED),
"down": sum(1 for c in self.components.values() if c.status == HealthStatus.DOWN),
}
}
# Singleton instance
health_manager = HealthManager()