У матеріалі розбирається підхід Arowosaye Remilekun з пакету cyberwizard/schedule-catchup. Його команда вирішує практичну проблему: як у Laravel дограти ті задачі з розкладу, які були пропущені під час простою сервера.
У чому проблема з пропущеними Laravel-задачами
Laravel Scheduler виконує завдання тільки в момент, коли спрацьовує cron. Якщо сервер був вимкнений о 00:00, команда, яка мала відпрацювати опівночі, просто не запускається після відновлення роботи. Framework з коробки не намагається «надолужити» пропущені виклики.
Пакет Schedule Catchup записує активність планувальника в базу та під час чергового запуску перевіряє, які запуски за розкладом пропущені. Для таких подій він може по черзі дограти старі «слоти» cron, починаючи з найстаріших.
Як позначити задачі, які потрібно відтворювати
За замовчуванням пакет не чіпає всі задачі підряд. Розробник має явно позначити події, які потрібно відтворювати після простою.
У файлі routes/console.php до потрібної події додається виклик replayOnCatchup():
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:daily')
->dailyAt('00:00')
->withoutOverlapping()
->onOneServer()
->replayOnCatchup();
Ключові деталі підходу:
- Відтворюються тільки події, позначені
replayOnCatchup(). Це зменшує ризики дублювання побічних ефектів. - Добрим кандидатом є задачі, де потрібен результат за кожен день (наприклад, добовий звіт).
- Приклад, який має мало сенсу відтворювати: воркер, що кожні 5 хвилин перевіряє чергу робіт. Він і так при наступному запуску побачить залишки в черзі.
Для кожної позначеної події пакет «проходиться» по часових слотах cron у минулому, відкидаючи ті, які вже:
- позначені як успішні, або
- позначені як «вичерпані» (exhausted) після граничної кількості спроб.
Невідпрацьовані входження для цієї події виконуються у порядку від найстарішого до найновішого. Перший запуск пакету працює обережно: якщо немає попереднього checkpoint, він не намагається автоматично відтворити невідому історію, а просто фіксує базову точку відліку.
Як використовувати «правильну» дату запланованого запуску
Якщо команду, яка розраховує дати від now(), запустити заднім числом, вона може обробити не той період. Наприклад, добовий звіт, що мав порахувати дані за п'ятницю, при відтворенні у суботу може випадково взяти суботні дані.
Щоб уникнути цього, у пакеті є трейтом ResolvesScheduledTime, який додає метод scheduledFor():
- при звичайному запуску він повертає поточний час;
- при відтворенні повертає мітку часу пропущеного запуску.
Приклад команди, яка обирає звітний період як «попередній день від запланованого часу»:
namespace App\Console\Commands;
use Cyberwizard\ScheduleCatchup\Traits\ResolvesScheduledTime;
use Illuminate\Console\Command;
class DailyReport extends Command
{
use ResolvesScheduledTime;
protected $signature = 'reports:daily';
public function handle(): int
{
$reportDate = $this->scheduledFor()->subDay()->toDateString();
$this->info("Reporting date: {$reportDate}");
// Generate or update the report for $reportDate here.
return self::SUCCESS;
}
}
Важливі нюанси використання:
- Існуючі виклики
now()у вашому коді як і раніше працюватимуть з поточним часом. Щоб логіка була коректною при відтворенні, у чутливих до дати місцях потрібно перейти на$this->scheduledFor(). - Для команд, яким під час відтворення потрібно поводитися інакше (наприклад, пропускати частину побічних ефектів), трейтом також надається метод
isCatchupReplay().
Ліміти відтворення, повторні спроби та інспекція черги
Щоб відтворення не «з'їло» всі ресурси, пакет вводить обмеження й механізми керування чергою.
Ліміт у 50 входжень за один прохід
За замовчуванням пакет відпрацьовує не більше ніж 50 пропущених входжень для однієї події за один «прохід» cron.
У версії 1.1.0, якщо ліміт досягнутий, у базі зберігається курсор, який вказує, де пакет зупинився. Завдяки цьому залишок відставання можна продовжити обробляти у наступних проходах, не починаючи з нуля.
Обробка збоїв та відкладених входжень
- Провалені та відкладені входження записуються окремо й можуть бути повторно запущені.
- Повторні спроби зупиняються, коли лічильник спроб досягає
max_attempts(за замовчуванням 3). - Якщо сталася «перекриття» (overlap) і задача не може бути виконана, це входження вважається відкладеним, а не успішним.
- Починаючи з v1.1.0, задачі, які зазвичай запускаються у фоні, під час відтворення запускаються у «foreground» режимі, щоб пакет міг коректно зафіксувати їхні коди виходу.
Перегляд кандидатів перед виконанням
Автор пакету пропонує спочатку подивитися, що саме буде відтворюватися, а вже потім запускати реальне виконання. Для цього є команда:
php artisan schedule-catchup:run --dry-run --only=reports:daily
Пояснення опцій:
--dry-runпоказує, які входження будуть кандидатами на виконання, але не запускає їх.--onlyфільтрує задачі за підрядком у summary події. Це дозволяє сконцентруватися, наприклад, лише наreports:daily.- Якщо прибрати
--dry-run, обрані входження будуть виконані. - Фільтрований запуск не просуває глобальний checkpoint. Це корисно, якщо ви хочете протестувати або частково відпрацювати backlog, не змінюючи загальну точку синхронізації.
Пакет також диспатчить дві події:
ScheduleCatchupCompleted— після успішних відтворень;ScheduleCatchupFailed— якщо з'явилися нові збої або вичерпані спроби.
До цих подій можна підвісити власні listener-и, наприклад для сповіщень або логування. Автор матеріалу окремо підкреслює: моніторинг планувальника (на кшталт рішень із огляду про scheduler monitoring) все одно потрібен, адже будь-яке відтворення можливе тільки після того, як сам scheduler знову почне запускатися.
Встановлення, авто-конфігурація та обмеження безпеки
Schedule Catchup вимагає PHP 8.2 або новіший і сумісний з Laravel 11, 12 та 13. Процес встановлення мінімальний:
composer require cyberwizard/schedule-catchup
php artisan migrate
Після встановлення пакетний service provider, який Laravel підхоплює автоматично, додає кілька власних задач у scheduler:
- команди catch-up та «heartbeat» кожні 5 хвилин;
- щоденне очищення історії та записів про простої.
За замовчуванням історія «осілих» (settled) записів і outage-логів зберігається 90 днів.
Якщо планувальник Laravel працює на кількох серверах, авторка статті (через посилання на інший матеріал) звертає увагу: для коректної роботи потрібне спільне кеш-сховище як для стандартних mutex-ів Laravel Scheduler, так і для блокувань, які використовує сам пакет.
Як уникнути небезпечних дублікатів роботи
Головний ризик при будь-якому відтворенні — дублювання побічних ефектів. У матеріалі наводиться приклад з email-розсилкою: якщо команда вже надіслала лист і впала до того, як пакет встиг записати успішне завершення, повторний запуск може відправити лист ще раз.
Висновок з підходу автора:
- Команди, які виконують незворотні дії (email, інтеграції з платіжними системами, зміни у зовнішніх сервісах), потрібно проектувати так, щоб вони могли безпечно повторюватися.
- Для звітів розв'язанням може бути «upsert» або оновлення запису, ключем якого є дата звіту. Тоді повторний запуск не створюватиме дублікати, а оновить наявний запис.
- Перед тим як позначити будь-яку задачу
replayOnCatchup(), варто окремо проаналізувати її побічні ефекти й перевірити, чи справді вона ідемпотентна або хоча б безпечна для повтору.
Детальніше з реалізацією й API пакету можна ознайомитися в репозиторії автора: https://github.com/cyberwizard-dev/schedule-catchup.
Джерело: Yannick Lyn Fatt — Laravel News