Skip to content

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.

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.

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

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-service en 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 si meetingUrl ya está en session_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.

GET /api/admin/jobs # lista los 11
POST /api/admin/jobs/<nombre> # ejecuta uno

Ambos 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.