Налаштування планировщика задач (Cron-like scheduler) для фоновых процесів
Системний cron — стандартний інструмент для періодичних завдань, але має обмеження: складність управління, відсутність історії виконання, відсутність обробки помилок, не працює в контейнерах без додаткового налаштування. Фреймворкові планировщики вирішують ці проблеми, додаючи керування через код.
Laravel Task Scheduler
Принцип роботи: одна запись у системному cron викликає планировщик кожну хвилину, а він сам вирішує, які завдання запустити:
# /etc/cron.d/laravel
* * * * * www-data php /var/www/artisan schedule:run >> /dev/null 2>&1
Всі розписання визначаються у routes/console.php (Laravel 9+) або app/Console/Kernel.php:
// routes/console.php
use Illuminate\Support\Facades\Schedule;
// Artisan-команди
Schedule::command('reports:daily')->dailyAt('02:00');
Schedule::command('sitemap:generate')->hourly();
Schedule::command('cache:clear-expired')->everyFifteenMinutes();
// Диспетчеризація Queue Job
Schedule::job(new CleanupOldUploadsJob())->weekly()->sundays()->at('03:00');
Schedule::job(new SyncExchangeRatesJob(), 'high')->everyThirtyMinutes();
// Довільний код
Schedule::call(function () {
DB::table('sessions')->where('last_activity', '<', now()->subDays(30))->delete();
})->daily()->name('cleanup-sessions')->withoutOverlapping();
// Shell-команда
Schedule::exec('node scripts/process-queue.js')->everyFiveMinutes();
Важливі модифікатори
withoutOverlapping() — не запускати завдання, якщо попередній запуск ще не завершився. Критично для довгих завдань:
Schedule::command('import:products')
->hourly()
->withoutOverlapping(10); // блокування на 10 хвилин
runInBackground() — не чекати завершення команди перед наступною. Планировщик продовжує роботу, поки завдання виконується у окремому процесі:
Schedule::command('reports:generate')->daily()->runInBackground();
onOneServer() — при кількох серверах виконувати завдання лише на одному. Вимагає cache-драйвер з підтримкою атомарних блокувань (Redis, Memcached):
Schedule::command('newsletter:send')
->dailyAt('09:00')
->onOneServer()
->withoutOverlapping();
between() — обмежити часовий діапазон:
Schedule::command('process:orders')
->everyMinute()
->between('08:00', '22:00'); // лише в робочі години
when() / skip() — умовне виконання:
Schedule::command('sync:users')
->hourly()
->skip(fn() => app()->isDownForMaintenance());
Зберігання історії виконання
За замовчуванням Laravel не зберігає історію завдань. Додаємо через хуки onSuccess/onFailure:
Schedule::command('reports:daily')
->dailyAt('02:00')
->before(function () {
ScheduleLog::create([
'command' => 'reports:daily',
'status' => 'started',
'started_at'=> now(),
]);
})
->onSuccess(function (\Illuminate\Foundation\Bus\PendingDispatch $pending) {
ScheduleLog::where('command', 'reports:daily')
->latest()
->first()
?->update(['status' => 'success', 'finished_at' => now()]);
})
->onFailure(function () {
ScheduleLog::where('command', 'reports:daily')
->latest()
->first()
?->update(['status' => 'failed', 'finished_at' => now()]);
Http::post(config('services.slack.webhooks.alerts'), [
'text' => ":x: Scheduled task `reports:daily` failed",
]);
});
Або використовуємо пакет spatie/laravel-schedule-monitor, який робить це автоматично для всіх завдань та інтегрується з Oh Dear для зовнішнього моніторингу.
Моніторинг через Healthcheck URL
Паттерн heartbeat: при успішному виконанні завдання пингує зовнішній сервіс (Healthchecks.io, Better Uptime, Dead Man's Snitch). Якщо пінг не прийшов — сервіс відправляє алерт:
Schedule::command('backup:run')
->daily()
->onSuccess(function () {
Http::get('https://hc-ping.com/' . config('services.healthchecks.backup_uuid'));
})
->onFailure(function () {
Http::get('https://hc-ping.com/' . config('services.healthchecks.backup_uuid') . '/fail');
});
Динамічні розписання з бази даних
Розписання з конфіга — це статика. Якщо потрібно керувати розписаннями через інтерфейс (наприклад, у кожного клієнта своя час відправки звіту):
// routes/console.php
use App\Models\ScheduledTask;
ScheduledTask::where('is_active', true)->each(function (ScheduledTask $task) {
$event = Schedule::call(function () use ($task) {
dispatch(new DynamicScheduledJob($task->id));
})
->cron($task->cron_expression)
->name("dynamic-task-{$task->id}")
->withoutOverlapping();
if ($task->only_on_weekdays) {
$event->weekdays();
}
});
Таблиця scheduled_tasks:
CREATE TABLE scheduled_tasks (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
cron_expression VARCHAR(100) NOT NULL, -- '0 9 * * 1-5'
job_class VARCHAR(500) NOT NULL,
payload JSONB,
is_active BOOLEAN DEFAULT true,
last_run_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Node.js: node-cron / agenda
Для Node.js-сервісів — node-cron (прості завдання) або agenda (з персистентністю у MongoDB):
// node-cron
import cron from 'node-cron';
cron.schedule('0 */2 * * *', async () => {
console.log('Running every 2 hours');
await syncExchangeRates();
}, {
scheduled: true,
timezone: 'Europe/Kiev',
});
// agenda з MongoDB — історія виконання з коробки
import Agenda from 'agenda';
const agenda = new Agenda({ db: { address: process.env.MONGODB_URI } });
agenda.define('send daily digest', async (job) => {
await sendDailyDigest(job.attrs.data.userId);
});
await agenda.start();
await agenda.every('24 hours', 'send daily digest', { userId: 123 });
Supervisor для планировщика
У контейнерному середовищі (Docker) системний cron може бути недоступним або небажаним. Альтернатива — запускати schedule:work (з'явилася у Laravel 8):
php artisan schedule:work
Це процес, який сам стежить за розписанням без системного cron. У Dockerfile:
CMD ["php", "artisan", "schedule:work"]
Або у Supervisor рядом з queue worker:
[program:scheduler]
command=php /var/www/artisan schedule:work
autostart=true
autorestart=true
user=www-data
stdout_logfile=/var/log/scheduler.log
Сроки
Переведення існуючих cron-завдань на Laravel Scheduler, базові модифікатори — 2–3 години. Зберігання історії, алертинг, healthcheck-інтеграція — ще 3–4 години. Динамічні розписання з БД — окремо, 5–7 годин.







