Background Jobs
No hay BullMQ ni Redis. Los trabajos de fondo son Cron Triggers de Cloudflare
Workers: apps/api/wrangler.jsonc declara los crons, src/jobs/index.ts los
reparte por expresión y hora UTC, y src/jobs/runners/* los implementa. El
stack Node que los arrancaba con BullMQ se retiró el 7-sep-2026 con el servicio
de Cloud Run.
El cruce entre los dos ficheros está gateado por
src/jobs/__tests__/dispatch.test.ts: lee los crons declarados y falla si un
runner se queda sin ninguno que lo alcance, si los dos bloques de
wrangler.jsonc divergen, o si se declara un cron que el despachador ignora.
Desincronizarlos no rompe nada visible —el trabajo simplemente deja de
hacerse—, por eso hay un test y no una nota.
Migración a D1: un solo cron
Section titled “Migración a D1: un solo cron”Mientras dura la migración a D1, triggers.crons es solo ["0 * * * *"]. El
cron */5 consultaba Neon cada cinco minutos y no la dejaba dormir (44 de las
100 CU-h/mes del plan Free en 7 días). El tick horario carga también los
cuatro runners frecuentes. Los once son scans idempotentes de “todo lo
vencido”, así que el efecto es latencia de hasta 60 minutos, no trabajo
perdido: un tick perdido se autocura en el siguiente.
Los once runners
Section titled “Los once runners”| Runner | Cadencia (post-migración) | Qué hace |
|---|---|---|
auto-complete |
cada 5 min | Completa sesiones pasada la hora de fin + gracia (modos AUTO_COMPLETE y PENDING_REVIEW) |
calendar-sync |
cada 5 min | Reintenta en lotes los session_calendar_sync en pending. Se salta si google-calendar está en cooldown |
scheduled-messages |
cada 5 min | Dispara los scheduled_messages vencidos → contact_log + notificación |
session-reminder |
cada 15 min | Recordatorios de sesión a alumno y profesor |
waitlist-expiry |
cada hora | Caduca ofertas de lista de espera y avisa al siguiente en la cola |
audit-cleanup |
02:00 del día 1 | Purga de audit_logs por tramos: financiero 7 años, alumno 3, general 1, resto 6 meses |
data-retention |
03:00 del domingo | Retención GDPR: alumnos inactivos, session_content, contact_log, solicitudes de borrado vencidas |
content-analytics |
04:00 diario | Agregación de analítica de contenido |
platform-snapshot |
05:00 diario | Snapshot de uso hacia Platform |
lifecycle-detection |
06:00 diario | Ciclo de vida del alumno: huecos, churn, matrículas por vencer, rachas |
metric-alerts |
07:00 diario | Umbrales de KPI del profesor → notificaciones de alerta |
Colas, no cron
Section titled “Colas, no cron”Dos trabajos siguen siendo bajo demanda y viajan por CF Queues
(src/jobs/consumers.ts, cola pinteach-google-jobs con DLQ):
- calendar-sync de una sesión, encolado por
session-serviceen cada create/update/delete. - meet-link, encolado junto al anterior; no hace nada si el profesor no
tiene el scope
meetings.space.created, y se salta simeetingUrlya está ensession_calendar_sync.
Si no hay binding de cola (tests, wrangler dev pelado), sendGoogleJob()
devuelve false y el caller cae a su plan B: el calendar deja la fila en
pending para el cron batch, y el enlace de Meet lo provisiona el propio
calendar sync.
Disparar uno a mano
Section titled “Disparar uno a mano”GET /api/admin/jobs # lista los 11POST /api/admin/jobs/<nombre> # ejecuta unoAmbos con sesión de operador (SSO SAASTRO en /admin). Es la vía de verificación en producción
y la válvula de escape mientras la cadencia es horaria.