Stack: Django/DRF · Proyecto: TicketFlow Estado: Publicada — cierre del módulo de procesamiento asíncrono Prerrequisito: Lección 31 — Tareas programadas y lotes
Objetivos
- Distinguir consistencia fuerte (transacción local, 10) de eventual, y definir QUÉ es consistente al final en TicketFlow.
- Orquestar la saga de compra: pago + reserva + email con compensaciones cuando un paso remoto ya no se puede deshacer con rollback.
- 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:
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.UNKNOWNReglas 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
- ¿Qué eliges hacer fuerte y qué eventual en TicketFlow, y qué significa "convergencia definida" para lo eventual?
- Orquestación vs coreografía: ¿por qué la compra se orquesta y los emails se coreografían?
- Los tres resultados del borde remoto: ¿qué JAMÁS se hace ante UNKNOWN y cuál es la secuencia correcta?
- ¿Por qué la compensación es un hecho inverso y no un borrado? Enumera sus 4 reglas.
- ¿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.