Урок 02

Очереди в Laravel: основы

Что даёт Laravel

Встроенная система очередей:

  • Драйверы: sync, database, redis, sqs, beanstalkd.
  • Jobs — классы задач.
  • Workers — queue:work.
  • Failed jobs — таблица.
  • Horizon — для Redis.
  • Schedule — расписание.
  • Batches — группы.
  • Chains — цепочки.

Настройка

.env:

QUEUE_CONNECTION=redis

Варианты:

  • sync — сразу (dev).
  • database — таблица.
  • redis — Redis.
  • sqs — AWS.
  • beanstalkd.

config/queue.php — параметры.

Создание Job

php artisan make:job SendWelcomeEmail

Файл app/Jobs/SendWelcomeEmail.php:

namespace App\Jobs;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class SendWelcomeEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        public User $user,
    ) {}

    public function handle(): void
    {
        Mail::to($this->user)->send(new WelcomeEmail($this->user));
    }
}

Что здесь

Трейты:

  • Dispatchable — Job::dispatch().
  • InteractsWithQueue — методы delete, release.
  • Queueable — настройки очереди.
  • SerializesModels — сериализация моделей по ID.

Интерфейс ShouldQueue — задача идёт в очередь.

handle — что делать.

Dispatch

Через класс:

SendWelcomeEmail::dispatch($user);

Через helper:

dispatch(new SendWelcomeEmail($user));

Синхронно:

SendWelcomeEmail::dispatchSync($user);

С задержкой:

SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));

В конкретную очередь:

SendWelcomeEmail::dispatch($user)->onQueue('emails');

Через соединение:

SendWelcomeEmail::dispatch($user)->onConnection('redis');

Worker

Запуск:

php artisan queue:work

Что происходит:

  1. Слушает очередь.
  2. Забирает задачу.
  3. Десериализует.
  4. Вызывает handle.
  5. Удаляет из очереди при успехе.
  6. При ошибке — failed_jobs.

Параметры:

php artisan queue:work --queue=high,default    # приоритет
php artisan queue:work --tries=3                # 3 попытки
php artisan queue:work --timeout=60             # 60 сек
php artisan queue:work --sleep=3                # пауза

--queue=high,default — сначала high, потом default.

Retry

В Job:

class SendWelcomeEmail implements ShouldQueue
{
    public int $tries = 3;
    public int $timeout = 30;
    public int $backoff = 60;

    public function handle(): void { }

    public function failed(\Throwable $e): void
    {
        Log::error('Email failed', [
            'user_id' => $this->user->id,
            'error' => $e->getMessage(),
        ]);
    }
}

Разбор:

  • $tries — сколько раз.
  • $timeout — секунд на попытку.
  • $backoff — секунд между попытками.
  • failed — что делать после всех попыток.

Массив backoff

Разные задержки:

public array $backoff = [10, 30, 60];

Попытка 1 → упала → 10 сек. Попытка 2 → упала → 30 сек. Попытка 3 → упала → failed.

Release — вернуть в очередь вручную

public function handle(): void
{
    if ($this->rateLimited()) {
        $this->release(60);   // вернуть через 60 сек
        return;
    }

    // ...
}

Что даёт: если внешний API ещё не готов — отложить.

События Job

Hooks:

// AppServiceProvider::boot()
use Illuminate\Support\Facades\Queue;

Queue::before(function (JobProcessing $event) {
    // перед началом
});

Queue::after(function (JobProcessed $event) {
    // после успеха
});

Queue::failing(function (JobFailed $event) {
    // после провала
});

В Job:

public function middleware(): array
{
    return [new RateLimited('emails')];
}

Middleware

Rate limited:

use Illuminate\Queue\Middleware\RateLimited;

public function middleware(): array
{
    return [new RateLimited('emails')];
}

В AppServiceProvider:

RateLimiter::for('emails', function ($job) {
    return Limit::perMinute(100);
});

Что даёт: не более 100 email в минуту.

Без перекрытия (WithoutOverlapping)

public function middleware(): array
{
    return [(new WithoutOverlapping($this->user->id))->dontRelease()];
}

Что даёт: не запускать вторую задачу для того же юзера.

Failed jobs

Таблица failed_jobs:

php artisan queue:failed
php artisan queue:retry 5
php artisan queue:retry all
php artisan queue:forget 5
php artisan queue:flush

Миграция:

php artisan queue:failed-table
php artisan migrate

Практика: регистрация

Controller:

public function store(StoreUserRequest $request)
{
    $user = User::create($request->validated());

    SendWelcomeEmail::dispatch($user);
    ResizeAvatar::dispatch($user);
    NotifyAdmin::dispatch($user);

    return response()->json($user, 201);
}

Jobs:

// SendWelcomeEmail
public function handle(): void
{
    Mail::to($this->user)->send(new WelcomeEmail($this->user));
}

// ResizeAvatar
public function handle(): void
{
    Image::load($this->user->avatar)->resize(200, 200)->save();
}

// NotifyAdmin
public function handle(): void
{
    Slack::send("New user: {$this->user->email}");
}

Запуск воркера:

php artisan queue:work

Проверка:

curl -X POST http://localhost:8000/api/register \
    -H 'Content-Type: application/json' \
    -d '{"name":"Иван","email":"ivan@example.com","password":"secret"}'

Ответ мгновенный, письмо — в фоне.

Частые ошибки

1. Забыли ShouldQueue:

class SendEmail
{
    // без интерфейса — выполняется синхронно
}

2. Воркер не запущен:

Jobs накапливаются в БД, не выполняются

Решение: php artisan queue:work.

3. Данные в Job не сериализуются:

public function __construct(public $callback) { }
// Closure не сериализуется

Решение: только скаляры, модели (через SerializesModels).

4. Не обрабатывать failed:

Задача упала — никто не знает

5. Забыли queue:restart после деплоя:

Воркеры работают со старым кодом

Итог

  • Job — класс задачи.
  • ShouldQueue — в очередь.
  • dispatch, dispatchSync — способы.
  • Worker — queue:work.
  • $tries, $timeout, $backoff — настройки.
  • failed — при провале.
  • failed_jobs — таблица.
  • Middleware — RateLimited, WithoutOverlapping.

Дальше — Laravel Jobs: углублённо.