proxilion 0.0.1__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.
- proxilion/__init__.py +136 -0
- proxilion/audit/__init__.py +133 -0
- proxilion/audit/base_exporters.py +527 -0
- proxilion/audit/compliance/__init__.py +130 -0
- proxilion/audit/compliance/base.py +457 -0
- proxilion/audit/compliance/eu_ai_act.py +603 -0
- proxilion/audit/compliance/iso27001.py +544 -0
- proxilion/audit/compliance/soc2.py +491 -0
- proxilion/audit/events.py +493 -0
- proxilion/audit/explainability.py +1173 -0
- proxilion/audit/exporters/__init__.py +58 -0
- proxilion/audit/exporters/aws_s3.py +636 -0
- proxilion/audit/exporters/azure_storage.py +608 -0
- proxilion/audit/exporters/cloud_base.py +468 -0
- proxilion/audit/exporters/gcp_storage.py +570 -0
- proxilion/audit/exporters/multi_exporter.py +498 -0
- proxilion/audit/hash_chain.py +652 -0
- proxilion/audit/logger.py +543 -0
- proxilion/caching/__init__.py +49 -0
- proxilion/caching/tool_cache.py +633 -0
- proxilion/context/__init__.py +73 -0
- proxilion/context/context_window.py +556 -0
- proxilion/context/message_history.py +505 -0
- proxilion/context/session.py +735 -0
- proxilion/contrib/__init__.py +51 -0
- proxilion/contrib/anthropic.py +609 -0
- proxilion/contrib/google.py +1012 -0
- proxilion/contrib/langchain.py +641 -0
- proxilion/contrib/mcp.py +893 -0
- proxilion/contrib/openai.py +646 -0
- proxilion/core.py +3058 -0
- proxilion/decorators.py +966 -0
- proxilion/engines/__init__.py +287 -0
- proxilion/engines/base.py +266 -0
- proxilion/engines/casbin_engine.py +412 -0
- proxilion/engines/opa_engine.py +493 -0
- proxilion/engines/simple.py +437 -0
- proxilion/exceptions.py +887 -0
- proxilion/guards/__init__.py +54 -0
- proxilion/guards/input_guard.py +522 -0
- proxilion/guards/output_guard.py +634 -0
- proxilion/observability/__init__.py +198 -0
- proxilion/observability/cost_tracker.py +866 -0
- proxilion/observability/hooks.py +683 -0
- proxilion/observability/metrics.py +798 -0
- proxilion/observability/session_cost_tracker.py +1063 -0
- proxilion/policies/__init__.py +67 -0
- proxilion/policies/base.py +304 -0
- proxilion/policies/builtin.py +486 -0
- proxilion/policies/registry.py +376 -0
- proxilion/providers/__init__.py +201 -0
- proxilion/providers/adapter.py +468 -0
- proxilion/providers/anthropic_adapter.py +330 -0
- proxilion/providers/gemini_adapter.py +391 -0
- proxilion/providers/openai_adapter.py +294 -0
- proxilion/py.typed +0 -0
- proxilion/resilience/__init__.py +81 -0
- proxilion/resilience/degradation.py +615 -0
- proxilion/resilience/fallback.py +555 -0
- proxilion/resilience/retry.py +554 -0
- proxilion/scheduling/__init__.py +57 -0
- proxilion/scheduling/priority_queue.py +419 -0
- proxilion/scheduling/scheduler.py +459 -0
- proxilion/security/__init__.py +244 -0
- proxilion/security/agent_trust.py +968 -0
- proxilion/security/behavioral_drift.py +794 -0
- proxilion/security/cascade_protection.py +869 -0
- proxilion/security/circuit_breaker.py +428 -0
- proxilion/security/cost_limiter.py +690 -0
- proxilion/security/idor_protection.py +460 -0
- proxilion/security/intent_capsule.py +849 -0
- proxilion/security/intent_validator.py +495 -0
- proxilion/security/memory_integrity.py +767 -0
- proxilion/security/rate_limiter.py +509 -0
- proxilion/security/scope_enforcer.py +680 -0
- proxilion/security/sequence_validator.py +636 -0
- proxilion/security/trust_boundaries.py +784 -0
- proxilion/streaming/__init__.py +70 -0
- proxilion/streaming/detector.py +761 -0
- proxilion/streaming/transformer.py +674 -0
- proxilion/timeouts/__init__.py +55 -0
- proxilion/timeouts/decorators.py +477 -0
- proxilion/timeouts/manager.py +545 -0
- proxilion/tools/__init__.py +69 -0
- proxilion/tools/decorators.py +493 -0
- proxilion/tools/registry.py +732 -0
- proxilion/types.py +339 -0
- proxilion/validation/__init__.py +93 -0
- proxilion/validation/pydantic_schema.py +351 -0
- proxilion/validation/schema.py +651 -0
- proxilion-0.0.1.dist-info/METADATA +872 -0
- proxilion-0.0.1.dist-info/RECORD +94 -0
- proxilion-0.0.1.dist-info/WHEEL +4 -0
- proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Circuit breaker implementation for Proxilion.
|
|
3
|
+
|
|
4
|
+
This module provides the circuit breaker pattern to prevent
|
|
5
|
+
cascading failures when external services or tools fail.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any, TypeVar
|
|
17
|
+
|
|
18
|
+
from proxilion.exceptions import CircuitOpenError
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
T = TypeVar("T")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CircuitState(Enum):
|
|
26
|
+
"""Circuit breaker states."""
|
|
27
|
+
CLOSED = "closed" # Normal operation, requests pass through
|
|
28
|
+
OPEN = "open" # Failing, requests rejected
|
|
29
|
+
HALF_OPEN = "half_open" # Testing if service recovered
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class CircuitStats:
|
|
34
|
+
"""Statistics for a circuit breaker."""
|
|
35
|
+
failures: int = 0
|
|
36
|
+
successes: int = 0
|
|
37
|
+
consecutive_failures: int = 0
|
|
38
|
+
consecutive_successes: int = 0
|
|
39
|
+
last_failure_time: float | None = None
|
|
40
|
+
last_success_time: float | None = None
|
|
41
|
+
last_failure_error: str | None = None
|
|
42
|
+
state_change_time: float = field(default_factory=time.monotonic)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CircuitBreaker:
|
|
46
|
+
"""
|
|
47
|
+
Circuit breaker for protecting against cascading failures.
|
|
48
|
+
|
|
49
|
+
The circuit breaker has three states:
|
|
50
|
+
- CLOSED: Normal operation. Requests pass through and failures are tracked.
|
|
51
|
+
- OPEN: The circuit is tripped. Requests are rejected immediately.
|
|
52
|
+
- HALF_OPEN: Testing recovery. Limited requests are allowed through.
|
|
53
|
+
|
|
54
|
+
State Transitions:
|
|
55
|
+
- CLOSED -> OPEN: When failures exceed the threshold.
|
|
56
|
+
- OPEN -> HALF_OPEN: After the reset timeout expires.
|
|
57
|
+
- HALF_OPEN -> CLOSED: When a request succeeds.
|
|
58
|
+
- HALF_OPEN -> OPEN: When a request fails.
|
|
59
|
+
|
|
60
|
+
Thread Safety:
|
|
61
|
+
All operations are thread-safe.
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
>>> breaker = CircuitBreaker(
|
|
65
|
+
... failure_threshold=5,
|
|
66
|
+
... reset_timeout=30.0,
|
|
67
|
+
... )
|
|
68
|
+
>>>
|
|
69
|
+
>>> try:
|
|
70
|
+
... result = breaker.call(external_api_call, arg1, arg2)
|
|
71
|
+
... except CircuitOpenError:
|
|
72
|
+
... # Circuit is open, use fallback
|
|
73
|
+
... result = fallback_response()
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
failure_threshold: int = 5,
|
|
79
|
+
reset_timeout: float = 30.0,
|
|
80
|
+
half_open_max: int = 1,
|
|
81
|
+
success_threshold: int = 1,
|
|
82
|
+
excluded_exceptions: tuple[type[Exception], ...] | None = None,
|
|
83
|
+
exponential_backoff: bool = True,
|
|
84
|
+
max_backoff: float = 300.0,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Initialize the circuit breaker.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
failure_threshold: Number of failures before opening circuit.
|
|
91
|
+
reset_timeout: Seconds to wait before trying half-open.
|
|
92
|
+
half_open_max: Max concurrent requests in half-open state.
|
|
93
|
+
success_threshold: Successes needed to close circuit from half-open.
|
|
94
|
+
excluded_exceptions: Exceptions that don't count as failures.
|
|
95
|
+
exponential_backoff: If True, increase timeout on repeated failures.
|
|
96
|
+
max_backoff: Maximum backoff timeout in seconds.
|
|
97
|
+
"""
|
|
98
|
+
self.failure_threshold = failure_threshold
|
|
99
|
+
self.reset_timeout = reset_timeout
|
|
100
|
+
self.half_open_max = half_open_max
|
|
101
|
+
self.success_threshold = success_threshold
|
|
102
|
+
self.excluded_exceptions = excluded_exceptions or ()
|
|
103
|
+
self.exponential_backoff = exponential_backoff
|
|
104
|
+
self.max_backoff = max_backoff
|
|
105
|
+
|
|
106
|
+
self._state = CircuitState.CLOSED
|
|
107
|
+
self._stats = CircuitStats()
|
|
108
|
+
self._lock = threading.RLock()
|
|
109
|
+
self._half_open_count = 0
|
|
110
|
+
self._backoff_multiplier = 1.0
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def state(self) -> CircuitState:
|
|
114
|
+
"""Get current circuit state."""
|
|
115
|
+
with self._lock:
|
|
116
|
+
self._maybe_transition_to_half_open()
|
|
117
|
+
return self._state
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def stats(self) -> CircuitStats:
|
|
121
|
+
"""Get circuit statistics."""
|
|
122
|
+
with self._lock:
|
|
123
|
+
return CircuitStats(
|
|
124
|
+
failures=self._stats.failures,
|
|
125
|
+
successes=self._stats.successes,
|
|
126
|
+
consecutive_failures=self._stats.consecutive_failures,
|
|
127
|
+
consecutive_successes=self._stats.consecutive_successes,
|
|
128
|
+
last_failure_time=self._stats.last_failure_time,
|
|
129
|
+
last_success_time=self._stats.last_success_time,
|
|
130
|
+
last_failure_error=self._stats.last_failure_error,
|
|
131
|
+
state_change_time=self._stats.state_change_time,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def _maybe_transition_to_half_open(self) -> None:
|
|
135
|
+
"""Check if we should transition from OPEN to HALF_OPEN."""
|
|
136
|
+
if self._state != CircuitState.OPEN:
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
current_timeout = self.reset_timeout * self._backoff_multiplier
|
|
140
|
+
elapsed = time.monotonic() - self._stats.state_change_time
|
|
141
|
+
|
|
142
|
+
if elapsed >= current_timeout:
|
|
143
|
+
logger.info(
|
|
144
|
+
f"Circuit transitioning from OPEN to HALF_OPEN "
|
|
145
|
+
f"after {elapsed:.1f}s"
|
|
146
|
+
)
|
|
147
|
+
self._state = CircuitState.HALF_OPEN
|
|
148
|
+
self._stats.state_change_time = time.monotonic()
|
|
149
|
+
self._half_open_count = 0
|
|
150
|
+
|
|
151
|
+
def _set_state(self, new_state: CircuitState) -> None:
|
|
152
|
+
"""Set circuit state and log the transition."""
|
|
153
|
+
old_state = self._state
|
|
154
|
+
if old_state != new_state:
|
|
155
|
+
self._state = new_state
|
|
156
|
+
self._stats.state_change_time = time.monotonic()
|
|
157
|
+
logger.info(f"Circuit state: {old_state.value} -> {new_state.value}")
|
|
158
|
+
|
|
159
|
+
def _record_success(self) -> None:
|
|
160
|
+
"""Record a successful call."""
|
|
161
|
+
self._stats.successes += 1
|
|
162
|
+
self._stats.consecutive_successes += 1
|
|
163
|
+
self._stats.consecutive_failures = 0
|
|
164
|
+
self._stats.last_success_time = time.monotonic()
|
|
165
|
+
|
|
166
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
167
|
+
if self._stats.consecutive_successes >= self.success_threshold:
|
|
168
|
+
self._set_state(CircuitState.CLOSED)
|
|
169
|
+
self._backoff_multiplier = 1.0 # Reset backoff
|
|
170
|
+
self._stats.consecutive_successes = 0
|
|
171
|
+
|
|
172
|
+
def _record_failure(self, error: Exception) -> None:
|
|
173
|
+
"""Record a failed call."""
|
|
174
|
+
self._stats.failures += 1
|
|
175
|
+
self._stats.consecutive_failures += 1
|
|
176
|
+
self._stats.consecutive_successes = 0
|
|
177
|
+
self._stats.last_failure_time = time.monotonic()
|
|
178
|
+
self._stats.last_failure_error = str(error)
|
|
179
|
+
|
|
180
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
181
|
+
# Any failure in half-open opens the circuit again
|
|
182
|
+
self._set_state(CircuitState.OPEN)
|
|
183
|
+
if self.exponential_backoff:
|
|
184
|
+
self._backoff_multiplier = min(
|
|
185
|
+
self._backoff_multiplier * 2,
|
|
186
|
+
self.max_backoff / self.reset_timeout,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
elif self._state == CircuitState.CLOSED:
|
|
190
|
+
if self._stats.consecutive_failures >= self.failure_threshold:
|
|
191
|
+
self._set_state(CircuitState.OPEN)
|
|
192
|
+
|
|
193
|
+
def call(
|
|
194
|
+
self,
|
|
195
|
+
func: Callable[..., T],
|
|
196
|
+
*args: Any,
|
|
197
|
+
**kwargs: Any,
|
|
198
|
+
) -> T:
|
|
199
|
+
"""
|
|
200
|
+
Execute a function through the circuit breaker.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
func: The function to call.
|
|
204
|
+
*args: Positional arguments for the function.
|
|
205
|
+
**kwargs: Keyword arguments for the function.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The function's return value.
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
CircuitOpenError: If the circuit is open.
|
|
212
|
+
Exception: Any exception raised by the function.
|
|
213
|
+
|
|
214
|
+
Example:
|
|
215
|
+
>>> result = breaker.call(api.get_data, user_id=123)
|
|
216
|
+
"""
|
|
217
|
+
with self._lock:
|
|
218
|
+
self._maybe_transition_to_half_open()
|
|
219
|
+
state = self._state
|
|
220
|
+
|
|
221
|
+
if state == CircuitState.OPEN:
|
|
222
|
+
current_timeout = self.reset_timeout * self._backoff_multiplier
|
|
223
|
+
elapsed = time.monotonic() - self._stats.state_change_time
|
|
224
|
+
remaining = current_timeout - elapsed
|
|
225
|
+
|
|
226
|
+
raise CircuitOpenError(
|
|
227
|
+
circuit_name=getattr(func, "__name__", "unknown"),
|
|
228
|
+
failure_count=self._stats.consecutive_failures,
|
|
229
|
+
reset_timeout=remaining,
|
|
230
|
+
last_failure=self._stats.last_failure_error,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if state == CircuitState.HALF_OPEN:
|
|
234
|
+
if self._half_open_count >= self.half_open_max:
|
|
235
|
+
raise CircuitOpenError(
|
|
236
|
+
circuit_name=getattr(func, "__name__", "unknown"),
|
|
237
|
+
failure_count=self._stats.consecutive_failures,
|
|
238
|
+
reset_timeout=0.0,
|
|
239
|
+
last_failure="Half-open limit reached",
|
|
240
|
+
)
|
|
241
|
+
self._half_open_count += 1
|
|
242
|
+
|
|
243
|
+
# Execute outside lock to avoid blocking
|
|
244
|
+
try:
|
|
245
|
+
result = func(*args, **kwargs)
|
|
246
|
+
with self._lock:
|
|
247
|
+
self._record_success()
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
except self.excluded_exceptions:
|
|
251
|
+
# Don't count as failure, but re-raise
|
|
252
|
+
raise
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
with self._lock:
|
|
256
|
+
self._record_failure(e)
|
|
257
|
+
raise
|
|
258
|
+
|
|
259
|
+
async def call_async(
|
|
260
|
+
self,
|
|
261
|
+
func: Callable[..., Any],
|
|
262
|
+
*args: Any,
|
|
263
|
+
**kwargs: Any,
|
|
264
|
+
) -> Any:
|
|
265
|
+
"""
|
|
266
|
+
Execute an async function through the circuit breaker.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
func: The async function to call.
|
|
270
|
+
*args: Positional arguments.
|
|
271
|
+
**kwargs: Keyword arguments.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
The function's return value.
|
|
275
|
+
|
|
276
|
+
Raises:
|
|
277
|
+
CircuitOpenError: If the circuit is open.
|
|
278
|
+
"""
|
|
279
|
+
with self._lock:
|
|
280
|
+
self._maybe_transition_to_half_open()
|
|
281
|
+
state = self._state
|
|
282
|
+
|
|
283
|
+
if state == CircuitState.OPEN:
|
|
284
|
+
current_timeout = self.reset_timeout * self._backoff_multiplier
|
|
285
|
+
elapsed = time.monotonic() - self._stats.state_change_time
|
|
286
|
+
remaining = current_timeout - elapsed
|
|
287
|
+
|
|
288
|
+
raise CircuitOpenError(
|
|
289
|
+
circuit_name=getattr(func, "__name__", "unknown"),
|
|
290
|
+
failure_count=self._stats.consecutive_failures,
|
|
291
|
+
reset_timeout=remaining,
|
|
292
|
+
last_failure=self._stats.last_failure_error,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if state == CircuitState.HALF_OPEN:
|
|
296
|
+
if self._half_open_count >= self.half_open_max:
|
|
297
|
+
raise CircuitOpenError(
|
|
298
|
+
circuit_name=getattr(func, "__name__", "unknown"),
|
|
299
|
+
failure_count=self._stats.consecutive_failures,
|
|
300
|
+
reset_timeout=0.0,
|
|
301
|
+
last_failure="Half-open limit reached",
|
|
302
|
+
)
|
|
303
|
+
self._half_open_count += 1
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
result = await func(*args, **kwargs)
|
|
307
|
+
with self._lock:
|
|
308
|
+
self._record_success()
|
|
309
|
+
return result
|
|
310
|
+
|
|
311
|
+
except self.excluded_exceptions:
|
|
312
|
+
raise
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
with self._lock:
|
|
316
|
+
self._record_failure(e)
|
|
317
|
+
raise
|
|
318
|
+
|
|
319
|
+
def reset(self) -> None:
|
|
320
|
+
"""Reset the circuit breaker to closed state."""
|
|
321
|
+
with self._lock:
|
|
322
|
+
self._set_state(CircuitState.CLOSED)
|
|
323
|
+
self._stats = CircuitStats()
|
|
324
|
+
self._half_open_count = 0
|
|
325
|
+
self._backoff_multiplier = 1.0
|
|
326
|
+
|
|
327
|
+
def force_open(self) -> None:
|
|
328
|
+
"""Force the circuit to open state (for testing/maintenance)."""
|
|
329
|
+
with self._lock:
|
|
330
|
+
self._set_state(CircuitState.OPEN)
|
|
331
|
+
|
|
332
|
+
def is_available(self) -> bool:
|
|
333
|
+
"""Check if the circuit will accept requests."""
|
|
334
|
+
return self.state != CircuitState.OPEN
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class CircuitBreakerRegistry:
|
|
338
|
+
"""
|
|
339
|
+
Registry for managing multiple circuit breakers.
|
|
340
|
+
|
|
341
|
+
Provides a central place to manage circuit breakers for
|
|
342
|
+
different tools or services.
|
|
343
|
+
|
|
344
|
+
Example:
|
|
345
|
+
>>> registry = CircuitBreakerRegistry()
|
|
346
|
+
>>> registry.register("external_api", CircuitBreaker(failure_threshold=3))
|
|
347
|
+
>>>
|
|
348
|
+
>>> breaker = registry.get("external_api")
|
|
349
|
+
>>> result = breaker.call(api_call)
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
def __init__(
|
|
353
|
+
self,
|
|
354
|
+
default_config: dict[str, Any] | None = None,
|
|
355
|
+
) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Initialize the registry.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
default_config: Default configuration for auto-created breakers.
|
|
361
|
+
"""
|
|
362
|
+
self._breakers: dict[str, CircuitBreaker] = {}
|
|
363
|
+
self._lock = threading.RLock()
|
|
364
|
+
self.default_config = default_config or {
|
|
365
|
+
"failure_threshold": 5,
|
|
366
|
+
"reset_timeout": 30.0,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
def register(
|
|
370
|
+
self,
|
|
371
|
+
name: str,
|
|
372
|
+
breaker: CircuitBreaker | None = None,
|
|
373
|
+
) -> CircuitBreaker:
|
|
374
|
+
"""
|
|
375
|
+
Register a circuit breaker.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
name: Name for the circuit breaker.
|
|
379
|
+
breaker: CircuitBreaker instance, or None to create with defaults.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
The registered circuit breaker.
|
|
383
|
+
"""
|
|
384
|
+
with self._lock:
|
|
385
|
+
if breaker is None:
|
|
386
|
+
breaker = CircuitBreaker(**self.default_config)
|
|
387
|
+
self._breakers[name] = breaker
|
|
388
|
+
return breaker
|
|
389
|
+
|
|
390
|
+
def get(self, name: str, auto_create: bool = True) -> CircuitBreaker:
|
|
391
|
+
"""
|
|
392
|
+
Get a circuit breaker by name.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
name: The circuit breaker name.
|
|
396
|
+
auto_create: If True, create a new breaker if not found.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
The circuit breaker.
|
|
400
|
+
|
|
401
|
+
Raises:
|
|
402
|
+
KeyError: If not found and auto_create is False.
|
|
403
|
+
"""
|
|
404
|
+
with self._lock:
|
|
405
|
+
if name not in self._breakers:
|
|
406
|
+
if auto_create:
|
|
407
|
+
return self.register(name)
|
|
408
|
+
raise KeyError(f"Circuit breaker '{name}' not found")
|
|
409
|
+
return self._breakers[name]
|
|
410
|
+
|
|
411
|
+
def get_all_stats(self) -> dict[str, dict[str, Any]]:
|
|
412
|
+
"""Get statistics for all circuit breakers."""
|
|
413
|
+
with self._lock:
|
|
414
|
+
return {
|
|
415
|
+
name: {
|
|
416
|
+
"state": breaker.state.value,
|
|
417
|
+
"failures": breaker.stats.failures,
|
|
418
|
+
"successes": breaker.stats.successes,
|
|
419
|
+
"consecutive_failures": breaker.stats.consecutive_failures,
|
|
420
|
+
}
|
|
421
|
+
for name, breaker in self._breakers.items()
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
def reset_all(self) -> None:
|
|
425
|
+
"""Reset all circuit breakers."""
|
|
426
|
+
with self._lock:
|
|
427
|
+
for breaker in self._breakers.values():
|
|
428
|
+
breaker.reset()
|