Laravel: Как обрабатывать длительные задания

Источник: «How to handle long-running jobs in Laravel»
С длительными заданиями в 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 — хороший подход, но мне он не нравится по двум причинам:

Делаем задание меньше

По этим причинам вместо изменения таймаута, почему бы нам не сделать задание меньше вместо этого?

Вместо отправки на выполнение одного большого задания для всех изображений в сообщении, мы можем отправлять одну задание для каждого изображения.

Задание выглядит примерно так:

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();

Теперь нам не нужно беспокоиться об истечении времени и мы можем уведомить пользователя, когда все изображения будут сохранены.

Дополнительные материалы

Предыдущая Статья

Laravel: Используем PHP Codesniffer

Следующая Статья

Laravel: Всё о контейнере внедрения зависимостей