Módulo 6 · Procesamiento asíncrono y mensajería

Lección 32 — Consistencia eventual y saga

El pago, la reserva y el correo: tres sistemas, una historia coherente.

Publicada
En esta lección
  1. Objetivos
  2. 1. Fuerte donde importa, eventual donde no
  3. 2. La saga de compra: orquestación sobre coreografía
  4. 3. El borde remoto: sí, no y NO SÉ
  5. 4. Compensaciones: el deshacer que sí existe
  6. 5. El diagrama completo y las garantías resultantes
  7. Autoevaluación

Stack: Django/DRF · Proyecto: TicketFlow Estado: Publicada — cierre del módulo de procesamiento asíncrono Prerrequisito: Lección 31 — Tareas programadas y lotes


Objetivos

  1. Distinguir consistencia fuerte (transacción local, 10) de eventual, y definir QUÉ es consistente al final en TicketFlow.
  2. Orquestar la saga de compra: pago + reserva + email con compensaciones cuando un paso remoto ya no se puede deshacer con rollback.
  3. Manejar los tres estados del borde remoto (sí/no/no-sé) con timeouts y reconocimiento por evento.

1. Fuerte donde importa, eventual donde no

La transacción de Postgres (10) da consistencia FUERTE dentro de una BD: reserva + asientos + outbox son atómicos. Pero el pago vive en la pasarela y el email en SMTP: no hay rollback en sistemas ajenos. La decisión de arquitectura es elegir qué es fuerte y qué es eventual: el dinero y el inventario son fuertes (transacción local + locks); las notificaciones y los reportes son eventuales (el email llega 30 s después; el dashboard, 2 min). "Eventual" no es "espera y reza": es una garantía con convergencia definida — si no hay fallo permanente, el estado converge (los reintentos de la 29-30); si lo hay, la alerta (46) dispara la reparación humana.

La trampa del principiante: hacer eventuales cosas que el usuario espera (la disponibilidad al comprar) o fuertes cosas imposibles (confirmar la reserva en la MISMA transacción que cobra la pasarela — dos recursos, sin transacción distribuida; por eso existe la saga).

2. La saga de compra: orquestación sobre coreografía

Una saga es la secuencia de transacciones locales con compensaciones: cada paso remoto tiene su deshacer (o su aceptación del hecho). Dos estilos: coreografía (cada servicio reacciona a eventos, nadie manda) y orquestación (un orquestador conoce la historia y manda comandos). TicketFlow elige orquestación para la compra (la historia tiene estados y plazos legales: el usuario ve "tu reserva se confirmará en breve") y coreografía para lo periférico (analytics, emails informativos reaccionan al stream de la 30).

ESTADOS:  PENDING → PAID → CONFIRMED
                     ↘ PAYMENT_FAILED → (compensación) → EXPIRED/CANCELLED

Orquestador (servicio Django, transacciones locales):
1. reservar (TX local: asientos+outbox)                    [fuerte, 10]
2. cobrar en pasarela (comando remoto, intent.id idempotente, 14)
3. al recibir PaymentSucceeded (evento, 25) → CONFIRMED + email + webhooks (on_commit, 29)
4. si PaymentFailed → compensar: liberar asientos + email "prueba otro método"

El orquestador NO es un microservicio: es un servicio Django con una tabla de estados (PurchaseSaga: saga_id, reserva, intent_id, estado, intentos). Persistente, no en memoria: el proceso puede morir y otro retoma (la tabla es la verdad, el estilo stateless del 55).

3. El borde remoto: sí, no y NO SÉ

El paso remoto tiene tres resultados, no dos: éxito, fallo claro, y no sé (timeout: ¿cobró o no?). El manejo del "no sé" es la diferencia entre un backend adulto y una pesadilla de soporte:

python
def ejecutar_paso(self, comando) -> Outcome:
    try:
        return self.gateway.cobrar(comando)           # SUCCEEDED | DECLINED
    except GatewayTimeout:
        # NO SÉ: NO compensar todavía (podría haber cobrado)
        return Outcome.UNKNOWN

Reglas del UNKNOWN: (1) nunca compensar en ciego — liberar asientos de una reserva cuyo cargo SÍ entró es regalar dinero y asiento; (2) preguntar: GET /charges/{intent.id} (la pasarela responde el estado final, por eso el intent.id es idempotente, 14); (3) si sigue sin saberse, reintentar la consulta con backoff (30) hasta el plazo máximo, y ahí sí compensar con registro auditable (el ledger de la 08 deja la pista). El timeout SIEMPRE se define por operación (la 54 lo sistematiza): sin timeout, el "no sé" es eterno.

4. Compensaciones: el deshacer que sí existe

La compensación no borra el pasado: añade un hecho inverso. El cargo no se "borra": se REEMBOLSA (ledger: dos entradas, la 08); la reserva no se des-configura: pasa a CANCELLED con motivo saga_compensated. Reglas de las compensaciones:

  • Idempotentes (pueden ejecutarse dos veces: el reembolso con el mismo intent/clave no duplica, 14).
  • Con orden inverso al de ejecución (si cobraste y reservaste, compensa reembolsando y liberando — la pila).
  • Registradas como eventos (RefundIssued, ReservationExpired) para que los consumidores periféricos (emails, analytics) converjan sin orquestación (30).
  • Con plazo: la compensación pendiente es un estado visible (compensation_pending) que el job diario (31) persigue y alerta si envejece.

5. El diagrama completo y las garantías resultantes

                ┌────────────────────────────────────────────┐
                │  PurchaseSaga (tabla: estado, intentos)    │
                └────────────────────────────────────────────┘
   comando         │ evento (stream, 30)          comando
        ┌──────────┴──────────┐               ┌───────────┴──────────┐
   Pasarela ──PaymentSucceeded──▶ CONFIRMED │  Pasarela (cobro)    │
        │        (webhook 17 / poll)          │  timeout → UNKNOWN   │
        ▼                                     │  → GET intent → fix  │
   PaymentFailed ──▶ compensar (refund + liberar + email)            │

Garantías que el sistema ofrece al negocio: la reserva y los asientos son fuertes (10); el pago converge (el intent responde sí/no con consulta de reconciliación); los emails/webhooks son eventuales con at-least-once + dedup (29/30); la saga entera es observable (cada transición es evento + métrica, 46) y reanudable (la tabla sobrevive reinicios). Lo que NO ofrece: atomicidad global entre BD y pasarela — nadie la ofrece; la saga es la respuesta honesta.


Autoevaluación

  1. ¿Qué eliges hacer fuerte y qué eventual en TicketFlow, y qué significa "convergencia definida" para lo eventual?
  2. Orquestación vs coreografía: ¿por qué la compra se orquesta y los emails se coreografían?
  3. Los tres resultados del borde remoto: ¿qué JAMÁS se hace ante UNKNOWN y cuál es la secuencia correcta?
  4. ¿Por qué la compensación es un hecho inverso y no un borrado? Enumera sus 4 reglas.
  5. ¿Qué garantiza la tabla PurchaseSaga que la memoria del proceso no, y qué pasa cuando el worker muere a mitad de saga?

Continúa con los ejercicios. Las solutions.md solo tras intentarlo.