---
name: circuit_breaker
kind: function
lang: py
domain: core
version: "1.0.0"
purity: impure
signature: "class CircuitBreaker:\n  def __init__(self, failure_threshold: int = 5, reset_timeout: float = 300.0): ...\n  def check(self) -> None: ...\n  def record_success(self) -> None: ...\n  def record_failure(self, error: Exception) -> None: ...\n  @property\n  def retry_after(self) -> float: ..."
description: "Patron circuit breaker thread-safe para proteger llamadas a APIs externas. Tres estados: CLOSED (normal), OPEN (bloqueando), HALF_OPEN (permitiendo 1 request de prueba). Integra con classify_api_error para distinguir errores permanentes de transitorios."
tags: [circuit-breaker, resilience, api, retry, error-handling, thread-safe]
uses_functions: [classify_api_error_py_core]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [threading, time, enum]
params:
  - name: failure_threshold
    desc: "numero de fallos consecutivos que dispara el estado OPEN"
  - name: reset_timeout
    desc: "tiempo en segundos que debe esperar antes de pasar a HALF_OPEN"
output: "instancia de CircuitBreaker con metodos check(), record_success(), record_failure(), retry_after"
tested: true
tests:
  - "Transicion CLOSED → OPEN despues de N fallos"
  - "Transicion OPEN → HALF_OPEN despues de timeout"
  - "Transicion HALF_OPEN → CLOSED en exito"
  - "Transicion HALF_OPEN → OPEN en fallo"
  - "Error permanente abre inmediatamente"
  - "Thread safety (concurrencia)"
  - "retry_after retorna 0 cuando no esta OPEN"
test_file_path: "python/functions/core/circuit_breaker_test.py"
file_path: "python/functions/core/circuit_breaker.py"
---

## Ejemplo

```python
from circuit_breaker import CircuitBreaker, CircuitBreakerOpen

cb = CircuitBreaker(failure_threshold=3, reset_timeout=60.0)

def call_api() -> dict:
    cb.check()  # raises CircuitBreakerOpen if circuit is open
    try:
        result = requests.get("https://api.example.com/data").json()
        cb.record_success()
        return result
    except Exception as exc:
        cb.record_failure(exc)
        raise

# After 3 consecutive failures the circuit opens:
# CircuitBreakerOpen: Circuit breaker is open. Retry after 30.0s
try:
    cb.check()
except CircuitBreakerOpen as e:
    print(f"Circuit open, retry in {e.retry_after}s")

# retry_after property (capped at 30s):
print(cb.retry_after)  # e.g. 28.4
```

## Notas

- **CLOSED**: Requests pasan normalmente. Tras `failure_threshold` fallos consecutivos transiciona a OPEN.
- **OPEN**: Requests bloqueados con `CircuitBreakerOpen`. Tras `reset_timeout` segundos transiciona a HALF_OPEN.
- **HALF_OPEN**: Permite 1 request de prueba. Exito → CLOSED. Fallo → OPEN.
- Errores permanentes (401, 403) abren el circuito inmediatamente sin esperar al umbral.
- `retry_after` devuelve 0.0 cuando el estado no es OPEN; en OPEN devuelve el tiempo restante, cap 30s.
- Thread-safe via `threading.Lock` protegiendo todo el estado interno.
- La dependencia en `classify_api_error` es opcional: si no se puede importar, hay fallback de texto.
