← Volver al catálogo
IA, ingeniería y MLOpsReferenciaIntermedioGratis

Resiliencia en Python: Reintentos y Backoff Exponencial

Patrones de resiliencia para aplicaciones Python: reintentos automáticos, backoff exponencial con jitter, timeouts y decoradores tolerantes a fallos usando tenacity. Ideal para servicios que consumen APIs externas o microservicios con dependencias inestables.

Descargar SKILL.md

Descarga abierta · sin registro · para Python, tenacity, httpx

// resultado_de_ejemplo

""" resilient_clients.py — NutriSync AI

Clientes resilientes para los tres servicios externos de NutriSync AI. Aplica patrones de tenacity: reintentos, backoff exponencial con jitter, timeouts y logging de cada intento.

Dependencias: tenacity>=8, httpx>=0.27, openai>=1.30

Uso: from resilient_clients import openai_complete, lookup_nutrients, send_report_email """

from future import annotations

import logging import random import time from dataclasses import dataclass, field from typing import Any

import httpx from tenacity import ( RetryCallState, before_sleep_log, retry, retry_if_exception_type, retry_if_result, stop_after_attempt, stop_after_delay, wait_exponential_jitter, )

---------------------------------------------------------------------------

Logging

---------------------------------------------------------------------------

logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s — %(message)s", ) logger = logging.getLogger("nutrisync.resilience")

---------------------------------------------------------------------------

Métricas en memoria (sustitúyelas por Prometheus/StatsD en producción)

---------------------------------------------------------------------------

@dataclass class RetryMetrics: """Conteo de intentos por servicio.""" attempts: dict[str, int] = field(default_factory=dict) failures: dict[str, int] = field(default_factory=dict)

def record_attempt(self, service: str) -> None:
    self.attempts[service] = self.attempts.get(service, 0) + 1

def record_failure(self, service: str) -> None:
    self.failures[service] = self.failures.get(service, 0) + 1

def summary(self) -> dict[str, Any]:
    return {"attempts": dict(self.attempts), "failures": dict(self.failures)}

metrics = RetryMetrics()

---------------------------------------------------------------------------

Excepciones retryables comunes

---------------------------------------------------------------------------

NETWORK_ERRORS = ( ConnectionError, TimeoutError, httpx.ConnectError, httpx.ReadTimeout, httpx.ConnectTimeout, httpx.RemoteProtocolError, )

RETRY_STATUS_CODES = {429, 500, 502, 503, 504}

def _is_retryable_response(response: httpx.Response) -> bool: """Devuelve True si el código HTTP indica un error transitorio.""" return response.status_code in RETRY_STATUS_CODES

def _make_before_sleep(service: str): """Callback que registra cada reintento y actualiza métricas.""" def _log(retry_state: RetryCallState) -> None: metrics.record_attempt(service) attempt_n = retry_state.attempt_number wait_s = retry_state.next_action.sleep if retry_state.next_action else 0 # type: ignore[union-attr] exc = retry_state.outcome.exception() if retry_state.outcome else None logger.warning( "[%s] Intento %d fallido — esperando %.2fs. Error: %s", service, attempt_n, wait_s, exc or "respuesta no retryable", ) return _log

===========================================================================

CLIENTE 1 — OpenAI GPT-4o (generación de planes nutricionales)

===========================================================================

Errores relevantes:

- openai.RateLimitError → 429, reintentable con backoff largo

- openai.APITimeoutError → timeout de red, reintentable

- openai.APIConnectionError → red inestable, reintentable

- openai.BadRequestError → prompt inválido, NO reintentable

- openai.AuthenticationError → API key mala, NO reintentable

try: import openai

OPENAI_RETRYABLE = (
    openai.RateLimitError,
    openai.APITimeoutError,
    openai.APIConnectionError,
)

except ImportError: # Fallback si openai no está instalado en este entorno de demo OPENAI_RETRYABLE = (ConnectionError, TimeoutError) # type: ignore[assignment] openai = None # type: ignore[assignment]

def _openai_retry_decorator(): return retry( retry=retry_if_exception_type(OPENAI_RETRYABLE), # Máximo 4 intentos O 90 s totales, lo que llegue primero stop=stop_after_attempt(4) | stop_after_delay(90), # Backoff: 2 s → 4 s → 8 s … con ±25 % de jitter wait=wait_exponential_jitter(initial=2, max=30, jitter=5), before_sleep=_make_before_sleep("openai-gpt4o"), reraise=True, )

@_openai_retry_decorator() def openai_complete( messages: list[dict[str, str]], model: str = "gpt-4o", temperature: float = 0.4, ) -> str: """ Llama a la API de OpenAI con reintentos automáticos en errores transitorios.

Args:
    messages: Lista de mensajes en formato OpenAI chat.
    model: Modelo a usar (por defecto gpt-4o).
    temperature: Temperatura de generación.

Returns:
    Texto de la respuesta del modelo.

Raises:
    openai.AuthenticationError: Si la API key es inválida (no se reintenta).
    openai.BadRequestError: Si el prompt es inválido (no se reintenta).
    openai.RateLimitError: Si se agota el presupuesto de reintentos.
"""
metrics.record_attempt("openai-gpt4o")
if openai is None:
    raise RuntimeError("openai no instalado — demo mode")

client = openai.OpenAI()  # Lee OPENAI_API_KEY del entorno
response = client.chat.completions.create(
    model=model,
    messages=messages,
    temperature=temperature,
    timeout=30,
)
return response.choices[0].message.content or ""

---------------------------------------------------------------------------

Ejemplo de uso — generación de plan nutricional

---------------------------------------------------------------------------

PLAN_PROMPT = [ { "role": "system", "content": ( "Eres NutriSync, un nutricionista IA. " "Genera planes de comida semanales detallados y científicamente fundamentados." ), }, { "role": "user", "content": ( "Genera un plan de 5 días para usuario masculino 32 años, 78 kg, " "objetivo: déficit calórico moderado (-300 kcal/día). " "Sin gluten. Formato JSON con: día, desayuno, almuerzo, cena, snack, kcal_total." ), }, ]

===========================================================================

CLIENTE 2 — Edamam Food API (lookup de nutrientes)

===========================================================================

EDAMAM_BASE_URL = "https://api.edamam.com/api/food-database/v2/parser"

def _edamam_retry_decorator(): return retry( retry=( retry_if_exception_type(NETWORK_ERRORS) | retry_if_result(_is_retryable_response) ), stop=stop_after_attempt(5) | stop_after_delay(60), wait=wait_exponential_jitter(initial=1, max=20, jitter=3), before_sleep=_make_before_sleep("edamam-food-api"), reraise=True, )

@_edamam_retry_decorator() def _edamam_raw_request(ingredient: str, app_id: str, app_key: str) -> httpx.Response: """Petición HTTP cruda a Edamam (reintentable).""" metrics.record_attempt("edamam-food-api") response = httpx.get( EDAMAM_BASE_URL, params={ "ingr": ingredient, "app_id": app_id, "app_key": app_key, "nutrition-type": "cooking", }, timeout=15, ) return response

def lookup_nutrients( ingredient: str, app_id: str = "EDAMAM_APP_ID", app_key: str = "EDAMAM_APP_KEY", ) -> dict[str, Any]: """ Consulta el perfil nutricional de un ingrediente en Edamam.

Args:
    ingredient: Nombre del ingrediente (ej. "100g chicken breast").
    app_id: Credencial Edamam Application ID.
    app_key: Credencial Edamam Application Key.

Returns:
    Dict con calories, protein, fat, carbs (por 100 g).

Raises:
    httpx.HTTPStatusError: Si la respuesta es 4xx no reintentable.
"""
response = _edamam_raw_request(ingredient, app_id, app_key)

if response.status_code == 401:
    raise httpx.HTTPStatusError(
        "Credenciales Edamam inválidas — no se reintenta",
        request=response.request,
        response=response,
    )

response.raise_for_status()
data = response.json()

# Extraer primer resultado
hint = data.get("hints", [{}])[0]
nutrients = hint.get("food", {}).get("nutrients", {})
return {
    "ingredient": ingredient,
    "calories_kcal": nutrients.get("ENERC_KCAL", 0),
    "protein_g": nutrients.get("PROCNT", 0),
    "fat_g": nutrients.get("FAT", 0),
    "carbs_g": nutrients.get("CHOCDF", 0),
    "fiber_g": nutrients.get("FIBTG", 0),
}

===========================================================================

CLIENTE 3 — SendGrid (envío de informes semanales)

===========================================================================

SENDGRID_API_URL = "https://api.sendgrid.com/v3/mail/send"

def _sendgrid_retry_decorator(): return retry( retry=( retry_if_exception_type(NETWORK_ERRORS) | retry_if_result(lambda r: r.status_code in {429, 500, 502, 503, 504}) ), stop=stop_after_attempt(4) | stop_after_delay(45), wait=wait_exponential_jitter(initial=2, max=15, jitter=2), before_sleep=_make_before_sleep("sendgrid"), reraise=True, )

@_sendgrid_retry_decorator() def _sendgrid_raw_send(payload: dict[str, Any], api_key: str) -> httpx.Response: """Envío HTTP crudo a SendGrid (reintentable).""" metrics.record_attempt("sendgrid") response = httpx.post( SENDGRID_API_URL, json=payload, headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", }, timeout=20, ) return response

def send_report_email( to_email: str, subject: str, html_content: str, from_email: str = "hola@nutrisync.ai", api_key: str = "SG.REPLACE_ME", ) -> bool: """ Envía el informe nutricional semanal por email con reintentos automáticos.

Args:
    to_email: Destinatario.
    subject: Asunto del email.
    html_content: Cuerpo HTML del informe.
    from_email: Remitente verificado en SendGrid.
    api_key: API Key de SendGrid.

Returns:
    True si el email se envió correctamente (2xx).

Raises:
    httpx.HTTPStatusError: En error 4xx (credenciales/payload inválido).
"""
payload = {
    "personalizations": [{"to": [{"email": to_email}]}],
    "from": {"email": from_email},
    "subject": subject,
    "content": [{"type": "text/html", "value": html_content}],
}

response = _sendgrid_raw_send(payload, api_key)

# 401/403 son permanentes — no se reintentan
if response.status_code in {401, 403}:
    raise httpx.HTTPStatusError(
        f"SendGrid auth error {response.status_code}",
        request=response.request,
        response=response,
    )

return response.status_code in {200, 202}

===========================================================================

DEMO LOCAL — simula fallos y reintentos sin APIs reales

===========================================================================

def _demo_simulate_flaky_service(service_name: str, fail_n_times: int = 2) -> None: """Demuestra el comportamiento de reintento sin llamadas reales a la red.""" attempt = [0]

@retry(
    retry=retry_if_exception_type(ConnectionError),
    stop=stop_after_attempt(fail_n_times + 1),
    wait=wait_exponential_jitter(initial=0.1, max=1.0, jitter=0.05),
    before_sleep=_make_before_sleep(service_name),
    reraise=False,
)
def _flaky_call() -> str:
    attempt[0] += 1
    if attempt[0] <= fail_n_times:
        logger.info("[%s] Simulando fallo transitorio #%d", service_name, attempt[0])
        raise ConnectionError(f"Fallo transitorio simulado #{attempt[0]}")
    logger.info("[%s] Llamada exitosa en intento #%d", service_name, attempt[0])
    return f"OK tras {attempt[0]} intentos"

result = _flaky_call()
logger.info("[%s] Resultado final: %s", service_name, result)

if name == "main": print("\n" + "=" * 60) print(" NutriSync AI — Demo de Resiliencia con tenacity") print("=" * 60 + "\n")

print(">> Simulando servicio inestable: OpenAI GPT-4o (2 fallos transitorios)")
_demo_simulate_flaky_service("openai-gpt4o", fail_n_times=2)

print("\n>> Simulando servicio inestable: Edamam Food API (1 fallo transitorio)")
_demo_simulate_flaky_service("edamam-food-api", fail_n_times=1)

print("\n>> Simulando servicio inestable: SendGrid (3 fallos transitorios)")
_demo_simulate_flaky_service("sendgrid", fail_n_times=3)

print("\n>> Resumen de métricas de reintentos:")
import json
print(json.dumps(metrics.summary(), indent=2))

print("\n" + "=" * 60)
print("  Módulo listo para integrar en FastAPI de NutriSync AI")
print("  Import: from resilient_clients import openai_complete,")
print("          lookup_nutrients, send_report_email")
print("=" * 60 + "\n")

// qué_hace

Proporciona patrones listos para usar que hacen que las llamadas a servicios externos en Python fallen de forma elegante ante errores transitorios.

// cómo_lo_hace

Aplica decoradores de tenacity con backoff exponencial, jitter aleatorio y filtrado por tipo de excepción o código HTTP para reintentar solo los errores recuperables.

// ejemplo_de_uso

Cuando integras APIs de terceros como pagos o SMS que pueden fallar esporádicamente. Ej.: envuelves la llamada a Stripe con un decorador tenacity que reintenta 3 veces con backoff exponencial solo ante errores 429 o 503.

// plataformas

Pythontenacityhttpx
Categoría
IA, ingeniería y MLOps
Tipo
Referencia
Nivel
Intermedio
Licencia
MIT
Seguridad
seguro · riesgo bajo
Versión
1.0.0

// opiniones_de_la_comunidad

Opiniones

Cargando opiniones…

// pase_cultiva_ia

Llévate todo el arsenal con el Pase

Todas las skills, prompts y automatizaciones del catálogo en un único archivo, listas para usar: un pago, acceso de por vida y las novedades que añadamos. Sin suscripción.

Pago único · IVA incluido · pago seguro con Stripe.

Acceso inmediato · si no es lo que esperabas, te devolvemos los 10 €.