Módulo 7: Reliability Patterns & Production Checklist
7. Proyecto: Reliability Layer
Descripción
En las cápsulas anteriores construiste cada patrón de reliability de forma aislada. En este proyecto vas a integrarlos en una Reliability Layer completa que se conecta con la app del Módulo 6. La DI que implementaste en el M6 hace que este trabajo sea casi transparente para el domain: solo cambias dependencies.py para añadir la capa de reliability. Tu domain service (analyze_sentiment) no va a saber que tiene retry, circuit breaker ni fallback — y esa es la señal de que la arquitectura está bien diseñada.
Estructura del proyecto
src/
├── infrastructure/
│ ├── llm_provider.py # Protocol LLMProvider (del M6)
│ ├── openai_provider.py # Implementación OpenAI (del M6)
│ ├── mock_provider.py # Para tests (del M6)
│ │
│ │ # ─── Reliability Layer (nuevo en M7) ───
│ ├── error_classifier.py # Clasifica errores: TRANSIENT, AUTH, etc.
│ ├── retry_provider.py # Wrapper con tenacity
│ ├── circuit_breaker.py # CircuitBreaker state machine
│ ├── circuit_breaker_provider.py # Wrapper de CircuitBreaker
│ ├── rate_limiter.py # TokenBucket
│ ├── rate_limited_provider.py # Wrapper de TokenBucket
│ ├── fallback_provider.py # Fallback chain
│ └── cache_provider.py # Cache como fallback
│
├── health/
│ ├── __init__.py
│ └── checks.py # /health/live, /ready, /deps
│
├── app/
│ ├── dependencies.py # Aquí se ensambla todo (el único archivo que cambia)
│ ├── main.py
│ └── routers/
│ └── sentiment.py
│
tests/
├── conftest.py
└── unit/
├── test_retry_provider.py
├── test_circuit_breaker.py
├── test_rate_limiter.py
├── test_fallback_provider.py
└── test_reliability_integration.py # Tests de la composición completa
Paso 1: Ensamblar la reliability layer en dependencies.py
# src/app/dependencies.py
from functools import lru_cache
from src.config import get_settings
from src.infrastructure.llm_provider import LLMProvider
from src.infrastructure.openai_provider import OpenAIProvider
from src.infrastructure.mock_provider import MockProvider
from src.infrastructure.retry_provider import RetryProvider
from src.infrastructure.circuit_breaker import CircuitBreaker
from src.infrastructure.circuit_breaker_provider import CircuitBreakerProvider
from src.infrastructure.rate_limited_provider import RateLimitedProvider
from src.infrastructure.fallback_provider import FallbackProvider
import structlog
log = structlog.get_logger()
# Instancias globales de componentes que necesitan estado
# (el circuit breaker y rate limiter deben ser singletons para funcionar correctamente)
_primary_circuit_breaker: CircuitBreaker = None
_fallback_circuit_breaker: CircuitBreaker = None
_rate_limiter_metrics: dict = {}
def _get_primary_circuit_breaker() -> CircuitBreaker:
global _primary_circuit_breaker
if _primary_circuit_breaker is None:
settings = get_settings()
_primary_circuit_breaker = CircuitBreaker(
name="openai_primary",
failure_threshold=settings.circuit_breaker_threshold,
recovery_timeout=settings.circuit_breaker_timeout,
)
return _primary_circuit_breaker
def _get_fallback_circuit_breaker() -> CircuitBreaker:
global _fallback_circuit_breaker
if _fallback_circuit_breaker is None:
_fallback_circuit_breaker = CircuitBreaker(
name="openai_fallback",
failure_threshold=10, # Fallback tiene umbral más alto (más tolerante)
recovery_timeout=30,
)
return _fallback_circuit_breaker
def build_llm_provider(settings=None) -> LLMProvider:
"""
Construye el LLM provider con la reliability layer completa.
La cadena resultante (de outer a inner):
FallbackProvider
→ RateLimitedProvider (primary)
→ CircuitBreakerProvider (primary)
→ RetryProvider (primary)
→ OpenAIProvider("gpt-4o") ← el que hace la llamada real
Si el primary falla en todos los retries:
→ FallbackProvider intenta secondary
→ CircuitBreakerProvider (fallback)
→ RetryProvider (fallback)
→ OpenAIProvider("gpt-4o-mini") ← más barato, fallback
Si el secondary también falla:
→ FallbackProvider devuelve static_fallback
"""
if settings is None:
settings = get_settings()
# ─── Mock mode (para tests y desarrollo sin API key) ───
if settings.use_mock_provider:
log.info("using_mock_provider")
return MockProvider(
response='{"sentiment": "positive", "score": 0.8, "confidence": 0.9}'
)
# ─── Construir el primary provider ───
primary_base = OpenAIProvider(
client=settings.create_openai_client(),
model=settings.openai_model,
temperature=settings.temperature,
max_tokens=settings.max_tokens,
)
primary_with_retry = RetryProvider(
inner=primary_base,
max_attempts=settings.max_retry_attempts,
min_wait_seconds=settings.retry_min_wait,
max_wait_seconds=settings.retry_max_wait,
)
primary_with_cb = CircuitBreakerProvider(
inner=primary_with_retry,
circuit_breaker=_get_primary_circuit_breaker()
)
primary_rate_limited = RateLimitedProvider(
inner=primary_with_cb,
requests_per_minute=int(settings.max_requests_per_minute * 0.8),
max_wait_seconds=30.0,
model=settings.openai_model
)
# ─── Construir el fallback provider (modelo más pequeño) ───
fallback_base = OpenAIProvider(
client=settings.create_openai_client(),
model="gpt-4o-mini", # Siempre el más barato como fallback
temperature=settings.temperature,
max_tokens=settings.max_tokens,
)
fallback_with_retry = RetryProvider(
inner=fallback_base,
max_attempts=2, # Menos retries para el fallback
min_wait_seconds=1.0,
max_wait_seconds=10.0,
)
fallback_with_cb = CircuitBreakerProvider(
inner=fallback_with_retry,
circuit_breaker=_get_fallback_circuit_breaker()
)
# ─── Fallback chain completa ───
combined = FallbackProvider(
providers=[primary_rate_limited, fallback_with_cb],
names=["primary_gpt4o", "fallback_gpt4o_mini"],
static_fallback='{"sentiment": "unknown", "score": 0.0, "confidence": 0.0}',
)
log.info(
"reliability_layer_initialized",
primary_model=settings.openai_model,
fallback_model="gpt-4o-mini",
max_retries=settings.max_retry_attempts,
circuit_threshold=settings.circuit_breaker_threshold
)
return combined
def get_llm_provider() -> LLMProvider:
"""FastAPI dependency para inyectar el LLM provider en los endpoints."""
return build_llm_provider()
def get_circuit_breaker_metrics() -> dict:
"""Devuelve métricas de los circuit breakers (para health checks)."""
metrics = {}
if _primary_circuit_breaker:
metrics["primary"] = _primary_circuit_breaker.get_metrics()
if _fallback_circuit_breaker:
metrics["fallback"] = _fallback_circuit_breaker.get_metrics()
return metrics
def get_rate_limiter_metrics() -> dict:
"""Devuelve métricas del rate limiter (para health checks)."""
return _rate_limiter_metrics
Paso 2: Configuración en Settings
# src/config.py (añadir a la clase Settings del M6)
from pydantic_settings import BaseSettings
from pydantic import Field
class Settings(BaseSettings):
# ... todos los campos del M6 ...
# ─── Reliability: Retry ───
max_retry_attempts: int = Field(
default=4,
description="Número máximo de intentos (1 original + 3 retries)"
)
retry_min_wait: float = Field(
default=1.0,
description="Tiempo mínimo de espera entre retries (segundos)"
)
retry_max_wait: float = Field(
default=30.0,
description="Tiempo máximo de espera entre retries (segundos)"
)
# ─── Reliability: Circuit Breaker ───
circuit_breaker_threshold: int = Field(
default=5,
description="Failures consecutivos para abrir el circuit"
)
circuit_breaker_timeout: int = Field(
default=60,
description="Segundos en OPEN antes de transicionar a HALF_OPEN"
)
# ─── Reliability: Rate Limiting ───
max_requests_per_minute: int = Field(
default=60,
description="Requests por minuto permitidos (client-side limit)"
)
rate_limit_max_wait: float = Field(
default=30.0,
description="Máximo segundos a esperar en la cola del rate limiter"
)
Paso 3: Integrar health checks en main.py
# src/app/main.py
from fastapi import FastAPI
from src.health.checks import router as health_router
from src.app.routers.sentiment import router as sentiment_router
from src.logging_config import configure_logging
from src.startup import run_startup_checks
def create_app() -> FastAPI:
settings = get_settings()
configure_logging(env=settings.environment)
app = FastAPI(
title="Sentiment Analysis API",
description="API con reliability layer completa",
version="2.0.0"
)
# Registrar middleware (del M6)
from src.middleware import RequestTracingMiddleware
app.add_middleware(RequestTracingMiddleware)
# Health checks (sin autenticación)
app.include_router(health_router)
# API routes
app.include_router(sentiment_router, prefix="/api/v1")
# Startup checks
@app.on_event("startup")
async def startup():
run_startup_checks()
return app
app = create_app()
Paso 4: Tests de la reliability layer
# tests/unit/test_reliability_integration.py
"""
Tests de integración de la reliability layer completa.
Verifica que los patterns se componen correctamente.
"""
import pytest
import time
from src.infrastructure.mock_provider import MockProvider
from src.infrastructure.retry_provider import RetryProvider
from src.infrastructure.circuit_breaker import CircuitBreaker, CircuitState
from src.infrastructure.circuit_breaker_provider import CircuitBreakerProvider
from src.infrastructure.rate_limited_provider import RateLimitedProvider
from src.infrastructure.fallback_provider import FallbackProvider
from src.infrastructure.llm_provider import LLMProviderError
from src.infrastructure.error_classifier import ErrorCategory
# ─── Helper para crear errores transitorios ───
def transient_error():
return LLMProviderError(
"Rate limit", category=ErrorCategory.TRANSIENT, should_retry=True
)
def permanent_error():
return LLMProviderError(
"Auth failed", category=ErrorCategory.AUTH_ERROR, should_retry=False
)
class TestRetryPlusFallback:
"""Verifica que retry y fallback trabajan juntos correctamente."""
def test_primary_succeeds_no_fallback_needed(self):
"""Si primary funciona, fallback nunca se activa."""
primary = MockProvider('{"sentiment": "positive", "score": 0.8, "confidence": 0.9}')
secondary = MockProvider('{"sentiment": "negative", "score": 0.2, "confidence": 0.7}')
provider = FallbackProvider(
providers=[primary, secondary],
names=["primary", "secondary"]
)
result = provider.complete([{"role": "user", "content": "test"}])
assert "positive" in result
assert primary.call_count == 1
assert secondary.call_count == 0
def test_primary_fails_fallback_activates(self):
"""Si primary falla (después de retries), fallback debe activarse."""
fail_count = [0]
def primary_complete(messages, **kwargs):
fail_count[0] += 1
raise LLMProviderError(
"Outage", category=ErrorCategory.OUTAGE, should_retry=True
)
primary_mock = MockProvider()
primary_mock.complete = primary_complete
primary_with_retry = RetryProvider(
primary_mock,
max_attempts=2,
min_wait_seconds=0.01,
max_wait_seconds=0.05
)
secondary = MockProvider('{"sentiment": "neutral", "score": 0.5, "confidence": 0.6}')
provider = FallbackProvider(
providers=[primary_with_retry, secondary],
names=["primary", "secondary"],
static_fallback='{"sentiment": "unknown"}'
)
result = provider.complete([{"role": "user", "content": "test"}])
# Primary se intentó 2 veces (max_attempts), luego fue al secondary
assert fail_count[0] == 2
assert "neutral" in result
assert secondary.call_count == 1
class TestCircuitBreakerPlusRetry:
"""Verifica que circuit breaker y retry interactúan correctamente."""
def test_circuit_opens_after_persistent_failures(self):
"""
Cuando primary falla repetidamente (con retries),
el circuit breaker debe abrirse.
"""
circuit = CircuitBreaker("test", failure_threshold=3, recovery_timeout=60)
call_count = [0]
def always_fail(messages, **kwargs):
call_count[0] += 1
raise LLMProviderError("Timeout", category=ErrorCategory.TRANSIENT, should_retry=True)
mock = MockProvider()
mock.complete = always_fail
with_retry = RetryProvider(mock, max_attempts=2, min_wait_seconds=0.01, max_wait_seconds=0.05)
with_cb = CircuitBreakerProvider(with_retry, circuit_breaker=circuit)
# 3 requests completos (cada uno hace 2 intentos internos)
for _ in range(3):
with pytest.raises(LLMProviderError):
with_cb.complete([{"role": "user", "content": "test"}])
# El circuit debe estar abierto
assert circuit.state == CircuitState.OPEN
# La siguiente llamada debe fallar inmediatamente sin llamar al inner provider
call_count_before = call_count[0]
with pytest.raises(LLMProviderError) as exc_info:
with_cb.complete([{"role": "user", "content": "test"}])
# No se hicieron nuevas llamadas al provider
assert call_count[0] == call_count_before
class TestRateLimiterIntegration:
"""Verifica que el rate limiter funciona dentro de la cadena."""
def test_rate_limited_provider_throttles(self):
"""El rate limiter debe rechazar cuando el bucket está vacío."""
mock = MockProvider('{"sentiment": "positive", "score": 0.8, "confidence": 0.9}')
# Rate muy bajo para testear fácilmente
rate_limited = RateLimitedProvider(
inner=mock,
requests_per_minute=6, # 0.1 requests/segundo
burst_size=2, # Burst de solo 2
max_wait_seconds=0.1 # No esperar mucho en tests
)
# Los primeros 2 deben pasar (el burst)
for _ in range(2):
result = rate_limited.complete([{"role": "user", "content": "test"}])
assert result is not None
# El tercero puede fallar o esperar (depende del timing)
# En tests con 0.1s de max_wait, eventualmente se rechaza
# Verificar que el bucket puede replenish
class TestFullStack:
"""Test del stack completo: Rate → Circuit → Retry → Fallback."""
def test_all_components_compose_correctly(self):
"""
Test end-to-end de la composición completa.
Primary falla, secondary funciona, se devuelve resultado.
"""
secondary = MockProvider('{"sentiment": "neutral", "score": 0.5, "confidence": 0.6}')
def primary_fail(messages, **kwargs):
raise LLMProviderError("Primary down", category=ErrorCategory.OUTAGE, should_retry=False)
primary_mock = MockProvider()
primary_mock.complete = primary_fail
# Construir la cadena completa
primary_circuit = CircuitBreaker("primary", failure_threshold=2, recovery_timeout=60)
primary_with_cb = CircuitBreakerProvider(primary_mock, circuit_breaker=primary_circuit)
primary_with_retry = RetryProvider(primary_with_cb, max_attempts=2, min_wait_seconds=0.01, max_wait_seconds=0.05)
full_provider = FallbackProvider(
providers=[primary_with_retry, secondary],
names=["primary", "secondary"],
static_fallback='{"sentiment": "unknown"}'
)
result = full_provider.complete([{"role": "user", "content": "test text"}])
assert "neutral" in result
# Verificar métricas de degradación
metrics = full_provider.get_metrics()
assert metrics["degraded_calls"] == 1
assert metrics["fallback_counts"]["secondary"] == 1
Paso 5: Test de los health checks
# tests/unit/test_health.py
import pytest
from fastapi.testclient import TestClient
from unittest.mock import patch, MagicMock
def test_liveness_always_returns_200(client: TestClient):
"""El endpoint de liveness debe siempre retornar 200."""
response = client.get("/health/live")
assert response.status_code == 200
assert response.json()["status"] == "alive"
def test_readiness_with_closed_circuit(client: TestClient):
"""Readiness debe retornar 200 cuando el circuit está cerrado."""
with patch("src.app.dependencies.get_circuit_breaker_metrics") as mock_metrics:
mock_metrics.return_value = {
"primary": {"state": "closed"},
"fallback": {"state": "closed"}
}
response = client.get("/health/ready")
assert response.status_code == 200
def test_readiness_with_open_circuit(client: TestClient):
"""Readiness debe retornar 503 cuando el circuit está abierto."""
with patch("src.app.dependencies.get_circuit_breaker_metrics") as mock_metrics:
mock_metrics.return_value = {
"primary": {"state": "open"},
"fallback": {"state": "closed"}
}
response = client.get("/health/ready")
assert response.status_code == 503
def test_dependency_check_openai_ok(client: TestClient):
"""deps check debe reportar OK cuando OpenAI responde."""
with patch("src.config.get_settings") as mock_settings:
mock_client = MagicMock()
mock_client.models.list.return_value = MagicMock(data=[1, 2, 3])
mock_settings.return_value.create_openai_client.return_value = mock_client
response = client.get("/health/deps")
assert response.status_code == 200
assert response.json()["checks"]["openai"]["status"] == "ok"
def test_dependency_check_openai_down(client: TestClient):
"""deps check debe reportar error cuando OpenAI no responde."""
with patch("src.config.get_settings") as mock_settings:
mock_client = MagicMock()
mock_client.models.list.side_effect = ConnectionError("timeout")
mock_settings.return_value.create_openai_client.return_value = mock_client
response = client.get("/health/deps")
assert response.status_code == 503
assert response.json()["checks"]["openai"]["status"] == "error"
Comparación: antes vs después
| Escenario | Sin Reliability Layer | Con Reliability Layer |
|---|---|---|
| OpenAI timeout | Error 500 al usuario | Retry 3x, luego fallback |
| 429 Rate limit | Error 500 al usuario | Backoff automático |
| Outage 30 min | 10k requests fallidos | Circuit abre, falla rápido, fallback |
| Spike de tráfico | 429 masivos | Rate limiter distribuye la carga |
| JSON malformado | Error 500 al usuario | Parse fallback + default |
| K8s restart loop | — | Liveness evita restart innecesario |
| K8s traffic routing | — | Readiness enruta tráfico correctamente |
Troubleshooting de la composición
Problema: El orden de los wrappers parece confuso. Solución: Recordar el flujo de adentro hacia afuera. El más "cercano" al proveedor real maneja primero. Una llamada viaja así:
FallbackProvider → RateLimitedProvider → CircuitBreakerProvider → RetryProvider → OpenAIProvider
(más externo) (control de velocidad) (detecta outage) (reintenta) (la llamada real)
Problema: Los circuit breakers y rate limiters deben ser singletons, pero estoy creando el provider en cada request de FastAPI.
Solución: Extraer los componentes con estado a variables globales o a un contenedor de dependencias. El build_llm_provider() ya hace esto — los circuit breakers son singletons fuera de la función.
Problema: Tests fallan por los tiempos de espera del retry.
Solución: En tests, usar min_wait_seconds=0.01, max_wait_seconds=0.05 para que los retries sean casi instantáneos. En producción se usan los valores reales del Settings.
Ejercicios
Ejercicio 1: Añadir Anthropic como secondary
El proyecto actual usa gpt-4o-mini como fallback. Modifica dependencies.py para usar un hipotético AnthropicProvider como el secondary provider en la fallback chain.
Ver guía
from src.infrastructure.anthropic_provider import AnthropicProvider # hipotético
# En build_llm_provider():
secondary_base = AnthropicProvider(
model="claude-haiku-3",
api_key=settings.anthropic_api_key
)
secondary_with_retry = RetryProvider(secondary_base, max_attempts=2, ...)
secondary_with_cb = CircuitBreakerProvider(secondary_with_retry, ...)
combined = FallbackProvider(
providers=[primary_rate_limited, secondary_with_cb],
names=["openai_primary", "anthropic_secondary"],
static_fallback=...
)
Ejercicio 2: Métricas en /health/deps
Añade al endpoint /health/deps las métricas de la reliability layer: estado de los circuit breakers y utilización del rate limiter.
Ver guía
# En checks.py:
from src.app.dependencies import get_circuit_breaker_metrics, get_rate_limiter_metrics
@router.get("/deps")
async def dependency_check():
checks = {}
# ... verificación de OpenAI ...
checks["circuit_breakers"] = get_circuit_breaker_metrics()
checks["rate_limits"] = get_rate_limiter_metrics()
return {"checks": checks}
Ejercicio 3: Simular un outage completo
Escribe un test de integración que simule un outage completo: el primary y el secondary fallan, y verifica que el sistema devuelve el static fallback con el flag degraded=True:
Ver solución
def test_full_outage_returns_static_fallback():
error = LLMProviderError(
"Service unavailable",
category=ErrorCategory.OUTAGE,
should_retry=False
)
primary = MockProvider()
primary.complete = lambda m, **k: (_ for _ in ()).throw(error)
secondary = MockProvider()
secondary.complete = lambda m, **k: (_ for _ in ()).throw(error)
fallback = FallbackProvider(
providers=[primary, secondary],
names=["primary", "secondary"],
static_fallback='{"sentiment": "unknown", "score": 0.0, "confidence": 0.0}'
)
result = fallback.complete([{"role": "user", "content": "test"}])
data = json.loads(result)
assert data["sentiment"] == "unknown"
assert fallback.is_degraded is True
Ejercicio 4: Verificar composición correcta
Escribe un test que cree la cadena completa FallbackProvider(RateLimited(CircuitBreaker(Retry(Mock)))) y verifique que una llamada exitosa atraviesa todas las capas sin errores:
Ver solución
def test_full_chain_success():
base = MockProvider('{"sentiment": "positive", "score": 0.9, "confidence": 0.95}')
with_retry = RetryProvider(base, max_attempts=3,
min_wait_seconds=0.01, max_wait_seconds=0.01)
with_cb = CircuitBreakerProvider(with_retry, failure_threshold=5)
with_rate = RateLimitedProvider(with_cb, requests_per_minute=60)
combined = FallbackProvider(
providers=[with_rate],
names=["primary"],
static_fallback='{"sentiment": "unknown"}'
)
result = combined.complete([{"role": "user", "content": "test"}])
assert "positive" in result
assert base.call_count == 1
assert not combined.is_degraded
Troubleshooting
"El circuit breaker se crea nuevo en cada request"
Esto anula su propósito — necesita ser un singleton para acumular failures. Verifica que en dependencies.py los circuit breakers se crean fuera de la función build_llm_provider() o se cachean con un patrón como _get_primary_circuit_breaker() que retorna siempre la misma instancia.
"Los tests de integración son lentos por los retries"
Siempre usa min_wait_seconds=0.01, max_wait_seconds=0.05 en tus providers de test. Si no parametrizas estos valores, los retries usan los defaults de producción (1-30s) y tu suite de tests tarda minutos.
"FallbackProvider no detecta que el primary falló"
El FallbackProvider necesita que el primary lance una excepción — no que retorne un error como string. Si tu OpenAIProvider captura la excepción y retorna None o un string de error, el fallback no se activa. Asegúrate de que los errores se propagan como excepciones hasta el FallbackProvider.
"Las métricas de /health/deps no muestran los circuit breakers"
Verifica que get_circuit_breaker_metrics() accede a las mismas instancias que usa build_llm_provider(). Si son instancias diferentes, las métricas estarán vacías. Ambas funciones deben referenciar los mismos singletons.
"Al hacer deploy, el CircuitBreaker se resetea y pierdo el historial"
Esto es esperado — el CircuitBreaker vive en memoria. Cada deploy crea nuevas instancias con contadores en cero. Para la mayoría de apps AI, esto está bien: prefieres empezar "limpio" que heredar un circuit abierto de la versión anterior. Si necesitas persistir estado entre deploys, puedes usar Redis para almacenar el estado del circuit, pero agrega complejidad y rara vez vale la pena.
"No sé qué provider está respondiendo en este momento"
Agrega logging en el FallbackProvider para registrar cuál provider sirvió cada request. Puedes agregar un campo provider_used al log:
log.info("request_served",
provider_used=provider_name,
is_primary=(i == 0),
request_id=request_id
)
Esto te permite filtrar en tus logs cuántos requests usan el primary vs fallback, lo cual es una métrica clave de salud del sistema.
Checklist de integración
Antes de dar por terminado este proyecto, verifica cada ítem:
- [ ] `dependencies.py` crea la cadena completa de providers
- [ ] CircuitBreaker es singleton (no se recrea por request)
- [ ] Tests pasan con `min_wait_seconds=0.01` (sin delays reales)
- [ ] Test de cadena completa: primary OK → respuesta normal
- [ ] Test de fallback: primary falla → secondary responde
- [ ] Test de outage total: todos fallan → static fallback
- [ ] Health /live retorna 200 sin verificar OpenAI
- [ ] Health /ready verifica circuit breakers y config
- [ ] Health /deps muestra métricas de CB y rate limiter
- [ ] Logs muestran retry_attempt, circuit_opened, fallback_used
- [ ] Config tiene todos los parámetros de reliability
- [ ] Settings valida constraints de producción
Resumen
- La DI del M6 es el enabler: añadir reliability solo requiere cambiar
dependencies.py - Composición:
FallbackProvider(RateLimited(CircuitBreaker(Retry(OpenAI)))) - Singletons de estado: circuit breakers y rate limiters deben vivir fuera del request lifecycle
- Tests rápidos: usar
min_wait_seconds=0.01en tests para evitar sleeps lentos - Health checks como contrato con Kubernetes: liveness/readiness bien implementados hacen la app cloud-native
- El proyecto completo demuestra que puedes agregar resiliencia a una app AI sin modificar el domain — esa es la señal de que tu arquitectura está bien diseñada
Recursos adicionales
- tenacity Documentation — Retry library
- FastAPI Dependencies — Sistema de DI
- Kubernetes Probes — Configuración de probes
- Release It! (Nygard) — El libro de referencia