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

Lección 31 — Tareas programadas y lotes

Cron moderno y procesamiento por lotes sin pegarte un tiro en el pie.

Publicada
En esta lección
  1. Ejercicio 1 — El catálogo y su dueño
  2. Ejercicio 2 — El lote
  3. Ejercicio 3 — Idempotencia de ventana
  4. Ejercicio 4 — Locks de corrida
  5. Ejercicio 5 — El cierre de eventos
  6. Entrega

Lotes, locks y ventanas de idempotencia. Sin solutions.md hasta entregar.

Ejercicio 1 — El catálogo y su dueño

  1. Escribe el beat_schedule completo de TicketFlow (la tabla de la lección) en el repo. Comenta cada entrada con su ventana de negocio y su mecanismo de idempotencia.
  2. Busca crontabs huérfanas: crontab -l en tu dev/servidor y en los contenedores (Dockerfile con CRON es un pecado común). Documenta lo que encuentres y migra lo que sea tuyo al repo.
  3. 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

  1. 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 (mide len(qs) vs el cursor: explica la diferencia).
  2. 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.
  3. Mide el tiempo con 10k filas fake y cuenta las consultas (CaptureQueriesContext): ¿cuántas queries POR lote? ¿Cuántas serían sin values_list+iterator?

Ejercicio 3 — Idempotencia de ventana

  1. 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.
  2. 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

  1. Implementa with_run_lock (Redis) y aplícalo a liquidar_mes y al cierre diario. Test: con el lock ocupado (cache.add falla), la tarea retorna None con log "skip" y NO lanza error.
  2. 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).
  3. 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

  1. Implementa cerrar_eventos_pasados(): eventos con ends_at < ahora → estado CLOSED + EventClosed al outbox (25) + liberación de asientos huérfanos (reservas ACTIVE de eventos cerrados → EXPIRED). Todo en lotes.
  2. Test integral con FakeClock: evento ayer con 2 reservas (1 ACTIVE, 1 CONFIRMED) → ACTIVE→EXPIRED, CONFIRMED intacta, outbox con 1 EventClosed + 1 ReservationExpired, 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.