Lotes, locks y ventanas de idempotencia. Sin solutions.md hasta entregar.
Ejercicio 1 — El catálogo y su dueño
- Escribe el
beat_schedulecompleto de TicketFlow (la tabla de la lección) en el repo. Comenta cada entrada con su ventana de negocio y su mecanismo de idempotencia. - Busca crontabs huérfanas:
crontab -len tu dev/servidor y en los contenedores (Dockerfile conCRONes un pecado común). Documenta lo que encuentres y migra lo que sea tuyo al repo. - Decide la TZ: ¿qué dos tareas de tu catálogo dependen de la TZ y cómo las haces TZ-inmunes (ventana de escaneo)?
Ejercicio 2 — El lote
- Implementa
liquidar_mes(mes)con el patrón completo: iterator + lote 500 + commit por lote + sleep. Test con 1200 orgs fake: verifica 3 commits (mock de transaction.atomic o contador del fake) y que el proceso no carga 1200 orgs en memoria (midelen(qs)vs el cursor: explica la diferencia). - Añade el checkpoint: si la corrida muere en la org #843, la reanudación continúa desde ahí (
id > ultimo). Test: corre "a medias" (excepción en la 3ª), re-lanza, y verifica que NO se duplican entradas. - Mide el tiempo con 10k filas fake y cuenta las consultas (
CaptureQueriesContext): ¿cuántas queries POR lote? ¿Cuántas serían sinvalues_list+iterator?
Ejercicio 3 — Idempotencia de ventana
- Añade el
UniqueConstraint (org, mes)al ledger con su migración (11: expand-contract si ya tenías datos). Test:liquidar_mes("2027-03")dos veces → mismo número de filas, cero duplicados. - El recordatorio 24h: implementa con clave
(reserva, kind, fecha_local_evento)y dedup Redis. Test con FakeClock: corre la tarea 4 veces en el mismo día → UN email por reserva.
Ejercicio 4 — Locks de corrida
- Implementa
with_run_lock(Redis) y aplícalo aliquidar_mesy al cierre diario. Test: con el lock ocupado (cache.add falla), la tarea retorna None con log "skip" y NO lanza error. - Repite con advisory lock de Postgres (
pg_try_advisory_lock) en un comando de management. ¿Cuál prefieres en tu despliegue (43) y por qué? Escríbelo en el ADR de 5 líneas (48). - Escenario: el TTL del lock es 4 h y la corrida tarda 5 h. ¿Qué ocurre exactamente (quién entra, qué corrompe)? Propón el fix (heartbeat que renueva el lock).
Ejercicio 5 — El cierre de eventos
- Implementa
cerrar_eventos_pasados(): eventos conends_at < ahora→ estado CLOSED +EventClosedal outbox (25) + liberación de asientos huérfanos (reservas ACTIVE de eventos cerrados → EXPIRED). Todo en lotes. - Test integral con FakeClock: evento ayer con 2 reservas (1 ACTIVE, 1 CONFIRMED) → ACTIVE→EXPIRED, CONFIRMED intacta, outbox con 1
EventClosed+ 1ReservationExpired, y una segunda corrida no cambia NADA (idempotente).
Entrega
Pega el beat_schedule, el test del lote con checkpoint, los dos locks comparados y el test integral del cierre. Después: Lección 32 — Consistencia eventual y saga.