isa-model 0.3.91__py3-none-any.whl → 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- isa_model/client.py +1166 -584
- isa_model/core/cache/redis_cache.py +410 -0
- isa_model/core/config/config_manager.py +282 -12
- isa_model/core/config.py +91 -1
- isa_model/core/database/__init__.py +1 -0
- isa_model/core/database/direct_db_client.py +114 -0
- isa_model/core/database/migration_manager.py +563 -0
- isa_model/core/database/migrations.py +297 -0
- isa_model/core/database/supabase_client.py +258 -0
- isa_model/core/dependencies.py +316 -0
- isa_model/core/discovery/__init__.py +19 -0
- isa_model/core/discovery/consul_discovery.py +190 -0
- isa_model/core/logging/__init__.py +54 -0
- isa_model/core/logging/influx_logger.py +523 -0
- isa_model/core/logging/loki_logger.py +160 -0
- isa_model/core/models/__init__.py +46 -0
- isa_model/core/models/config_models.py +625 -0
- isa_model/core/models/deployment_billing_tracker.py +430 -0
- isa_model/core/models/model_billing_tracker.py +60 -88
- isa_model/core/models/model_manager.py +66 -25
- isa_model/core/models/model_metadata.py +690 -0
- isa_model/core/models/model_repo.py +217 -55
- isa_model/core/models/model_statistics_tracker.py +234 -0
- isa_model/core/models/model_storage.py +0 -1
- isa_model/core/models/model_version_manager.py +959 -0
- isa_model/core/models/system_models.py +857 -0
- isa_model/core/pricing_manager.py +2 -249
- isa_model/core/repositories/__init__.py +9 -0
- isa_model/core/repositories/config_repository.py +912 -0
- isa_model/core/resilience/circuit_breaker.py +366 -0
- isa_model/core/security/secrets.py +358 -0
- isa_model/core/services/__init__.py +2 -4
- isa_model/core/services/intelligent_model_selector.py +479 -370
- isa_model/core/storage/hf_storage.py +2 -2
- isa_model/core/types.py +8 -0
- isa_model/deployment/__init__.py +5 -48
- isa_model/deployment/core/__init__.py +2 -31
- isa_model/deployment/core/deployment_manager.py +1278 -368
- isa_model/deployment/local/__init__.py +31 -0
- isa_model/deployment/local/config.py +248 -0
- isa_model/deployment/local/gpu_gateway.py +607 -0
- isa_model/deployment/local/health_checker.py +428 -0
- isa_model/deployment/local/provider.py +586 -0
- isa_model/deployment/local/tensorrt_service.py +621 -0
- isa_model/deployment/local/transformers_service.py +644 -0
- isa_model/deployment/local/vllm_service.py +527 -0
- isa_model/deployment/modal/__init__.py +8 -0
- isa_model/deployment/modal/config.py +136 -0
- isa_model/deployment/modal/deployer.py +894 -0
- isa_model/deployment/modal/services/__init__.py +3 -0
- isa_model/deployment/modal/services/audio/__init__.py +1 -0
- isa_model/deployment/modal/services/audio/isa_audio_chatTTS_service.py +520 -0
- isa_model/deployment/modal/services/audio/isa_audio_openvoice_service.py +758 -0
- isa_model/deployment/modal/services/audio/isa_audio_service_v2.py +1044 -0
- isa_model/deployment/modal/services/embedding/__init__.py +1 -0
- isa_model/deployment/modal/services/embedding/isa_embed_rerank_service.py +296 -0
- isa_model/deployment/modal/services/llm/__init__.py +1 -0
- isa_model/deployment/modal/services/llm/isa_llm_service.py +424 -0
- isa_model/deployment/modal/services/video/__init__.py +1 -0
- isa_model/deployment/modal/services/video/isa_video_hunyuan_service.py +423 -0
- isa_model/deployment/modal/services/vision/__init__.py +1 -0
- isa_model/deployment/modal/services/vision/isa_vision_ocr_service.py +519 -0
- isa_model/deployment/modal/services/vision/isa_vision_qwen25_service.py +709 -0
- isa_model/deployment/modal/services/vision/isa_vision_table_service.py +676 -0
- isa_model/deployment/modal/services/vision/isa_vision_ui_service.py +833 -0
- isa_model/deployment/modal/services/vision/isa_vision_ui_service_optimized.py +660 -0
- isa_model/deployment/models/org-org-acme-corp-tenant-a-service-llm-20250825-225822/tenant-a-service_modal_service.py +48 -0
- isa_model/deployment/models/org-test-org-123-prefix-test-service-llm-20250825-225822/prefix-test-service_modal_service.py +48 -0
- isa_model/deployment/models/test-llm-service-llm-20250825-204442/test-llm-service_modal_service.py +48 -0
- isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-212906/test-monitoring-gpt2_modal_service.py +48 -0
- isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-213009/test-monitoring-gpt2_modal_service.py +48 -0
- isa_model/deployment/storage/__init__.py +5 -0
- isa_model/deployment/storage/deployment_repository.py +824 -0
- isa_model/deployment/triton/__init__.py +10 -0
- isa_model/deployment/triton/config.py +196 -0
- isa_model/deployment/triton/configs/__init__.py +1 -0
- isa_model/deployment/triton/provider.py +512 -0
- isa_model/deployment/triton/scripts/__init__.py +1 -0
- isa_model/deployment/triton/templates/__init__.py +1 -0
- isa_model/inference/__init__.py +47 -1
- isa_model/inference/ai_factory.py +179 -16
- isa_model/inference/legacy_services/__init__.py +21 -0
- isa_model/inference/legacy_services/model_evaluation.py +637 -0
- isa_model/inference/legacy_services/model_service.py +573 -0
- isa_model/inference/legacy_services/model_serving.py +717 -0
- isa_model/inference/legacy_services/model_training.py +561 -0
- isa_model/inference/models/__init__.py +21 -0
- isa_model/inference/models/inference_config.py +551 -0
- isa_model/inference/models/inference_record.py +675 -0
- isa_model/inference/models/performance_models.py +714 -0
- isa_model/inference/repositories/__init__.py +9 -0
- isa_model/inference/repositories/inference_repository.py +828 -0
- isa_model/inference/services/audio/__init__.py +21 -0
- isa_model/inference/services/audio/base_realtime_service.py +225 -0
- isa_model/inference/services/audio/base_stt_service.py +184 -11
- isa_model/inference/services/audio/isa_tts_service.py +0 -0
- isa_model/inference/services/audio/openai_realtime_service.py +320 -124
- isa_model/inference/services/audio/openai_stt_service.py +53 -11
- isa_model/inference/services/base_service.py +17 -1
- isa_model/inference/services/custom_model_manager.py +277 -0
- isa_model/inference/services/embedding/__init__.py +13 -0
- isa_model/inference/services/embedding/base_embed_service.py +111 -8
- isa_model/inference/services/embedding/isa_embed_service.py +305 -0
- isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
- isa_model/inference/services/embedding/openai_embed_service.py +2 -4
- isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
- isa_model/inference/services/embedding/tests/test_embedding.py +222 -0
- isa_model/inference/services/img/__init__.py +2 -2
- isa_model/inference/services/img/base_image_gen_service.py +24 -7
- isa_model/inference/services/img/replicate_image_gen_service.py +84 -422
- isa_model/inference/services/img/services/replicate_face_swap.py +193 -0
- isa_model/inference/services/img/services/replicate_flux.py +226 -0
- isa_model/inference/services/img/services/replicate_flux_kontext.py +219 -0
- isa_model/inference/services/img/services/replicate_sticker_maker.py +249 -0
- isa_model/inference/services/img/tests/test_img_client.py +297 -0
- isa_model/inference/services/llm/__init__.py +10 -2
- isa_model/inference/services/llm/base_llm_service.py +361 -26
- isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
- isa_model/inference/services/llm/helpers/llm_adapter.py +71 -12
- isa_model/inference/services/llm/helpers/llm_prompts.py +342 -0
- isa_model/inference/services/llm/helpers/llm_utils.py +321 -23
- isa_model/inference/services/llm/huggingface_llm_service.py +581 -0
- isa_model/inference/services/llm/local_llm_service.py +747 -0
- isa_model/inference/services/llm/ollama_llm_service.py +11 -3
- isa_model/inference/services/llm/openai_llm_service.py +670 -56
- isa_model/inference/services/llm/yyds_llm_service.py +10 -3
- isa_model/inference/services/vision/__init__.py +27 -6
- isa_model/inference/services/vision/base_vision_service.py +118 -185
- isa_model/inference/services/vision/blip_vision_service.py +359 -0
- isa_model/inference/services/vision/helpers/image_utils.py +19 -10
- isa_model/inference/services/vision/isa_vision_service.py +634 -0
- isa_model/inference/services/vision/openai_vision_service.py +19 -10
- isa_model/inference/services/vision/tests/test_ocr_client.py +284 -0
- isa_model/inference/services/vision/vgg16_vision_service.py +257 -0
- isa_model/serving/api/cache_manager.py +245 -0
- isa_model/serving/api/dependencies/__init__.py +1 -0
- isa_model/serving/api/dependencies/auth.py +194 -0
- isa_model/serving/api/dependencies/database.py +139 -0
- isa_model/serving/api/error_handlers.py +284 -0
- isa_model/serving/api/fastapi_server.py +240 -18
- isa_model/serving/api/middleware/auth.py +317 -0
- isa_model/serving/api/middleware/security.py +268 -0
- isa_model/serving/api/middleware/tenant_context.py +414 -0
- isa_model/serving/api/routes/analytics.py +489 -0
- isa_model/serving/api/routes/config.py +645 -0
- isa_model/serving/api/routes/deployment_billing.py +315 -0
- isa_model/serving/api/routes/deployments.py +475 -0
- isa_model/serving/api/routes/gpu_gateway.py +440 -0
- isa_model/serving/api/routes/health.py +32 -12
- isa_model/serving/api/routes/inference_monitoring.py +486 -0
- isa_model/serving/api/routes/local_deployments.py +448 -0
- isa_model/serving/api/routes/logs.py +430 -0
- isa_model/serving/api/routes/settings.py +582 -0
- isa_model/serving/api/routes/tenants.py +575 -0
- isa_model/serving/api/routes/unified.py +992 -171
- isa_model/serving/api/routes/webhooks.py +479 -0
- isa_model/serving/api/startup.py +318 -0
- isa_model/serving/modal_proxy_server.py +249 -0
- isa_model/utils/gpu_utils.py +311 -0
- {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/METADATA +76 -22
- isa_model-0.4.3.dist-info/RECORD +193 -0
- isa_model/deployment/cloud/__init__.py +0 -9
- isa_model/deployment/cloud/modal/__init__.py +0 -10
- isa_model/deployment/cloud/modal/isa_vision_doc_service.py +0 -766
- isa_model/deployment/cloud/modal/isa_vision_table_service.py +0 -532
- isa_model/deployment/cloud/modal/isa_vision_ui_service.py +0 -406
- isa_model/deployment/cloud/modal/register_models.py +0 -321
- isa_model/deployment/core/deployment_config.py +0 -356
- isa_model/deployment/core/isa_deployment_service.py +0 -401
- isa_model/deployment/gpu_int8_ds8/app/server.py +0 -66
- isa_model/deployment/gpu_int8_ds8/scripts/test_client.py +0 -43
- isa_model/deployment/gpu_int8_ds8/scripts/test_client_os.py +0 -35
- isa_model/deployment/runtime/deployed_service.py +0 -338
- isa_model/deployment/services/__init__.py +0 -9
- isa_model/deployment/services/auto_deploy_vision_service.py +0 -538
- isa_model/deployment/services/model_service.py +0 -332
- isa_model/deployment/services/service_monitor.py +0 -356
- isa_model/deployment/services/service_registry.py +0 -527
- isa_model/eval/__init__.py +0 -92
- isa_model/eval/benchmarks.py +0 -469
- isa_model/eval/config/__init__.py +0 -10
- isa_model/eval/config/evaluation_config.py +0 -108
- isa_model/eval/evaluators/__init__.py +0 -18
- isa_model/eval/evaluators/base_evaluator.py +0 -503
- isa_model/eval/evaluators/llm_evaluator.py +0 -472
- isa_model/eval/factory.py +0 -531
- isa_model/eval/infrastructure/__init__.py +0 -24
- isa_model/eval/infrastructure/experiment_tracker.py +0 -466
- isa_model/eval/metrics.py +0 -798
- isa_model/inference/adapter/unified_api.py +0 -248
- isa_model/inference/services/helpers/stacked_config.py +0 -148
- isa_model/inference/services/img/flux_professional_service.py +0 -603
- isa_model/inference/services/img/helpers/base_stacked_service.py +0 -274
- isa_model/inference/services/others/table_transformer_service.py +0 -61
- isa_model/inference/services/vision/doc_analysis_service.py +0 -640
- isa_model/inference/services/vision/helpers/base_stacked_service.py +0 -274
- isa_model/inference/services/vision/ui_analysis_service.py +0 -823
- isa_model/scripts/inference_tracker.py +0 -283
- isa_model/scripts/mlflow_manager.py +0 -379
- isa_model/scripts/model_registry.py +0 -465
- isa_model/scripts/register_models.py +0 -370
- isa_model/scripts/register_models_with_embeddings.py +0 -510
- isa_model/scripts/start_mlflow.py +0 -95
- isa_model/scripts/training_tracker.py +0 -257
- isa_model/training/__init__.py +0 -74
- isa_model/training/annotation/annotation_schema.py +0 -47
- isa_model/training/annotation/processors/annotation_processor.py +0 -126
- isa_model/training/annotation/storage/dataset_manager.py +0 -131
- isa_model/training/annotation/storage/dataset_schema.py +0 -44
- isa_model/training/annotation/tests/test_annotation_flow.py +0 -109
- isa_model/training/annotation/tests/test_minio copy.py +0 -113
- isa_model/training/annotation/tests/test_minio_upload.py +0 -43
- isa_model/training/annotation/views/annotation_controller.py +0 -158
- isa_model/training/cloud/__init__.py +0 -22
- isa_model/training/cloud/job_orchestrator.py +0 -402
- isa_model/training/cloud/runpod_trainer.py +0 -454
- isa_model/training/cloud/storage_manager.py +0 -482
- isa_model/training/core/__init__.py +0 -23
- isa_model/training/core/config.py +0 -181
- isa_model/training/core/dataset.py +0 -222
- isa_model/training/core/trainer.py +0 -720
- isa_model/training/core/utils.py +0 -213
- isa_model/training/factory.py +0 -424
- isa_model-0.3.91.dist-info/RECORD +0 -138
- /isa_model/{core/storage/minio_storage.py → deployment/modal/services/audio/isa_audio_fish_service.py} +0 -0
- /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
- {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/WHEEL +0 -0
- {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,366 @@
|
|
1
|
+
"""
|
2
|
+
Circuit Breaker Implementation for External Service Calls
|
3
|
+
|
4
|
+
Provides resilience patterns including:
|
5
|
+
- Circuit breaker for external API calls
|
6
|
+
- Retry with exponential backoff
|
7
|
+
- Timeout handling
|
8
|
+
- Health monitoring
|
9
|
+
"""
|
10
|
+
|
11
|
+
import asyncio
|
12
|
+
import time
|
13
|
+
import logging
|
14
|
+
import functools
|
15
|
+
from typing import Dict, Any, Optional, Callable, Union, List
|
16
|
+
from enum import Enum
|
17
|
+
from dataclasses import dataclass, field
|
18
|
+
from circuitbreaker import circuit
|
19
|
+
import structlog
|
20
|
+
|
21
|
+
logger = structlog.get_logger(__name__)
|
22
|
+
|
23
|
+
class CircuitState(Enum):
|
24
|
+
CLOSED = "closed" # Normal operation
|
25
|
+
OPEN = "open" # Circuit breaker triggered, blocking calls
|
26
|
+
HALF_OPEN = "half_open" # Testing if service is recovered
|
27
|
+
|
28
|
+
@dataclass
|
29
|
+
class CircuitBreakerConfig:
|
30
|
+
"""Configuration for circuit breaker"""
|
31
|
+
failure_threshold: int = 5 # Number of failures before opening
|
32
|
+
recovery_timeout: int = 30 # Seconds before trying to recover
|
33
|
+
expected_exception: type = Exception # Exception type that triggers circuit breaker
|
34
|
+
success_threshold: int = 3 # Successful calls needed to close circuit
|
35
|
+
timeout: float = 30.0 # Request timeout in seconds
|
36
|
+
|
37
|
+
@dataclass
|
38
|
+
class CircuitBreakerStats:
|
39
|
+
"""Circuit breaker statistics"""
|
40
|
+
total_calls: int = 0
|
41
|
+
successful_calls: int = 0
|
42
|
+
failed_calls: int = 0
|
43
|
+
consecutive_failures: int = 0
|
44
|
+
consecutive_successes: int = 0
|
45
|
+
last_failure_time: Optional[float] = None
|
46
|
+
last_success_time: Optional[float] = None
|
47
|
+
state_changes: List[Dict[str, Any]] = field(default_factory=list)
|
48
|
+
|
49
|
+
class EnhancedCircuitBreaker:
|
50
|
+
"""Enhanced circuit breaker with monitoring and configuration"""
|
51
|
+
|
52
|
+
def __init__(self, name: str, config: CircuitBreakerConfig):
|
53
|
+
self.name = name
|
54
|
+
self.config = config
|
55
|
+
self.stats = CircuitBreakerStats()
|
56
|
+
self.state = CircuitState.CLOSED
|
57
|
+
self.last_state_change = time.time()
|
58
|
+
|
59
|
+
# Create underlying circuit breaker
|
60
|
+
self._circuit = circuit(
|
61
|
+
failure_threshold=config.failure_threshold,
|
62
|
+
recovery_timeout=config.recovery_timeout,
|
63
|
+
expected_exception=config.expected_exception
|
64
|
+
)
|
65
|
+
|
66
|
+
logger.info("Circuit breaker initialized", name=name, config=config)
|
67
|
+
|
68
|
+
def __call__(self, func: Callable):
|
69
|
+
"""Decorator to wrap functions with circuit breaker"""
|
70
|
+
|
71
|
+
@functools.wraps(func)
|
72
|
+
async def wrapper(*args, **kwargs):
|
73
|
+
return await self.call(func, *args, **kwargs)
|
74
|
+
|
75
|
+
# Apply the circuit breaker decorator
|
76
|
+
wrapped_func = self._circuit(wrapper)
|
77
|
+
return wrapped_func
|
78
|
+
|
79
|
+
async def call(self, func: Callable, *args, **kwargs):
|
80
|
+
"""Execute function with circuit breaker protection"""
|
81
|
+
start_time = time.time()
|
82
|
+
|
83
|
+
try:
|
84
|
+
# Record call attempt
|
85
|
+
self.stats.total_calls += 1
|
86
|
+
|
87
|
+
# Check if circuit is open
|
88
|
+
if self.state == CircuitState.OPEN:
|
89
|
+
if time.time() - self.last_state_change < self.config.recovery_timeout:
|
90
|
+
raise CircuitBreakerOpenException(
|
91
|
+
f"Circuit breaker '{self.name}' is open"
|
92
|
+
)
|
93
|
+
else:
|
94
|
+
# Try to move to half-open state
|
95
|
+
self._change_state(CircuitState.HALF_OPEN)
|
96
|
+
|
97
|
+
# Execute the function with timeout
|
98
|
+
try:
|
99
|
+
if asyncio.iscoroutinefunction(func):
|
100
|
+
result = await asyncio.wait_for(
|
101
|
+
func(*args, **kwargs),
|
102
|
+
timeout=self.config.timeout
|
103
|
+
)
|
104
|
+
else:
|
105
|
+
result = func(*args, **kwargs)
|
106
|
+
|
107
|
+
# Record success
|
108
|
+
self._record_success()
|
109
|
+
|
110
|
+
return result
|
111
|
+
|
112
|
+
except asyncio.TimeoutError:
|
113
|
+
self._record_failure()
|
114
|
+
raise CircuitBreakerTimeoutException(
|
115
|
+
f"Timeout after {self.config.timeout}s for '{self.name}'"
|
116
|
+
)
|
117
|
+
except self.config.expected_exception as e:
|
118
|
+
self._record_failure()
|
119
|
+
raise
|
120
|
+
|
121
|
+
except Exception as e:
|
122
|
+
duration = time.time() - start_time
|
123
|
+
logger.error(
|
124
|
+
"Circuit breaker call failed",
|
125
|
+
name=self.name,
|
126
|
+
error=str(e),
|
127
|
+
duration=duration,
|
128
|
+
state=self.state.value
|
129
|
+
)
|
130
|
+
raise
|
131
|
+
|
132
|
+
def _record_success(self):
|
133
|
+
"""Record successful call"""
|
134
|
+
self.stats.successful_calls += 1
|
135
|
+
self.stats.consecutive_successes += 1
|
136
|
+
self.stats.consecutive_failures = 0
|
137
|
+
self.stats.last_success_time = time.time()
|
138
|
+
|
139
|
+
# If we're in half-open state and have enough successes, close the circuit
|
140
|
+
if (self.state == CircuitState.HALF_OPEN and
|
141
|
+
self.stats.consecutive_successes >= self.config.success_threshold):
|
142
|
+
self._change_state(CircuitState.CLOSED)
|
143
|
+
|
144
|
+
logger.debug(
|
145
|
+
"Circuit breaker success",
|
146
|
+
name=self.name,
|
147
|
+
consecutive_successes=self.stats.consecutive_successes,
|
148
|
+
state=self.state.value
|
149
|
+
)
|
150
|
+
|
151
|
+
def _record_failure(self):
|
152
|
+
"""Record failed call"""
|
153
|
+
self.stats.failed_calls += 1
|
154
|
+
self.stats.consecutive_failures += 1
|
155
|
+
self.stats.consecutive_successes = 0
|
156
|
+
self.stats.last_failure_time = time.time()
|
157
|
+
|
158
|
+
# Check if we should open the circuit
|
159
|
+
if (self.state == CircuitState.CLOSED and
|
160
|
+
self.stats.consecutive_failures >= self.config.failure_threshold):
|
161
|
+
self._change_state(CircuitState.OPEN)
|
162
|
+
elif self.state == CircuitState.HALF_OPEN:
|
163
|
+
# Any failure in half-open state reopens the circuit
|
164
|
+
self._change_state(CircuitState.OPEN)
|
165
|
+
|
166
|
+
logger.warning(
|
167
|
+
"Circuit breaker failure",
|
168
|
+
name=self.name,
|
169
|
+
consecutive_failures=self.stats.consecutive_failures,
|
170
|
+
state=self.state.value
|
171
|
+
)
|
172
|
+
|
173
|
+
def _change_state(self, new_state: CircuitState):
|
174
|
+
"""Change circuit breaker state"""
|
175
|
+
old_state = self.state
|
176
|
+
self.state = new_state
|
177
|
+
self.last_state_change = time.time()
|
178
|
+
|
179
|
+
# Record state change
|
180
|
+
state_change = {
|
181
|
+
"from_state": old_state.value,
|
182
|
+
"to_state": new_state.value,
|
183
|
+
"timestamp": self.last_state_change,
|
184
|
+
"total_calls": self.stats.total_calls,
|
185
|
+
"consecutive_failures": self.stats.consecutive_failures
|
186
|
+
}
|
187
|
+
self.stats.state_changes.append(state_change)
|
188
|
+
|
189
|
+
logger.warning(
|
190
|
+
"Circuit breaker state changed",
|
191
|
+
name=self.name,
|
192
|
+
from_state=old_state.value,
|
193
|
+
to_state=new_state.value,
|
194
|
+
consecutive_failures=self.stats.consecutive_failures
|
195
|
+
)
|
196
|
+
|
197
|
+
def get_stats(self) -> Dict[str, Any]:
|
198
|
+
"""Get circuit breaker statistics"""
|
199
|
+
return {
|
200
|
+
"name": self.name,
|
201
|
+
"state": self.state.value,
|
202
|
+
"total_calls": self.stats.total_calls,
|
203
|
+
"successful_calls": self.stats.successful_calls,
|
204
|
+
"failed_calls": self.stats.failed_calls,
|
205
|
+
"success_rate": (
|
206
|
+
self.stats.successful_calls / self.stats.total_calls
|
207
|
+
if self.stats.total_calls > 0 else 0
|
208
|
+
),
|
209
|
+
"consecutive_failures": self.stats.consecutive_failures,
|
210
|
+
"consecutive_successes": self.stats.consecutive_successes,
|
211
|
+
"last_failure_time": self.stats.last_failure_time,
|
212
|
+
"last_success_time": self.stats.last_success_time,
|
213
|
+
"last_state_change": self.last_state_change,
|
214
|
+
"config": {
|
215
|
+
"failure_threshold": self.config.failure_threshold,
|
216
|
+
"recovery_timeout": self.config.recovery_timeout,
|
217
|
+
"timeout": self.config.timeout
|
218
|
+
}
|
219
|
+
}
|
220
|
+
|
221
|
+
def reset(self):
|
222
|
+
"""Reset circuit breaker to initial state"""
|
223
|
+
self.state = CircuitState.CLOSED
|
224
|
+
self.stats = CircuitBreakerStats()
|
225
|
+
self.last_state_change = time.time()
|
226
|
+
|
227
|
+
logger.info("Circuit breaker reset", name=self.name)
|
228
|
+
|
229
|
+
class CircuitBreakerOpenException(Exception):
|
230
|
+
"""Exception raised when circuit breaker is open"""
|
231
|
+
pass
|
232
|
+
|
233
|
+
class CircuitBreakerTimeoutException(Exception):
|
234
|
+
"""Exception raised when call times out"""
|
235
|
+
pass
|
236
|
+
|
237
|
+
# Global circuit breaker registry
|
238
|
+
_circuit_breakers: Dict[str, EnhancedCircuitBreaker] = {}
|
239
|
+
|
240
|
+
def get_circuit_breaker(
|
241
|
+
name: str,
|
242
|
+
config: Optional[CircuitBreakerConfig] = None
|
243
|
+
) -> EnhancedCircuitBreaker:
|
244
|
+
"""Get or create a circuit breaker"""
|
245
|
+
if name not in _circuit_breakers:
|
246
|
+
if config is None:
|
247
|
+
config = CircuitBreakerConfig()
|
248
|
+
_circuit_breakers[name] = EnhancedCircuitBreaker(name, config)
|
249
|
+
|
250
|
+
return _circuit_breakers[name]
|
251
|
+
|
252
|
+
def circuit_breaker(
|
253
|
+
name: str,
|
254
|
+
failure_threshold: int = 5,
|
255
|
+
recovery_timeout: int = 30,
|
256
|
+
timeout: float = 30.0,
|
257
|
+
expected_exception: type = Exception
|
258
|
+
):
|
259
|
+
"""Decorator for applying circuit breaker to functions"""
|
260
|
+
config = CircuitBreakerConfig(
|
261
|
+
failure_threshold=failure_threshold,
|
262
|
+
recovery_timeout=recovery_timeout,
|
263
|
+
timeout=timeout,
|
264
|
+
expected_exception=expected_exception
|
265
|
+
)
|
266
|
+
|
267
|
+
breaker = get_circuit_breaker(name, config)
|
268
|
+
return breaker
|
269
|
+
|
270
|
+
# Predefined circuit breakers for common services
|
271
|
+
def openai_circuit_breaker():
|
272
|
+
"""Circuit breaker for OpenAI API calls"""
|
273
|
+
return circuit_breaker(
|
274
|
+
name="openai",
|
275
|
+
failure_threshold=3,
|
276
|
+
recovery_timeout=60,
|
277
|
+
timeout=120.0 # OpenAI can be slow
|
278
|
+
)
|
279
|
+
|
280
|
+
def replicate_circuit_breaker():
|
281
|
+
"""Circuit breaker for Replicate API calls"""
|
282
|
+
return circuit_breaker(
|
283
|
+
name="replicate",
|
284
|
+
failure_threshold=3,
|
285
|
+
recovery_timeout=45,
|
286
|
+
timeout=300.0 # Replicate can be very slow for image generation
|
287
|
+
)
|
288
|
+
|
289
|
+
def database_circuit_breaker():
|
290
|
+
"""Circuit breaker for database calls"""
|
291
|
+
return circuit_breaker(
|
292
|
+
name="database",
|
293
|
+
failure_threshold=5,
|
294
|
+
recovery_timeout=20,
|
295
|
+
timeout=10.0
|
296
|
+
)
|
297
|
+
|
298
|
+
def redis_circuit_breaker():
|
299
|
+
"""Circuit breaker for Redis calls"""
|
300
|
+
return circuit_breaker(
|
301
|
+
name="redis",
|
302
|
+
failure_threshold=3,
|
303
|
+
recovery_timeout=15,
|
304
|
+
timeout=5.0
|
305
|
+
)
|
306
|
+
|
307
|
+
# Health check for all circuit breakers
|
308
|
+
async def check_circuit_breakers_health() -> Dict[str, Any]:
|
309
|
+
"""Check health of all circuit breakers"""
|
310
|
+
health_status = {
|
311
|
+
"circuit_breakers": {},
|
312
|
+
"total_breakers": len(_circuit_breakers),
|
313
|
+
"open_breakers": 0,
|
314
|
+
"status": "healthy"
|
315
|
+
}
|
316
|
+
|
317
|
+
for name, breaker in _circuit_breakers.items():
|
318
|
+
stats = breaker.get_stats()
|
319
|
+
health_status["circuit_breakers"][name] = stats
|
320
|
+
|
321
|
+
if stats["state"] == "open":
|
322
|
+
health_status["open_breakers"] += 1
|
323
|
+
|
324
|
+
# Overall health status
|
325
|
+
if health_status["open_breakers"] > 0:
|
326
|
+
health_status["status"] = "degraded"
|
327
|
+
|
328
|
+
return health_status
|
329
|
+
|
330
|
+
# Utility functions for retry with exponential backoff
|
331
|
+
async def retry_with_backoff(
|
332
|
+
func: Callable,
|
333
|
+
max_retries: int = 3,
|
334
|
+
base_delay: float = 1.0,
|
335
|
+
max_delay: float = 60.0,
|
336
|
+
exponential_base: float = 2.0,
|
337
|
+
exceptions: tuple = (Exception,)
|
338
|
+
):
|
339
|
+
"""Retry function with exponential backoff"""
|
340
|
+
|
341
|
+
for attempt in range(max_retries + 1):
|
342
|
+
try:
|
343
|
+
if asyncio.iscoroutinefunction(func):
|
344
|
+
return await func()
|
345
|
+
else:
|
346
|
+
return func()
|
347
|
+
except exceptions as e:
|
348
|
+
if attempt == max_retries:
|
349
|
+
logger.error(
|
350
|
+
"All retry attempts failed",
|
351
|
+
attempts=attempt + 1,
|
352
|
+
error=str(e)
|
353
|
+
)
|
354
|
+
raise
|
355
|
+
|
356
|
+
delay = min(base_delay * (exponential_base ** attempt), max_delay)
|
357
|
+
|
358
|
+
logger.warning(
|
359
|
+
"Retry attempt failed, backing off",
|
360
|
+
attempt=attempt + 1,
|
361
|
+
max_retries=max_retries,
|
362
|
+
delay=delay,
|
363
|
+
error=str(e)
|
364
|
+
)
|
365
|
+
|
366
|
+
await asyncio.sleep(delay)
|