Module 6 · Async processing and messaging

Lesson 31 — Scheduled tasks and batches

Modern cron and batch processing without shooting yourself in the foot.

Published
In this lesson
  1. Exercise 1 — The catalog and its owner
  2. Exercise 2 — The batch
  3. Exercise 3 — Window idempotency
  4. Exercise 4 — Run locks
  5. Exercise 5 — The event closing
  6. Submit

Batches, locks and idempotency windows. No solutions.md before submitting.

Exercise 1 — The catalog and its owner

  1. Write TicketFlow's complete beat_schedule (the lesson's table) in the repo. Comment each entry with its business window and its idempotency mechanism.
  2. Hunt orphan crontabs: crontab -l on your dev/server and in the containers (a Dockerfile with CRON is a common sin). Document what you find and migrate what is yours into the repo.
  3. Decide the TZ: which two tasks of your catalog depend on the TZ, and how do you make them TZ-immune (scan window)?

Exercise 2 — The batch

  1. Implement liquidar_mes(mes) with the full pattern: iterator + 500 batch + commit per batch + sleep. Test with 1200 fake orgs: verify 3 commits (a transaction.atomic mock or the fake's counter) and that the process doesn't load 1200 orgs into memory (measure len(qs) vs the cursor: explain the difference).
  2. Add the checkpoint: if the run dies at org #843, the resume continues from there (id > ultimo). Test: run it "halfway" (an exception on the 3rd), relaunch, and verify entries are NOT duplicated.
  3. Measure the time with 10k fake rows and count the queries (CaptureQueriesContext): how many queries PER batch? How many would there be without values_list+iterator?

Exercise 3 — Window idempotency

  1. Add the UniqueConstraint (org, mes) to the ledger with its migration (11: expand-contract if you already have data). Test: liquidar_mes("2027-03") twice → same number of rows, zero duplicates.
  2. The 24h reminder: implement with the key (reserva, kind, fecha_local_evento) and Redis dedup. Test with FakeClock: run the task 4 times on the same day → ONE email per reservation.

Exercise 4 — Run locks

  1. Implement with_run_lock (Redis) and apply it to liquidar_mes and the daily closing. Test: with the lock taken (cache.add fails), the task returns None with a "skip" log and does NOT raise.
  2. Repeat with a Postgres advisory lock (pg_try_advisory_lock) in a management command. Which do you prefer in your deployment (43) and why? Write it in the 5-line ADR (48).
  3. Scenario: the lock's TTL is 4 h and the run takes 5 h. What exactly happens (who gets in, what gets corrupted)? Propose the fix (a heartbeat renewing the lock).

Exercise 5 — The event closing

  1. Implement cerrar_eventos_pasados(): events with ends_at < now → state CLOSED + EventClosed to the outbox (25) + releasing orphan seats (ACTIVE reservations of closed events → EXPIRED). All in batches.
  2. Integral test with FakeClock: yesterday's event with 2 reservations (1 ACTIVE, 1 CONFIRMED) → ACTIVE→EXPIRED, CONFIRMED untouched, outbox with 1 EventClosed + 1 ReservationExpired, and a second run changes NOTHING (idempotent).

Submit

Paste the beat_schedule, the batch test with checkpoint, the two locks compared and the closing's integral test. Next: Lesson 32 — Eventual consistency and sagas.