Laravel: Как обрабатывать длительные задания
С длительными заданиями сложно работать, они могут:
- Быть завершёнными до того, как они закончат выполняться.
- Сложно воспроизводиться.
- Неудачными/Успешными в зависимости от ввода.
К счастью, есть способ обойти проблемы с длительными заданиями в Laravel. Давайте рассмотрим несколько решений (последнее — хорошее, продолжайте читать).
Задание
Для этой статьи давайте воспользуемся примером из задания, которое загружает все изображения из статьи блога в S3 bucket.
Вот задание о которой идёт речь:
class StorePostImages implements ShouldQueue
{
public function __construct(public Post $post, public User $owner)
{
}
public function handle()
{
foreach ($this->post->images as $image) {
$content = file_get_content($image->url);
Storage::disk('s3')->put(
"images/{$this->post->id}/{$this->image->filename}",
$content
);
}
$owner->notify(new PostImagesStored($this->post));
}
}
Мы бы отправили это задание (например, из контроллера) следующим образом:
StorePostImages::dispatch($post, $request->user());
Это задание делает две вещи. Сохраняет изображения в S3 и после успешного сохранения всех изображений уведомляет владельца.
В зависимости от количества изображений это может занять много времени.
По умолчанию Laravel убивает задание через 60 секунд.
Поскольку это может занять больше времени, один из подходов — изменить время ожидания задания.
Изменение времени ожидания
Для изменения времени ожидания задания, можно перезаписать свойство $timeout
в классе задания.
class StorePostImages implements ShouldQueue
{
//👇 Делаем timeout больше
public $timeout = 120;
public function __construct(public Post $post, public User $owner)
{
}
public function handle()
{
foreach ($this->post->images as $image) {
$content = file_get_contents($image->url);
Storage::disk('s3')->put(
"images/{$this->post->id}/{$this->image->filename}",
$content
);
}
$owner->notify(new PostImagesStored($this->post));
}
}
Не забывайте, что также необходимо изменить свойство retry_after
в конфигурации очередей, во избежание дублирования заданий.
// config/queue.php
//...
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 130, /// 👈 Должно быть больше timeout'а
'block_for' => null,
'after_commit' => false,
],
//...
Laravel использует $timeout
для определения сколько времени потребуется рабочему процессу для выполнения задания. По истечению заданного времени Laravel убивает этот рабочий процесс.
retry_after
относится к времени, когда задание будет повторено. Не имеет значения, обрабатывает ли рабочий процесс задание или нет, если задание не помечено как завершённое, Laravel освободит это задание для повторной обработки другим рабочим процессом. Это может привести к тому, что задание будет выполняться два раза вместо одного.
Изменение $timeout
— хороший подход, но мне он не нравится по двум причинам:
- Нужно изменять конфигурацию очереди, что может повлиять на другие задания.
- Что делать если задание выполняется боле 120 секунд? Что если в будущем мы наткнёмся на сообщение с очень большим количеством изображений?
Делаем задание меньше
По этим причинам вместо изменения таймаута, почему бы нам не сделать задание меньше вместо этого?
Вместо отправки на выполнение одного большого задания для всех изображений в сообщении, мы можем отправлять одну задание для каждого изображения.
Задание выглядит примерно так:
class StoreImage implements ShouldQueue
{
public function __construct(public Image $image, public $postId)
{
}
public function handle()
{
// This is fast 👌 ⚡
$content = file_get_contents($this->image->url);
Storage::disk('s3')->put("images/{$this->postId}/", $content);
}
}
Это задание берёт одно изображение и сохраняет его в S3. Выполнение занимает не так много времени.
Мы бы добавили несколько заданий в очередь для каждого сообщения, по одному для каждого изображения:
foreach ($post->images as $image) {
StoreImage::dispatch($image, $post->id);
}
Отлично. Теперь мы можем без проблем обрабатывать все изображения. Но как уведомить пользователя, что все изображения были корректно сохранены? Это вообще возможно?
Пакетные задания
Да, к счастью, в Laravel есть замечательная функция, называемая пакетирование заданий.
С помощью пакетной обработки заданий мы можем зарегистрировать обратный вызов выполняемый после успешного завершения каждого задания.
Нужно передать массив заданий в Bus::batch
и обратный вызов.
$jobs = [];
foreach ($post->images as $image) {
$jobs[] = StoreImage::dispatch($image, $post->id);
}
// Пакетирование заданий 🥳
Bus::batch($jobs)->then(function (Batch $batch) use ($user, $post) {
$user->notify(new PostImagesStored($post));
// 👆 выполнится после удачно завершения всех заданий
})->dispatch();
Теперь нам не нужно беспокоиться об истечении времени и мы можем уведомить пользователя, когда все изображения будут сохранены.