Оновлення десяти мільйонів рядків (бекфіл) проходить вдало, доки через чотири години сервер не перезавантажується під час деплою. Тепер вам лишається лише гадати, які саме записи вже було оброблено. Пакет Laravel Chores від Amr Lotfy Saleh обгортає подібні одноразові операції в батчі та зберігає прогрес у базі даних після кожного кроку. Якщо процес перерветься, він відновиться з місця зупинки, а не почнеться спочатку. Концепція пакета натхненна gem-ом maintenance_tasks від Shopify для Rails.
Ось що пропонує пакет:
- Збереження прогресу через контрольні точки — ID останнього обробленого запису записується в таблицю
chore_runsпісля кожного батчу. У разі збою або Ctrl+C ви втратите прогрес максимум одного батчу. - Keyset-пагінація — батчі розбиваються за первинним ключем, а не за offset. Це дозволяє уникнути класичної помилки, коли оновлені записи «випадають» із вибірки під час ітерації.
- Ізоляція помилок — якщо запис викликає виняток, він логується в таблицю failures і пропускається, а процес триває далі.
- Жодної додаткової інфраструктури — стан зберігається у вашій базі даних. Redis, воркери черг чи сторонні сервіси не потрібні.
- Шість Artisan-команд — для створення заготовок, запуску, перегляду списку, паузи, аналізу помилок та повторних спроб.
- Дружній до CI вивід — режим виводу JSON та чіткі коди завершення:
0для успіху,1для завершення з помилками,2для критичних збоїв.
# Написання Chore
Chore — це клас із двома методами: collection() повертає запит для вибірки записів, а process() обробляє конкретний запис. Створити заготовку можна командою php artisan make:chore:
namespace App\Chores;
use AmrLotfy\Chores\Chore;
use App\Models\User;
use Illuminate\Contracts\Database\Eloquent\Builder;
class NormalizePhoneNumbers extends Chore
{
public int $batchSize = 500;
public function collection(): Builder
{
return User::whereNotNull('phone')
->where('phone', 'not like', '+%');
}
public function process($record): void
{
$record->update([
'phone' => PhoneNumber::parse($record->phone, 'EG')->toE164(),
]);
}
}
Це весь код класу. Розбиття на батчі, відстеження прогресу та логування помилок відбуваються автоматично. Якщо не вказати batchSize у класі, використовуватиметься значення за замовчуванням (500) із конфігураційного файлу.
# Запуск, пауза та відновлення
Команда chore:run запускає виконання із відображенням прогресу в терміналі в реальному часі:
php artisan chore:run NormalizePhoneNumbers
Прогрес фіксується після кожного батчу, тому та сама команда продовжить роботу, якщо її було перервано деплоєм, збоєм або Ctrl+C. Команда chore:pause зупиняє виконання на межі наступного батчу, а chore:list показує доступні Chores та історію їх запусків.
Важливий нюанс щодо гарантій: контрольна точка створюється для батчу, а не для кожного окремого запису. Це означає, що записи з батчу, який був у процесі виконання під час збою, можуть бути оброблені повторно після відновлення. Намагайтеся робити метод process() ідемпотентним — як у прикладі вище, де вже нормалізовані номери просто не підпадатимуть під умови запиту collection().
Для регулярних завдань, наприклад очищення застарілих записів, команду можна додати у планувальник задач Laravel:
$schedule->command('chore:run PurgeExpiredRecords')->monthly();
# Робота з помилками
Якщо під час обробки запису виникає виняток, виконання не зупиняється. Пакет логує запис та помилку в таблицю failures, пропускає його і рухається далі. Коли основний процес завершиться, ви зможете переглянути помилки та спробувати обробити їх знову як окрему операцію:
php artisan chore:failures NormalizePhoneNumbers
php artisan chore:retry NormalizePhoneNumbers
Такий розподіл критично важливий для тривалих задач: сотня некоректних номерів серед десяти мільйонів не повинна вбивати чотиригодинний процес. А повторна спроба для ста записів після виправлення коду коштує значно менше, ніж повний перезапуск усього масиву.
Зараз Chores виконуються у фоновому режимі з одним воркером на задачу, а колекція повинна мати первинний ключ, що піддається сортуванню (наприклад, auto-increment або ULID). Паралельне виконання через черги є в планах розробки; якщо ж вам потрібно розподілити масове оновлення між воркерами вже зараз, зверніть увагу на Queue-SQL.
# Встановлення
Laravel Chores потребує PHP 8.2+ та Laravel 12 або 13. Пакет підтримує MySQL, PostgreSQL та SQLite:
composer require amrlotfy/laravel-chores
php artisan vendor:publish --tag=chores-migrations
php artisan migrate
У файлі конфігурації можна змінити шлях до класів (типово app/Chores), стандартний розмір батчу, назви таблиць та інтервал затримки (sleep) між батчами для регулювання навантаження на базу даних.
Сирцевий код, документація та план розробки доступні в репозиторії Laravel Chores на GitHub.