Питання на співбесіді: Scheduling
Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів
3 питання
Розклад описується в PHP, а не в crontab - тож він лежить у git разом із кодом.
Опис завдання у routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('vacancies:parse')->twiceDaily(6, 18);
Schedule::command('reports:send')->dailyAt('09:00');
Schedule::job(new CleanupTempFiles)->hourly();
Що потрібно на сервері - один запис у cron:
* * * * * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1
Саме так: щохвилини й один рядок на весь застосунок. Cron будить Laravel щохвилини, а той сам вирішує, чиє зараз час. Новий пункт розкладу не потребує змін у cron.
Перевірити, не чекаючи часу:
php artisan schedule:list # що і коли має виконатися
php artisan schedule:run # виконати те, чому час зараз
php artisan schedule:work # тримати планувальник локально, без cron
Типові помилки новачка:
- Прописати кожне завдання окремим рядком у crontab - тоді розклад із коду не працює зовсім.
- Забути
cdу шлях проєкту: команда не знайде застосунок. - Запускати від імені іншого користувача, ніж веб-сервер, - права на
storageне збігаються, і завдання падає на записі логу. - Не дивитися на вивід: без
emailOutputOnFailure()чи логування падіння завдання непомітне.
Планувальник дозволяє описати періодичні завдання прямо в коді. У Laravel 11+ розклад визначається в routes/console.php через фасад Schedule:
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:send')->dailyAt('08:00');
Schedule::job(new PruneLogs)->weekly();
Schedule::call(fn () => Cache::flush())->hourly();
На сервері потрібен лише один cron-запис, що щохвилини викликає планувальник:
* * * * * cd /app && php artisan schedule:run >> /dev/null 2>&1
Корисні модифікатори: withoutOverlapping(), onOneServer(), runInBackground().
На одному сервері планувальник працює очевидно. Щойно серверів стає два, зʼявляються дві різні проблеми, які плутають між собою.
1. Завдання виконується двічі. Cron стоїть на обох серверах, тож щохвилинний schedule:run запускається двічі - і звіт розсилається двічі. Лікується onOneServer():
Schedule::command('reports:send')
->daily()
->onOneServer();
Перший сервер бере атомарне блокування в кеші, другий пропускає запуск. Драйвером за замовчуванням має бути database, memcached, dynamodb або redis, і всі сервери мусять дивитися в один центральний кеш; з file у кожного вузла кеш свій, тож блокування нічого не гарантує.
2. Завдання накладається саме на себе. Імпорт триває сім хвилин, а запускається щоп'ять - і другий екземпляр стартує поверх першого. Це вже про час виконання, а не про кількість серверів:
Schedule::command('import:run')
->everyFiveMinutes()
->withoutOverlapping();
За замовчуванням блокування тримається 24 години; якщо процес упав без звільнення, наступний запуск чекатиме. Тому варто задавати межу: withoutOverlapping(10).
Разом, коли потрібні обидві гарантії:
Schedule::command('import:run')
->everyFiveMinutes()
->withoutOverlapping(10)
->onOneServer();
Ще дві дрібниці, які кусаються. Часовий пояс планувальника береться з конфігурації застосунку - ->timezone('Europe/Kyiv') задає його явно. І завдання, що не пише жодного логу, мовчки не виконується місяцями; emailOutputOnFailure() або ->onFailure() роблять збій помітним.