""" Base Integration Classes Provides circuit breaker pattern and base classes for external integrations. """ import time import asyncio from typing import Callable, Any, Optional, Dict from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum from ..config import mcp_config from storage.database import get_connection class CircuitState(Enum): """Circuit breaker states""" CLOSED = "closed" # Normal operation OPEN = "open" # Failing, reject requests HALF_OPEN = "half_open" # Testing if service recovered @dataclass class CircuitBreakerStats: """Circuit breaker statistics""" state: CircuitState failure_count: int success_count: int last_failure_time: Optional[float] last_success_time: Optional[float] opened_at: Optional[float] next_retry_time: Optional[float] class CircuitBreakerOpen(Exception): """Exception raised when circuit breaker is open""" pass class CircuitBreaker: """ Circuit Breaker pattern implementation. Protects external service calls from cascading failures. Three states: CLOSED (normal), OPEN (failing), HALF_OPEN (testing). """ def __init__( self, integration_type: str, failure_threshold: int = None, timeout_seconds: int = None, half_open_max_calls: int = 3 ): """ Args: integration_type: Type of integration (figma, jira, confluence, etc.) failure_threshold: Number of failures before opening circuit timeout_seconds: Seconds to wait before trying again half_open_max_calls: Max successful calls in half-open before closing """ self.integration_type = integration_type self.failure_threshold = failure_threshold or mcp_config.CIRCUIT_BREAKER_FAILURE_THRESHOLD self.timeout_seconds = timeout_seconds or mcp_config.CIRCUIT_BREAKER_TIMEOUT_SECONDS self.half_open_max_calls = half_open_max_calls # In-memory state (could be moved to Redis for distributed setup) self.state = CircuitState.CLOSED self.failure_count = 0 self.success_count = 0 self.last_failure_time: Optional[float] = None self.last_success_time: Optional[float] = None self.opened_at: Optional[float] = None async def call(self, func: Callable, *args, **kwargs) -> Any: """ Call a function through the circuit breaker. Args: func: Function to call (can be sync or async) *args, **kwargs: Arguments to pass to func Returns: Function result Raises: CircuitBreakerOpen: If circuit is open Exception: Original exception from func if it fails """ # Check circuit state if self.state == CircuitState.OPEN: # Check if timeout has elapsed if time.time() - self.opened_at < self.timeout_seconds: await self._record_failure("Circuit breaker is OPEN", db_only=True) raise CircuitBreakerOpen( f"{self.integration_type} service is temporarily unavailable. " f"Retry after {self._seconds_until_retry():.0f}s" ) else: # Timeout elapsed, move to HALF_OPEN self.state = CircuitState.HALF_OPEN self.success_count = 0 # Execute function try: # Handle both sync and async functions if asyncio.iscoroutinefunction(func): result = await func(*args, **kwargs) else: result = func(*args, **kwargs) # Success! await self._record_success() # If in HALF_OPEN, check if we can close the circuit if self.state == CircuitState.HALF_OPEN: if self.success_count >= self.half_open_max_calls: self.state = CircuitState.CLOSED self.failure_count = 0 return result except Exception as e: # Failure await self._record_failure(str(e)) # Check if we should open the circuit if self.failure_count >= self.failure_threshold: self.state = CircuitState.OPEN self.opened_at = time.time() raise async def _record_success(self): """Record successful call""" self.success_count += 1 self.last_success_time = time.time() # Update database await self._update_health_db(is_healthy=True, error=None) async def _record_failure(self, error_message: str, db_only: bool = False): """Record failed call""" if not db_only: self.failure_count += 1 self.last_failure_time = time.time() # Update database await self._update_health_db(is_healthy=False, error=error_message) async def _update_health_db(self, is_healthy: bool, error: Optional[str]): """Update integration health in database""" loop = asyncio.get_event_loop() def update_db(): try: with get_connection() as conn: circuit_open_until = None if self.state == CircuitState.OPEN and self.opened_at: circuit_open_until = datetime.fromtimestamp( self.opened_at + self.timeout_seconds ).isoformat() if is_healthy: conn.execute( """ UPDATE integration_health SET is_healthy = 1, failure_count = 0, last_success_at = CURRENT_TIMESTAMP, circuit_open_until = NULL, updated_at = CURRENT_TIMESTAMP WHERE integration_type = ? """, (self.integration_type,) ) else: conn.execute( """ UPDATE integration_health SET is_healthy = 0, failure_count = ?, last_failure_at = CURRENT_TIMESTAMP, circuit_open_until = ?, updated_at = CURRENT_TIMESTAMP WHERE integration_type = ? """, (self.failure_count, circuit_open_until, self.integration_type) ) except Exception as e: print(f"Error updating integration health: {e}") await loop.run_in_executor(None, update_db) def _seconds_until_retry(self) -> float: """Get seconds until circuit can be retried""" if self.state != CircuitState.OPEN or not self.opened_at: return 0 elapsed = time.time() - self.opened_at remaining = self.timeout_seconds - elapsed return max(0, remaining) def get_stats(self) -> CircuitBreakerStats: """Get current circuit breaker statistics""" next_retry_time = None if self.state == CircuitState.OPEN and self.opened_at: next_retry_time = self.opened_at + self.timeout_seconds return CircuitBreakerStats( state=self.state, failure_count=self.failure_count, success_count=self.success_count, last_failure_time=self.last_failure_time, last_success_time=self.last_success_time, opened_at=self.opened_at, next_retry_time=next_retry_time ) class BaseIntegration: """Base class for all external integrations""" def __init__(self, integration_type: str, config: Dict[str, Any]): """ Args: integration_type: Type of integration (figma, jira, etc.) config: Integration configuration (decrypted) """ self.integration_type = integration_type self.config = config self.circuit_breaker = CircuitBreaker(integration_type) async def call_api(self, func: Callable, *args, **kwargs) -> Any: """ Call external API through circuit breaker. Args: func: API function to call *args, **kwargs: Arguments to pass Returns: API response Raises: CircuitBreakerOpen: If circuit is open Exception: Original API exception """ return await self.circuit_breaker.call(func, *args, **kwargs) def get_health(self) -> Dict[str, Any]: """Get integration health status""" stats = self.circuit_breaker.get_stats() return { "integration_type": self.integration_type, "state": stats.state.value, "is_healthy": stats.state == CircuitState.CLOSED, "failure_count": stats.failure_count, "success_count": stats.success_count, "last_failure_time": stats.last_failure_time, "last_success_time": stats.last_success_time, "next_retry_time": stats.next_retry_time }