Как это устроено:
Вместо того чтобы выполнять тяжёлый код прямо в HTTP-запросе, приложение кладёт в хранилище запись вида "выполни вот этот класс с такими параметрами". Отдельный процесс, воркер, забирает записи и выполняет их одну за другой. Хранилищем может быть таблица в базе, Redis или Amazon SQS. Для старта хватает обычной базы: начиная с Laravel 11 переменная QUEUE_CONNECTION в .env по умолчанию равна database, а миграции таблиц jobs и failed_jobs лежат в проекте из коробки, вы накатили их ещё в четвёртой главе вместе с остальными. Если у вас в .env стоит sync, задачи выполняются сразу, в том же запросе. Для отладки иногда удобно, но эффекта очереди вы не увидите.
Первая задача:
Возьмём для примера экспорт статей пользователя в CSV, операция заметно медленнее обычного запроса. Генерируем класс:
Код: Выделить всё
php artisan make:job ExportPostsReport
Код: Выделить всё
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Storage;
class ExportPostsReport implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public int $timeout = 60;
public function __construct(public User $user) {}
public function handle(): void
{
$csv = $this->user->posts()
->orderBy('created_at')
->get()
->map(fn ($post) => implode(';', [$post->id, $post->title, $post->created_at]))
->implode("\n");
Storage::put("reports/posts-{$this->user->id}.csv", $csv);
}
}
Отправляем задачу в очередь из контроллера:
Код: Выделить всё
ExportPostsReport::dispatch($request->user());
// отложить на 10 минут
ExportPostsReport::dispatch($user)->delay(now()->addMinutes(10));
// отправить в отдельную очередь
ExportPostsReport::dispatch($user)->onQueue('reports');
Запускаем воркер:
Код: Выделить всё
php artisan queue:work --tries=3 --timeout=90
# посмотреть упавшие задачи и перезапустить их
php artisan queue:failed
php artisan queue:retry all
# после каждого деплоя
php artisan queue:restart
На проде воркер должен жить вечно и подниматься после падений. Стандарт здесь Supervisor, ставится через apt install supervisor, конфиг кладут в /etc/supervisor/conf.d/laravel-worker.conf:
Код: Выделить всё
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/blog/artisan queue:work --tries=3 --max-time=3600
numprocs=2
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/blog/storage/logs/worker.log
Типичные грабли:
Первое место уверенно держит "задиспатчил, а ничего не происходит". Воркер просто не запущен, задачи копятся в jobs. Загляните в таблицу и в список процессов, прежде чем искать баг в коде.
Второе: после деплоя воркер продолжает выполнять старый код, потому что queue:work держит приложение в памяти. Лечится командой php artisan queue:restart, она через кэш просит воркеры мягко завершиться, а supervisor поднимает свежие. Добавьте её в деплой-скрипт один раз и забудьте.
Третье: сериализация моделей. В очередь уходит не вся модель, а только её id, воркер перед выполнением достаёт свежую запись из базы. Если запись к тому моменту удалили, получите ModelNotFoundException. По той же причине не передавайте в конструктор коллекции на тысячи моделей, передавайте id и выбирайте данные уже в handle().
Четвёртое: timeout должен быть меньше retry_after из config/queue.php (для database по умолчанию 90 секунд), иначе воркер решит, что зависшая задача потерялась, и она выполнится дважды. Для долгих задач поднимайте оба значения, а не только timeout.
Что усвоили:
Очередь это таблица с задачами плюс воркер, который их разгребает. Задача оформляется классом с ShouldQueue: конструктор принимает данные, handle() делает работу. На проде воркеры держит supervisor, после деплоя обязателен queue:restart, упавшие задачи живут в failed_jobs и перезапускаются через queue:retry. В следующей главе займёмся почтой, и очереди сразу пригодятся: письмо с ShouldQueue уходит в фон одной строчкой.