Apariencia
Cronjobs y tareas programadas
Catálogo consolidado de todo lo que se ejecuta de forma autónoma en el backend de HUMAE: jobs programados vía Laravel Scheduler, cronjobs del sistema operativo, y trabajos planificados para Fase 2.
Arquitectura
HUMAE usa dos capas de automatización:
┌──────────────────────────────────────────────────┐
│ 1. Cron del sistema operativo │
│ │
│ Entry único en /etc/crontab (www-data): │
│ * * * * * php artisan schedule:run │
│ │
│ Se ejecuta cada minuto, delega a Laravel. │
└──────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────┐
│ 2. Laravel Scheduler │
│ │
│ Registrado en routes/console.php (o Kernel.php):│
│ Schedule::job(new ExpireMembershipsJob) │
│ ->dailyAt('00:15')->onOneServer(); │
│ │
│ Laravel decide qué jobs correr este minuto. │
└──────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────┐
│ 3. Queue worker (Supervisor) │
│ │
│ Los jobs encolados (ShouldQueue) se procesan │
│ aquí, no directamente desde el scheduler. │
└──────────────────────────────────────────────────┘Adicionalmente hay cronjobs externos (no Laravel) para cosas como backups de DB.
Inventario maestro — MVP actual
Jobs programados en Laravel
| Job | Frecuencia | Hora | Timezone | Qué hace | Tablas que toca |
|---|---|---|---|---|---|
ExpireMembershipsJob | diario | 00:15 | America/Mexico_City | Marca como expired las membresías con expires_at <= now() | memberships |
Total actual: 1 job programado.
Cronjobs externos (sistema operativo)
| Tarea | Frecuencia | Hora | Descripción | Implementación |
|---|---|---|---|---|
| Laravel scheduler tick | cada minuto | — | Entry base que dispara schedule:run | * * * * * php artisan schedule:run |
| Backup MySQL | diario | 03:00 UTC | mysqldump → S3 | Script /usr/local/bin/humae-db-backup.sh |
| Rotación de logs Laravel | diaria | automática | Laravel con LOG_CHANNEL=daily mantiene los últimos 14 archivos | Sin cronjob, es parte del driver |
| Let's Encrypt renew (si aplica) | 2 veces/día | 00:00 y 12:00 | Renovar certificados SSL | certbot.timer de systemd |
Detalle de cada job
ExpireMembershipsJob
Archivo: app/Jobs/ExpireMembershipsJob.php
Qué hace exactamente:
- Query
membershipsdondestatus = activeANDexpires_at <= now(). - Actualiza
status = expired+updated_at = now(). - Loguea el count:
[ExpireMembershipsJob] expired N memberships.
Efectos secundarios:
- El candidato deja de aparecer en el directorio (
DirectorySearchService::applyMembershipFilter). - Sus asignaciones activas y entrevistas agendadas siguen funcionando.
- No envía notificación al candidato en MVP (pendiente Fase 2 con
MembershipExpiredNotification).
Registro del schedule:
php
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::job(new ExpireMembershipsJob)
->timezone('America/Mexico_City')
->dailyAt('00:15')
->name('memberships:expire')
->onOneServer()
->withoutOverlapping(10);Por qué 00:15 y no 00:00: evita coincidir con otros jobs que podrían correr al inicio exacto del día (backups, rotaciones) y da margen si el reloj del server está ligeramente desfasado.
Dependencias:
- Servicio inyectado:
MembershipService(Laravel resuelve automáticamente). - Acceso a DB MySQL (conexión default).
Falla si:
- MySQL no disponible → job falla, reintenta 3 veces (definido en
public int $tries = 3). - Si falla las 3 veces → pasa a
failed_jobs, alerta porNotification::route('mail', 'admin@humae.com.mx')si se configuró (Fase 2).
Cómo ver qué está programado
Listar todo
bash
php artisan schedule:listSalida esperada:
+--------+----------------+-----------------------------+----------+
| 0 15 * * * | memberships:expire | ExpireMembershipsJob | 00:15 (America/Mexico_City) |
| * * * * * | schedule:interrupt | | |
+--------+----------------+-----------------------------+----------+Ejecutar el scheduler manualmente (dry run)
bash
# Ejecuta cualquier job cuya hora coincida con "now"
php artisan schedule:run
# Probar un job específico (si está nombrado)
php artisan schedule:test
# Muestra menú interactivoForzar un job fuera de horario
Para testing en prod o desarrollo:
bash
php artisan tinker
>>> \App\Jobs\ExpireMembershipsJob::dispatchSync();
# Se ejecuta inmediato, bloqueante, ignora scheduleO desde CLI:
bash
php artisan queue:work --once
# Procesa el primer job en la colaDebugging en producción
¿Se está ejecutando el scheduler?
bash
# 1. Verificar que el cronjob OS está activo
sudo crontab -u www-data -l
# Debe incluir: * * * * * cd /var/www/humae_backend && php artisan schedule:run >> /dev/null 2>&1
# 2. Ver si Laravel lo recibe
tail -f storage/logs/laravel-$(date +%F).log | grep -i "schedule\|ExpireMembership"
# 3. Dry run manual
cd /var/www/humae_backend
php artisan schedule:run -v
# Output verbose de qué se ejecutó y qué no¿El job corrió pero no hizo nada?
bash
php artisan tinker
# Ver cuántas membresías hay activas que deberían expirar
>>> \App\Models\Membership::where('status', 'active')
... ->where('expires_at', '<=', now())
... ->count();
# Si hay pero el job no las tocó, revisar timezone del servidor vs app
>>> now()->timezone;
>>> config('app.timezone');Causa común: timezone mismatch. El server corre en UTC pero el job espera America/Mexico_City. Si hay 6h de diferencia, el job de medianoche mexicana corre a las 06:15 UTC.
¿El job falló?
bash
php artisan queue:failed
# Lista de failed_jobs
# Ver detalle de uno
php artisan queue:failed:show <uuid>
# Reintentar
php artisan queue:retry <uuid>
# O limpiar
php artisan queue:forget <uuid>Logs del worker:
bash
sudo supervisorctl tail -f humae-queue:humae-queue_00 stderrCómo agregar un nuevo cronjob
Paso 1 — Crear el job
bash
php artisan make:job SendMembershipExpiringWarningJobArchivo generado en app/Jobs/SendMembershipExpiringWarningJob.php:
php
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Enums\MembershipStatus;
use App\Models\Membership;
use App\Notifications\MembershipExpiringNotification;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
final class SendMembershipExpiringWarningJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 120;
public function handle(): void
{
$warningDays = (int) config('humae.membership.expiring_warning_days', 15);
$expiring = Membership::query()
->with('user')
->where('status', MembershipStatus::Active->value)
->whereBetween('expires_at', [now(), now()->addDays($warningDays)])
->whereDoesntHave('warningsSent', fn ($q) =>
$q->where('type', 'expiring_soon')
->where('sent_at', '>', now()->subDays($warningDays))
)
->get();
foreach ($expiring as $membership) {
$membership->user?->notify(new MembershipExpiringNotification($membership));
// Registrar en membership_notifications_sent para no duplicar
}
logger()->info("[SendMembershipExpiringWarningJob] notified {$expiring->count()} users");
}
}Paso 2 — Registrar en el scheduler
routes/console.php:
php
use App\Jobs\SendMembershipExpiringWarningJob;
Schedule::job(new SendMembershipExpiringWarningJob)
->timezone('America/Mexico_City')
->dailyAt('09:00') // buena hora para correos
->name('memberships:warn')
->onOneServer()
->withoutOverlapping(30);Paso 3 — Actualizar esta documentación
Agregar fila a la tabla "Inventario maestro" en esta página.
Paso 4 — Verificar
bash
php artisan schedule:list
# Debe listar "memberships:warn" a las 09:00
php artisan schedule:test
# Ejecutar manualmente para verificar el comportamientoHelpers de frecuencia
Laravel Scheduler ofrece una DSL legible. Los más usados:
| Método | Qué hace |
|---|---|
->everyMinute() | cada minuto |
->everyFiveMinutes() | cada 5 minutos |
->everyTenMinutes() | cada 10 |
->everyThirtyMinutes() | cada 30 |
->hourly() | cada hora, al :00 |
->hourlyAt(15) | cada hora, al :15 |
->daily() | diario a las 00:00 |
->dailyAt('09:00') | diario a las 09:00 |
->twiceDaily(9, 21) | dos veces al día |
->weekly() | semanal (domingo 00:00) |
->weeklyOn(1, '08:00') | semanal (lunes 08:00) |
->monthly() | mensual (día 1 00:00) |
->monthlyOn(15, '03:00') | mensual (día 15 03:00) |
->quarterly() | trimestral |
->yearly() | anual |
->cron('*/5 * * * *') | expresión cron cruda |
Timezone
Siempre setear explícitamente para evitar bugs cuando el server cambia de zona:
php
Schedule::job(new MyJob)
->timezone('America/Mexico_City')
->dailyAt('09:00');Overlap protection
Si un job puede tardar más que su intervalo, proteger:
php
Schedule::job(new LongJob)
->hourly()
->withoutOverlapping(60); // lock expira a los 60 minMulti-server safety
Si tienes varias instancias del backend con cron activo, evitar que todas corran el mismo job:
php
Schedule::job(new MyJob)
->daily()
->onOneServer();Requiere que uses CACHE_STORE=redis o database (no file).
Condicionales
php
Schedule::job(new ReindexSearchJob)
->hourly()
->when(fn () => config('features.search_enabled'));
Schedule::job(new SomeJob)
->daily()
->environments(['production', 'staging']); // no corre en localFase 2 — Cronjobs planificados
Jobs mencionados en el roadmap que aún no están implementados:
| Job propuesto | Frecuencia sugerida | Propósito |
|---|---|---|
SendMembershipExpiringWarningJob | diario 09:00 | Email 15 días antes de expirar membresía |
NotifyMembershipExpiredJob | diario 09:00 | Email tras expiración + sugerencia de renovar |
SendInterviewReminder24hJob | cada hora | Recordatorio 24h antes de entrevistas confirmadas |
SendInterviewReminder1hJob | cada 15 min | Recordatorio 1h antes + enlace Meet |
NotifyVacancyExpiringJob | diario 09:00 | Avisar a empresas de vacantes próximas a expires_at |
AutoCancelExpiredVacanciesJob | diario 00:30 | Marcar como cancelada vacantes con expires_at < now() y estado no terminal |
CleanupDeletedAccountsJob | semanal | Borrar data de cuentas soft-deleted > 90 días (GDPR/LFPDPPP) |
PreAggregateReportsJob | diario 02:00 | Calcular métricas del día para /admin/reportes y cachear en Redis |
ReindexDirectorySearchJob | diario 03:00 | Si se agrega Meilisearch/Algolia |
PrunePsychometricAttemptsJob | mensual | Limpiar intentos in_progress abandonados > 30 días |
NotifyStaleFavoritesJob | semanal | Avisar al recruiter sobre favoritos sin acción > 30 días |
Al implementar cualquiera de estos, actualizar esta página con frecuencia, hora y propósito.
Cronjobs externos del sistema operativo
Estos NO son Laravel, son parte de la infra:
1 · Backup de base de datos
Ubicación: /usr/local/bin/humae-db-backup.sh
bash
#!/bin/bash
set -e
DATE=$(date +%F-%H%M)
DUMP_FILE=/tmp/humae_${DATE}.sql.gz
mysqldump \
-h $DB_HOST -u humae_backup -p"$BACKUP_PASSWORD" \
--single-transaction --routines --triggers \
humae_production | gzip > $DUMP_FILE
aws s3 cp $DUMP_FILE s3://humae-backups/db/ \
--storage-class STANDARD_IA
rm $DUMP_FILE
# Alerta si falla
if [ $? -ne 0 ]; then
curl -X POST https://hooks.slack.com/services/... \
-d '{"text":"❌ HUMAE DB backup failed"}'
fiCrontab:
0 3 * * * /usr/local/bin/humae-db-backup.sh >> /var/log/humae-backup.log 2>&1Retención: 30 días en S3 (lifecycle rule), 1 día local.
2 · Rotación de logs de Nginx
Laravel rota logs propios con LOG_CHANNEL=daily. Nginx usa logrotate estándar de Linux:
/etc/logrotate.d/nginx-humae:
/var/log/nginx/humae_*.log {
daily
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
/usr/sbin/nginx -s reopen
endscript
}3 · Renovación SSL (Let's Encrypt)
Si usas certbot, ya viene configurado con systemd timer:
bash
sudo systemctl list-timers | grep certbot
# → certbot.timer 2x al díaSi usas Cloudflare como proxy o gestionado (Vercel/Render), este paso no aplica.
4 · Limpieza de uploads temporales
Si alguna vez acumulas archivos en /tmp:
0 4 * * * find /tmp -name "humae_*" -mtime +7 -deleteMonitoreo operativo
Alertas recomendadas
Job no se ejecutó en las últimas 24h → Dead man's switch. Recomendado con Dead Man's Snitch o similar.
phpSchedule::job(new ExpireMembershipsJob) ->dailyAt('00:15') ->pingOnSuccess('https://nosnch.in/abc123');failed_jobscrece → alerta si supera N entries en 1h.Queue backlog → alerta si
LLEN queues:default> 100.Backup DB fallido → alerta desde el script (curl a Slack webhook).
Dashboard admin (Fase 2)
Ruta /admin/cronjobs con:
- Listado de schedules registrados con última ejecución + próxima ejecución
- Botón "ejecutar ahora" por cada uno (requires permission)
- Gráfico de success rate
- Logs inline
Implementación sugerida: spatie/laravel-schedule-monitor.
Relaciones con otros documentos
- Configurar el worker + cron en el servidor: Infraestructura → Queue y scheduler.
- Variables necesarias: Variables de entorno.
- Verificación post-deploy de que el scheduler corre: Verificación.
- Notifications disparadas por cronjobs: Catálogo de eventos.
- Sistema de queue que ejecuta los jobs: Notifications y jobs.
Siguiente
Cuándo falla algo y cómo diagnosticarlo: Troubleshooting →

