Черги
- Вступ
- Створення завдань
- Job Middleware
- Відправка завдань
- Пакетна обробка завдань
- Черги Замикань
- Запуск обробника черги
- Конфігурація Supervisor
- Робота з невдалими завданнями
- Очищення завдань з черг
- Моніторинг Ваших Черг
- Тестування
- Події Роботи
Вступ
Під час створення вашого веб-застосунку, у вас можуть бути деякі завдання, такі як розбір і збереження завантаженого CSV-файлу, які займають занадто багато часу для виконання під час типового веб-запиту. На щастя, Laravel дозволяє легко створювати завдання в черзі, які можуть оброблятися у фоновому режимі. Переміщуючи ресурсоємні завдання в чергу, ваш застосунок може відповідати на веб-запити з блискавичною швидкістю і забезпечувати кращий користувацький досвід для ваших клієнтів.
Черги Laravel надають уніфікований API для роботи з чергами через різні бекенди черг, такі як Amazon SQS, Redis або навіть реляційна база даних.
Конфігураційні параметри черги Laravel зберігаються у файлі конфігурації config/queue.php вашого застосунку. У цьому файлі ви знайдете конфігурації з'єднань для кожного з драйверів черги, які включені у фреймворк, включаючи драйвери бази даних, Amazon SQS, Redis та Beanstalkd, а також синхронний драйвер, який буде виконувати завдання негайно (для використання під час локальної розробки). Також включено драйвер черги null, який відкидає завдання в черзі.
Laravel тепер пропонує Horizon, чудову панель управління та систему конфігурації для ваших черг на базі Redis. Ознайомтеся з повною документацією Horizon для отримання додаткової інформації.
З'єднання проти Черг
Перш ніж почати працювати з чергами Laravel, важливо зрозуміти різницю між "з'єднаннями" та "чергами". У вашому конфігураційному файлі config/queue.php є конфігураційний масив connections. Цей параметр визначає з'єднання з бекенд-сервісами черг, такими як Amazon SQS, Beanstalk або Redis. Однак будь-яке задане з'єднання черги може мати декілька "черг", які можна розглядати як різні стеки або купи завдань у черзі.
Зверніть увагу, що кожен приклад конфігурації з'єднання у файлі конфігурації queue містить атрибут queue. Це черга за замовчуванням, до якої завдання будуть відправлені, коли вони надсилаються до даного з'єднання. Іншими словами, якщо ви відправляєте завдання без явного визначення, до якої черги воно має бути відправлене, завдання буде розміщено в черзі, яка визначена в атрибуті queue конфігурації з'єднання:
use App\Jobs\ProcessPodcast;
// Ця задача відправляється до черги за замовчуванням з'єднання за замовчуванням...
ProcessPodcast::dispatch();
// Ця робота відправляється в чергу "emails" за замовчуванням підключення...
ProcessPodcast::dispatch()->onQueue('emails');
Деякі застосунки можуть ніколи не потребувати відправляти завдання в декілька черг, натомість віддаючи перевагу одній простій черзі. Однак, відправка завдань у декілька черг може бути особливо корисною для застосунків, які бажають пріоритизувати або сегментувати обробку завдань, оскільки Laravel queue worker дозволяє вам вказати, які черги він повинен обробляти за пріоритетом. Наприклад, якщо ви відправляєте завдання в чергу high, ви можете запустити worker, який надає їм вищий пріоритет обробки:
php artisan queue:work --queue=high,default
Примітки та передумови для драйвера
База даних
Щоб використовувати драйвер черги database, вам потрібна таблиця бази даних для зберігання завдань. Зазвичай, це включено в стандартну 0001_01_01_000002_create_jobs_table.php міграцію бази даних Laravel; однак, якщо ваш застосунок не містить цієї міграції, ви можете скористатися командою Artisan make:queue-table, щоб створити її:
php artisan make:queue-table
php artisan migrate
Redis
Щоб використовувати драйвер черги redis, вам слід налаштувати підключення до бази даних Redis у вашому конфігураційному файлі config/database.php.
Опції Redis serializer та compression не підтримуються драйвером черги redis.
Redis Cluster
Якщо ваше з'єднання черги Redis використовує Redis Cluster, імена ваших черг повинні містити хеш-тег ключа. Це необхідно для того, щоб усі ключі Redis для даної черги були розміщені в одному хеш-слоті:
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', '{default}'),
'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
Блокування
Коли ви використовуєте чергу Redis, ви можете використовувати опцію конфігурації block_for, щоб вказати, як довго драйвер повинен чекати, поки завдання стане доступним, перш ніж перейти до наступної ітерації циклу обробника та повторно опитати базу даних Redis.
Налаштування цього значення відповідно до навантаження на вашу чергу може бути більш ефективним, ніж постійне опитування бази даних Redis для нових завдань. Наприклад, ви можете встановити значення 5, щоб вказати, що драйвер повинен блокуватися на п'ять секунд, очікуючи, поки завдання стане доступним:
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => 5,
'after_commit' => false,
],
Встановлення block_for на 0 змусить обробників черги блокуватися на невизначений час, поки не стане доступним завдання. Це також запобігатиме обробці сигналів, таких як SIGTERM, до обробки наступного завдання.
Інші вимоги до драйвера
Наступні залежності потрібні для вказаних драйверів черги. Ці залежності можуть бути встановлені через менеджер пакетів Composer:
- Amazon SQS:
aws/aws-sdk-php ~3.0 - Beanstalkd:
pda/pheanstalk ~5.0 - Redis:
predis/predis ~2.0або розширення PHP phpredis - MongoDB:
mongodb/laravel-mongodb
Створення Завдань
Генерація Класів Завдань
За замовчуванням всі чергові завдання для вашого застосунку зберігаються в директорії app/Jobs. Якщо директорія app/Jobs не існує, вона буде створена, коли ви виконаєте команду Artisan make:job:
php artisan make:job ProcessPodcast
Згенерований клас реалізує інтерфейс Illuminate\Contracts\Queue\ShouldQueue, вказуючи Laravel, що завдання слід помістити в чергу для асинхронного виконання.
Шаблони завдань можуть бути налаштовані за допомогою публікації шаблонів.
Структура Класу
Класи завдань дуже прості, зазвичай містять лише метод handle, який викликається, коли завдання обробляється чергою. Щоб почати, давайте розглянемо приклад класу завдання. У цьому прикладі ми уявимо, що керуємо сервісом публікації подкастів і нам потрібно обробити завантажені файли подкастів перед їх публікацією:
<?php
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Створити новий екземпляр завдання.
*/
public function __construct(
public Podcast $podcast,
) {}
/**
* Виконати завдання.
*/
public function handle(AudioProcessor $processor): void
{
// Обробка завантаженого подкасту...
}
}
У цьому прикладі зверніть увагу, що ми змогли передати модель Eloquent безпосередньо в конструктор завдання в черзі. Завдяки трейту Queueable, який використовує завдання, моделі Eloquent та їх завантажені відносини будуть коректно серіалізовані та десеріалізовані під час обробки завдання.
Якщо ваша поставлена в чергу задача приймає модель Eloquent у своєму конструкторі, лише ідентифікатор моделі буде серіалізовано в чергу. Коли задача фактично обробляється, система черги автоматично повторно отримує повну екземпляр моделі та її завантажені відносини з бази даних. Такий підхід до серіалізації моделей дозволяє надсилати набагато менші навантаження задач до вашого драйвера черги.
handle Метод Ін'єкції Залежностей
Метод handle викликається, коли завдання обробляється чергою. Зверніть увагу, що ми можемо використовувати типізацію залежностей у методі handle завдання. Сервіс-контейнер Laravel автоматично впроваджує ці залежності.
Якщо ви хочете повністю контролювати, як контейнер впроваджує залежності в метод handle, ви можете використовувати метод контейнера bindMethod. Метод bindMethod приймає зворотний виклик, який отримує завдання та контейнер. У межах зворотного виклику ви можете викликати метод handle так, як вам заманеться. Зазвичай, ви повинні викликати цей метод з методу boot вашого App\Providers\AppServiceProvider сервіс-провайдера:
use App\Jobs\ProcessPodcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Foundation\Application;
$this->app->bindMethod([ProcessPodcast::class, 'handle'], function (ProcessPodcast $job, Application $app) {
return $job->handle($app->make(AudioProcessor::class));
});
Бінарні дані, такі як вміст необроблених зображень, слід пропускати через функцію base64_encode перед передачею в чергу завдань. В іншому випадку завдання може не коректно серіалізуватися в JSON при розміщенні в черзі.
Черга Відносин
Оскільки всі завантажені відносини моделі Eloquent також серіалізуються, коли завдання ставиться в чергу, серіалізований рядок завдання іноді може стати досить великим. Крім того, коли завдання десеріалізується і відносини моделі повторно отримуються з бази даних, вони будуть отримані в повному обсязі. Будь-які попередні обмеження відносин, які були застосовані до серіалізації моделі під час процесу постановки завдання в чергу, не будуть застосовані, коли завдання буде десеріалізовано. Тому, якщо ви бажаєте працювати з підмножиною даної відносини, вам слід повторно обмежити ці відносини у вашому завданні в черзі.
Або, щоб запобігти серіалізації відносин, ви можете викликати метод withoutRelations на моделі при встановленні значення властивості. Цей метод поверне екземпляр моделі без завантажених відносин:
/**
* Створити новий екземпляр завдання.
*/
public function __construct(
Podcast $podcast,
) {
$this->podcast = $podcast->withoutRelations();
}
Якщо ви використовуєте просування властивостей конструктора PHP і хочете вказати, що модель Eloquent не повинна мати серіалізованих відносин, ви можете використовувати атрибут WithoutRelations:
use Illuminate\Queue\Attributes\WithoutRelations;
/**
* Створити новий екземпляр завдання.
*/
public function __construct(
#[WithoutRelations]
public Podcast $podcast,
) {}
Якщо завдання отримує колекцію або масив моделей Eloquent замість однієї моделі, моделі в цій колекції не матимуть відновлених зв'язків, коли завдання буде десеріалізовано та виконано. Це робиться для запобігання надмірному використанню ресурсів у завданнях, які працюють з великою кількістю моделей.
Унікальні Завдання
Унікальні завдання вимагають драйвера кешу, який підтримує блокування. На даний момент драйвери кешу memcached, redis, dynamodb, database, file та array підтримують атомарні блокування. Крім того, обмеження унікальних завдань не застосовуються до завдань у пакетах.
Іноді ви можете захотіти переконатися, що в черзі в будь-який момент часу знаходиться лише один екземпляр конкретної задачі. Ви можете зробити це, реалізувавши інтерфейс ShouldBeUnique у вашому класі задачі. Цей інтерфейс не вимагає від вас визначення будь-яких додаткових методів у вашому класі:
<?php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
// ...
}
У наведеному вище прикладі завдання UpdateSearchIndex є унікальним. Отже, завдання не буде відправлено, якщо інший екземпляр цього завдання вже знаходиться в черзі і ще не завершив обробку.
У деяких випадках ви можете захотіти визначити конкретний "ключ", який робить завдання унікальним, або ви можете захотіти вказати тайм-аут, після якого завдання більше не залишається унікальним. Щоб досягти цього, ви можете визначити властивості або методи uniqueId і uniqueFor у вашому класі завдання:
<?php
use App\Models\Product;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
/**
* Екземпляр продукту.
*
* @var \App\Product
*/
public $product;
/**
* Кількість секунд, після яких унікальне блокування завдання буде знято.
*
* @var int
*/
public $uniqueFor = 3600;
/**
* Отримати унікальний ID для завдання.
*/
public function uniqueId(): string
{
return $this->product->id;
}
}
У наведеному вище прикладі завдання UpdateSearchIndex є унікальним за ідентифікатором продукту. Отже, будь-які нові відправлення завдання з тим самим ідентифікатором продукту будуть ігноруватися, доки існуюче завдання не завершить обробку. Крім того, якщо існуюче завдання не буде оброблено протягом однієї години, унікальне блокування буде знято, і інше завдання з тим самим унікальним ключем може бути відправлено в чергу.
Якщо ваш застосунок відправляє завдання з декількох веб-серверів або контейнерів, ви повинні переконатися, що всі ваші сервери спілкуються з одним і тим же центральним сервером кешу, щоб Laravel міг точно визначити, чи є завдання унікальним.
Забезпечення унікальності завдань до початку обробки
За замовчуванням унікальні завдання "розблоковуються" після завершення обробки завдання або після невдачі всіх спроб повтору. Однак можуть бути ситуації, коли ви хочете, щоб ваше завдання розблокувалося відразу перед його обробкою. Щоб досягти цього, ваше завдання повинно реалізувати контракт ShouldBeUniqueUntilProcessing замість контракту ShouldBeUnique:
<?php
use App\Models\Product;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
// ...
}
Унікальні блокування завдань
За лаштунками, коли завдання ShouldBeUnique відправляється, Laravel намагається отримати блокування з ключем uniqueId. Якщо блокування не отримано, завдання не відправляється. Це блокування звільняється, коли завдання завершує обробку або зазнає невдачі у всіх спробах повтору. За замовчуванням, Laravel використовуватиме драйвер кешу за замовчуванням для отримання цього блокування. Однак, якщо ви бажаєте використовувати інший драйвер для отримання блокування, ви можете визначити метод uniqueVia, який повертає драйвер кешу, що має бути використаний:
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
// ...
/**
* Отримати драйвер кешу для унікального блокування завдання.
*/
public function uniqueVia(): Repository
{
return Cache::driver('redis');
}
}
Якщо вам потрібно лише обмежити одночасну обробку завдання, використовуйте WithoutOverlapping middleware для завдань.
Зашифровані Завдання
Laravel дозволяє забезпечити конфіденційність та цілісність даних завдання через шифрування. Щоб почати, просто додайте інтерфейс ShouldBeEncrypted до класу завдання. Після того, як цей інтерфейс буде додано до класу, Laravel автоматично зашифрує ваше завдання перед тим, як відправити його в чергу:
<?php
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
class UpdateSearchIndex implements ShouldQueue, ShouldBeEncrypted
{
// ...
}
Job Middleware
Job middleware дозволяє вам обгорнути виконання поставлених у чергу завдань власною логікою, зменшуючи шаблонний код у самих завданнях. Наприклад, розгляньте наступний метод handle, який використовує функції обмеження частоти запитів Redis у Laravel, щоб дозволити обробляти лише одне завдання кожні п'ять секунд:
use Illuminate\Support\Facades\Redis;
/**
* Виконати завдання.
*/
public function handle(): void
{
Redis::throttle('key')->block(0)->allow(1)->every(5)->then(function () {
info('Lock obtained...');
// Обробити завдання...
}, function () {
// Не вдалося отримати блокування...
return $this->release(5);
});
}
Хоча цей код є дійсним, реалізація методу handle стає шумною, оскільки вона захаращена логікою обмеження частоти запитів Redis. Крім того, цю логіку обмеження частоти запитів потрібно дублювати для будь-яких інших завдань, які ми хочемо обмежити.
Замість обмеження частоти запитів у методі handle, ми можемо визначити middleware для завдань, яке обробляє обмеження частоти запитів. Laravel не має стандартного місця для middleware завдань, тому ви можете розмістити middleware завдань у будь-якому місці вашого застосунку. У цьому прикладі ми розмістимо middleware у директорії app/Jobs/Middleware:
<?php
namespace App\Jobs\Middleware;
use Closure;
use Illuminate\Support\Facades\Redis;
class RateLimited
{
/**
* Обробити поставлену в чергу задачу.
*
* @param \Closure(object): void $next
*/
public function handle(object $job, Closure $next): void
{
Redis::throttle('key')
->block(0)->allow(1)->every(5)
->then(function () use ($job, $next) {
// Блокування отримано...
$next($job);
}, function () use ($job) {
// Не вдалося отримати блокування...
$job->release(5);
});
}
}
Як ви можете бачити, як і маршрутне middleware, job middleware отримує завдання, яке обробляється, та зворотний виклик, який слід викликати для продовження обробки завдання.
Ви можете згенерувати новий клас job middleware за допомогою команди Artisan make:job-middleware. Після створення job middleware, їх можна прикріпити до завдання, повертаючи їх з методу middleware завдання. Цей метод не існує в завданнях, створених за допомогою команди Artisan make:job, тому вам потрібно буде вручну додати його до вашого класу завдання:
use App\Jobs\Middleware\RateLimited; /** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [new RateLimited]; }
Можна також призначити middleware для слухачів подій у черзі, поштових повідомлень та сповіщень.
Обмеження частоти запитів
Хоча ми щойно продемонстрували, як написати власне middleware для обмеження частоти запитів до завдань, Laravel насправді включає middleware для обмеження частоти запитів, яке ви можете використовувати для обмеження частоти запитів до завдань. Як і обмежувачі частоти запитів для маршрутів, обмежувачі частоти запитів для завдань визначаються за допомогою методу for фасаду RateLimiter.
Наприклад, ви можете дозволити користувачам робити резервне копіювання їхніх даних раз на годину, не накладаючи такого обмеження на преміум-клієнтів. Щоб досягти цього, ви можете визначити RateLimiter у методі boot вашого AppServiceProvider:
use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Support\Facades\RateLimiter; /** * Ініціалізувати будь-які сервіси застосунку. */ public function boot(): void { RateLimiter::for('backups', function (object $job) { return $job->user->vipCustomer() ? Limit::none() : Limit::perHour(1)->by($job->user->id); }); }
У наведеному вище прикладі ми визначили обмеження швидкості на годину; однак, ви можете легко визначити обмеження швидкості на основі хвилин, використовуючи метод perMinute. Крім того, ви можете передати будь-яке значення, яке ви бажаєте, до методу by обмеження швидкості; однак, це значення найчастіше використовується для сегментації обмежень швидкості за клієнтом:
return Limit::perMinute(50)->by($job->user->id);
Як тільки ви визначили свій ліміт швидкості, ви можете прикріпити обмежувач швидкості до вашої задачі, використовуючи Illuminate\Queue\Middleware\RateLimited middleware. Кожного разу, коли задача перевищує ліміт швидкості, це middleware поверне задачу назад у чергу з відповідною затримкою, заснованою на тривалості ліміту швидкості.
use Illuminate\Queue\Middleware\RateLimited; /** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [new RateLimited('backups')]; }
Повернення роботи з обмеженою швидкістю назад у чергу все одно збільшить загальну кількість attempts для роботи. Можливо, ви захочете налаштувати властивості tries і maxExceptions у вашому класі роботи відповідно. Або ви можете скористатися методом retryUntil, щоб визначити кількість часу, протягом якого робота не повинна більше виконуватись.
Використовуючи метод releaseAfter, ви також можете вказати кількість секунд, які повинні пройти, перш ніж звільнена задача буде спробувана знову:
/** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [(new RateLimited('backups'))->releaseAfter(60)]; }
Якщо ви не хочете, щоб завдання повторювалося, коли воно обмежене за швидкістю, ви можете використовувати метод dontRelease:
/** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [(new RateLimited('backups'))->dontRelease()]; }
Якщо ви використовуєте Redis, ви можете використовувати Illuminate\Queue\Middleware\RateLimitedWithRedis middleware, яке налаштоване для Redis і є більш ефективним, ніж базове middleware для обмеження частоти запитів.
Запобігання перекриттю завдань
Laravel включає Illuminate\Queue\Middleware\WithoutOverlapping middleware, яке дозволяє запобігти перекриттю завдань на основі довільного ключа. Це може бути корисним, коли завдання в черзі змінює ресурс, який повинен змінюватися лише одним завданням одночасно.
Наприклад, уявімо, що у вас є завдання в черзі, яке оновлює кредитний рейтинг користувача, і ви хочете запобігти накладанню завдань оновлення кредитного рейтингу для того ж самого ID користувача. Щоб досягти цього, ви можете повернути WithoutOverlapping middleware з методу middleware вашого завдання:
use Illuminate\Queue\Middleware\WithoutOverlapping; /** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [new WithoutOverlapping($this->user->id)]; }
Будь-які перекриваючі завдання одного типу будуть повернені назад у чергу. Ви також можете вказати кількість секунд, які повинні пройти, перш ніж повернене завдання буде спробувано знову:
/** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [(new WithoutOverlapping($this->order->id))->releaseAfter(60)]; }
Якщо ви бажаєте негайно видалити будь-які перекриваючі завдання, щоб вони не були повторно виконані, ви можете використовувати метод dontRelease:
/** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [(new WithoutOverlapping($this->order->id))->dontRelease()]; }
WithoutOverlapping middleware працює на основі атомарного блокування Laravel. Іноді ваша задача може несподівано зазнати невдачі або перевищити час очікування таким чином, що блокування не буде знято. Тому ви можете явно визначити час закінчення блокування, використовуючи метод expireAfter. Наприклад, приклад нижче вкаже Laravel зняти блокування WithoutOverlapping через три хвилини після початку обробки задачі:
/** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [(new WithoutOverlapping($this->order->id))->expireAfter(180)]; }
Мiddleware WithoutOverlapping вимагає драйвер кешу, який підтримує блокування. На даний момент драйвери кешу memcached, redis, dynamodb, database, file та array підтримують атомарні блокування.
Спільне Використання Ключів Блокування Між Класами Завдань
За замовчуванням, WithoutOverlapping middleware буде запобігати перекриттю лише завдань одного класу. Отже, хоча два різні класи завдань можуть використовувати один і той самий ключ блокування, вони не будуть запобігати перекриттю. Однак, ви можете вказати Laravel застосовувати ключ для різних класів завдань, використовуючи метод shared:
use Illuminate\Queue\Middleware\WithoutOverlapping;
class ProviderIsDown
{
// ...
public function middleware(): array
{
return [
(new WithoutOverlapping("status:{$this->provider}"))->shared(),
];
}
}
class ProviderIsUp
{
// ...
public function middleware(): array
{
return [
(new WithoutOverlapping("status:{$this->provider}"))->shared(),
];
}
}
Обмеження Винятків
Laravel включає Illuminate\Queue\Middleware\ThrottlesExceptions middleware, яке дозволяє обмежувати виключення. Як тільки завдання викликає задану кількість виключень, всі подальші спроби виконати завдання затримуються до закінчення вказаного інтервалу часу. Це middleware особливо корисне для завдань, які взаємодіють з нестабільними сторонніми сервісами.
Наприклад, уявімо чергову задачу, яка взаємодіє з API третьої сторони, що починає викидати виключення. Щоб обмежити виключення, ви можете повернути ThrottlesExceptions middleware з методу middleware вашої задачі. Зазвичай, це middleware слід поєднувати із задачею, яка реалізує спроби на основі часу:
use DateTime; use Illuminate\Queue\Middleware\ThrottlesExceptions; /** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [new ThrottlesExceptions(10, 5 * 60)]; } /** * Визначити час, після якого завдання має завершитися через тайм-аут. */ public function retryUntil(): DateTime { return now()->addMinutes(30); }
Перший аргумент конструктора, прийнятий middleware, це кількість виключень, які завдання може викинути перед тим, як буде обмежено, тоді як другий аргумент конструктора — це кількість секунд, які повинні пройти перед тим, як завдання буде знову спробувано після того, як його було обмежено. У наведеному вище прикладі коду, якщо завдання викидає 10 послідовних виключень, ми чекатимемо 5 хвилин перед тим, як знову спробувати виконати завдання, обмежене 30-хвилинним лімітом часу.
Коли завдання викликає виняток, але поріг винятків ще не досягнуто, завдання зазвичай буде повторно виконано негайно. Однак, ви можете вказати кількість хвилин, на які таке завдання має бути відкладено, викликавши метод backoff при приєднанні middleware до завдання:
use Illuminate\Queue\Middleware\ThrottlesExceptions; /** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [(new ThrottlesExceptions(10, 5 * 60))->backoff(5)]; }
Внутрішньо це middleware використовує систему кешування Laravel для реалізації обмеження частоти запитів, і ім'я класу завдання використовується як "ключ" кешу. Ви можете перевизначити цей ключ, викликавши метод by при приєднанні middleware до вашого завдання. Це може бути корисним, якщо у вас є декілька завдань, які взаємодіють з однією і тією ж сторонньою службою, і ви хочете, щоб вони ділили спільний "кошик" обмеження:
use Illuminate\Queue\Middleware\ThrottlesExceptions; /** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [(new ThrottlesExceptions(10, 10 * 60))->by('key')]; }
За замовчуванням, це middleware буде обмежувати кожне виключення. Ви можете змінити цю поведінку, викликавши метод when при приєднанні middleware до вашої задачі. Виключення буде обмежено лише в тому випадку, якщо замикання, надане методу when, поверне true:
use Illuminate\Http\Client\HttpClientException; use Illuminate\Queue\Middleware\ThrottlesExceptions; /** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [(new ThrottlesExceptions(10, 10 * 60))->when( fn (Throwable $throwable) => $throwable instanceof HttpClientException )]; }
На відміну від методу when, який повертає завдання назад у чергу або викидає виняток, метод deleteWhen дозволяє повністю видалити завдання, коли виникає певний виняток:
use App\Exceptions\CustomerDeletedException; use Illuminate\Queue\Middleware\ThrottlesExceptions; /** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [(new ThrottlesExceptions(2, 10 * 60))->deleteWhen(CustomerDeletedException::class)]; }
Якщо ви хочете, щоб обмежені виключення повідомлялися обробнику виключень вашого застосунку, ви можете зробити це, викликавши метод report при приєднанні middleware до вашої задачі. За бажанням, ви можете надати замикання методу report, і виключення буде повідомлено лише в тому випадку, якщо дане замикання поверне true:
use Illuminate\Http\Client\HttpClientException; use Illuminate\Queue\Middleware\ThrottlesExceptions; /** * Отримати middleware, через яке має пройти завдання. * * @return array<int, object> */ public function middleware(): array { return [(new ThrottlesExceptions(10, 10 * 60))->report( fn (Throwable $throwable) => $throwable instanceof HttpClientException )]; }
Якщо ви використовуєте Redis, ви можете використовувати Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis middleware, яке оптимізоване для Redis і є більш ефективним, ніж базове middleware для обмеження винятків.
Пропуск Завдань
Skip middleware дозволяє вказати, що завдання слід пропустити / видалити без необхідності змінювати логіку завдання. Метод Skip::when видалить завдання, якщо задана умова оцінюється як true, тоді як метод Skip::unless видалить завдання, якщо умова оцінюється як false:
use Illuminate\Queue\Middleware\Skip; /** * Отримати middleware, через яке має пройти завдання. */ public function middleware(): array { return [ Skip::when($someCondition), ]; }
Ви також можете передати Closure до методів when та unless для більш складної умовної оцінки:
use Illuminate\Queue\Middleware\Skip; /** * Отримати middleware, через яке має пройти завдання. */ public function middleware(): array { return [ Skip::when(function (): bool { return $this->shouldSkip(); }), ]; }
Відправка Завдань
Після того як ви написали клас завдання, ви можете відправити його, використовуючи метод dispatch на самому завданні. Аргументи, передані методу dispatch, будуть передані конструктору завдання:
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Зберегти новий подкаст.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// ...
ProcessPodcast::dispatch($podcast);
return redirect('/podcasts');
}
}
Якщо ви хочете умовно відправити завдання, ви можете використовувати методи dispatchIf та dispatchUnless:
ProcessPodcast::dispatchIf($accountActive, $podcast);
ProcessPodcast::dispatchUnless($accountSuspended, $podcast);
У нових Laravel-застосунках драйвером черги за замовчуванням є драйвер sync. Цей драйвер виконує завдання синхронно у фоновому режимі поточного запиту, що часто зручно під час локальної розробки. Якщо ви хочете почати ставити завдання в чергу для обробки у фоновому режимі, ви можете вказати інший драйвер черги у файлі конфігурації вашого застосунку config/queue.php.
Відкладене Відправлення
Якщо ви хочете вказати, що завдання не повинно бути одразу доступним для обробки обробником черги, ви можете використовувати метод delay при відправці завдання. Наприклад, давайте вкажемо, що завдання не повинно бути доступним для обробки до 10 хвилин після його відправки:
<?php namespace App\Http\Controllers; use App\Jobs\ProcessPodcast; use App\Models\Podcast; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; class PodcastController extends Controller { /** * Зберегти новий подкаст. */ public function store(Request $request): RedirectResponse { $podcast = Podcast::create(/* ... */); // ... ProcessPodcast::dispatch($podcast) ->delay(now()->addMinutes(10)); return redirect('/podcasts'); } }
У деяких випадках для завдань може бути налаштована затримка за замовчуванням. Якщо вам потрібно обійти цю затримку і відправити завдання для негайної обробки, ви можете використовувати метод withoutDelay:
ProcessPodcast::dispatch($podcast)->withoutDelay();
Сервіс черг Amazon SQS має максимальний час затримки 15 хвилин.
Відправка після того, як відповідь надіслана до браузера
Альтернативно, метод dispatchAfterResponse затримує відправку завдання до моменту, коли HTTP-відповідь буде надіслана до браузера користувача, якщо ваш веб-сервер використовує FastCGI. Це все ще дозволить користувачу почати використовувати застосунок, навіть якщо завдання в черзі все ще виконується. Це зазвичай слід використовувати лише для завдань, які займають близько секунди, таких як відправка електронної пошти. Оскільки вони обробляються в межах поточного HTTP-запиту, завдання, відправлені таким чином, не потребують, щоб обробник черги був запущений для їх обробки:
use App\Jobs\SendNotification;
SendNotification::dispatchAfterResponse();
Ви також можете dispatch замикання і приєднати метод afterResponse до хелпера dispatch, щоб виконати замикання після того, як HTTP-відповідь була відправлена до браузера:
use App\Mail\WelcomeMessage;
use Illuminate\Support\Facades\Mail;
dispatch(function () {
Mail::to('example@example.com')->send(new WelcomeMessage);
})->afterResponse();
Синхронна Відправка
Якщо ви хочете виконати завдання негайно (синхронно), ви можете використовувати метод dispatchSync. При використанні цього методу завдання не буде поставлено в чергу і буде виконано негайно в поточному процесі:
<?php namespace App\Http\Controllers; use App\Jobs\ProcessPodcast; use App\Models\Podcast; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; class PodcastController extends Controller { /** * Зберегти новий подкаст. */ public function store(Request $request): RedirectResponse { $podcast = Podcast::create(/* ... */); // Створити подкаст... ProcessPodcast::dispatchSync($podcast); return redirect('/podcasts'); } }
Завдання & Транзакції Бази Даних
Хоча цілком допустимо відправляти завдання в межах транзакцій бази даних, слід особливо подбати про те, щоб ваше завдання дійсно могло виконатися успішно. При відправленні завдання в межах транзакції можливо, що завдання буде оброблено обробником до того, як батьківська транзакція буде зафіксована. Коли це відбувається, будь-які оновлення, які ви зробили в моделях або записах бази даних під час транзакції(й) бази даних, можуть ще не відображатися в базі даних. Крім того, будь-які моделі або записи бази даних, створені в межах транзакції(й), можуть не існувати в базі даних.
На щастя, Laravel надає кілька методів для вирішення цієї проблеми. По-перше, ви можете встановити опцію з'єднання after_commit у конфігураційному масиві вашого з'єднання черги:
'redis' => [
'driver' => 'redis',
// ...
'after_commit' => true,
],
Коли параметр after_commit встановлено в true, ви можете відправляти завдання в межах транзакцій бази даних; однак Laravel чекатиме, поки відкриті батьківські транзакції бази даних не будуть зафіксовані, перш ніж фактично відправити завдання. Звісно, якщо жодні транзакції бази даних наразі не відкриті, завдання буде відправлено негайно.
Якщо транзакція відкотиться через виняток, що виник під час транзакції, завдання, які були відправлені під час цієї транзакції, будуть відхилені.
Встановлення параметра конфігурації after_commit в true також призведе до того, що всі поставлені в чергу слухачі подій, поштові повідомлення, сповіщення та події трансляції будуть відправлені після завершення всіх відкритих транзакцій бази даних.
Безпосереднє вказання поведінки надсилання під час фіксації транзакцій БД
Якщо ви не встановите параметр конфігурації з'єднання черги after_commit в true, ви все ще можете вказати, що конкретне завдання має бути відправлене після того, як всі відкриті транзакції бази даних будуть зафіксовані. Щоб досягти цього, ви можете приєднати метод afterCommit до вашої операції відправки:
use App\Jobs\ProcessPodcast;
ProcessPodcast::dispatch($podcast)->afterCommit();
Аналогічно, якщо параметр конфігурації after_commit встановлено на true, ви можете вказати, що певне завдання має бути відправлено негайно, не чекаючи завершення будь-яких відкритих транзакцій бази даних:
ProcessPodcast::dispatch($podcast)->beforeCommit();
Послідовне виконання завдань
Зв'язування завдань дозволяє вам вказати список завдань у черзі, які повинні виконуватися послідовно після успішного виконання основного завдання. Якщо одне завдання в послідовності зазнає невдачі, решта завдань не буде виконано. Щоб виконати ланцюг завдань у черзі, ви можете використовувати метод chain, наданий фасадом Bus. Командний автобус Laravel є нижчим рівнем компонента, на якому побудовано диспетчеризацію завдань у черзі:
use App\Jobs\OptimizePodcast;
use App\Jobs\ProcessPodcast;
use App\Jobs\ReleasePodcast;
use Illuminate\Support\Facades\Bus;
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->dispatch();
Окрім об'єднання екземплярів класу завдань, ви також можете об'єднувати замикання:
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
function () {
Podcast::update(/* ... */);
},
])->dispatch();
Видалення завдань за допомогою методу $this->delete() всередині завдання не завадить обробці зв'язаних завдань. Ланцюг припинить виконання лише в тому випадку, якщо завдання в ланцюгу зазнає невдачі.
Зв'язок ланцюга та черга
Якщо ви хочете вказати з'єднання та чергу, які слід використовувати для зв'язаних завдань, ви можете скористатися методами onConnection та onQueue. Ці методи вказують з'єднання черги та ім'я черги, які слід використовувати, якщо тільки завдання в черзі не призначено явно іншому з'єднанню / черзі:
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->onConnection('redis')->onQueue('podcasts')->dispatch();
Додавання завдань до ланцюга
Іноді вам може знадобитися додати завдання на початок або в кінець існуючого ланцюга завдань з іншого завдання в цьому ланцюзі. Ви можете зробити це за допомогою методів prependToChain та appendToChain:
/**
* Виконати завдання.
*/
public function handle(): void
{
// ...
// Додати на початок поточного ланцюга, виконати завдання відразу після поточного завдання...
$this->prependToChain(new TranscribePodcast);
// Додати до поточного ланцюга, виконати завдання в кінці ланцюга...
$this->appendToChain(new TranscribePodcast);
}
Ланцюгові збої
Коли ви об'єднуєте завдання в ланцюжок, ви можете використовувати метод catch, щоб вказати замикання, яке має бути викликане, якщо завдання в ланцюжку зазнає невдачі. Передана зворотна функція отримає екземпляр Throwable, який спричинив невдачу завдання:
use Illuminate\Support\Facades\Bus;
use Throwable;
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->catch(function (Throwable $e) {
// Завдання в ланцюжку зазнало невдачі...
})->dispatch();
Оскільки зворотні виклики ланцюга серіалізуються та виконуються пізніше чергою Laravel, ви не повинні використовувати змінну $this у зворотних викликах ланцюга.
Налаштування черги та з'єднання
Відправка в конкретну чергу
Шляхом додавання завдань до різних черг, ви можете "категоризувати" ваші завдання в черзі і навіть пріоритизувати, скільки обробників ви призначаєте для різних черг. Майте на увазі, це не додає завдання до різних "з'єднань" черги, як визначено у вашому файлі конфігурації черги, а лише до конкретних черг в межах одного з'єднання. Щоб вказати чергу, використовуйте метод onQueue при відправці завдання:
<?php namespace App\Http\Controllers; use App\Jobs\ProcessPodcast; use App\Models\Podcast; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; class PodcastController extends Controller { /** * Зберегти новий подкаст. */ public function store(Request $request): RedirectResponse { $podcast = Podcast::create(/* ... */); // Створити подкаст... ProcessPodcast::dispatch($podcast)->onQueue('processing'); return redirect('/podcasts'); } }
Альтернативно, ви можете вказати чергу завдання, викликавши метод onQueue у конструкторі завдання:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Створити новий екземпляр завдання.
*/
public function __construct()
{
$this->onQueue('processing');
}
}
Відправка на конкретне з'єднання
Якщо ваш застосунок взаємодіє з декількома з'єднаннями черги, ви можете вказати, до якого з'єднання відправити завдання, використовуючи метод onConnection:
<?php namespace App\Http\Controllers; use App\Jobs\ProcessPodcast; use App\Models\Podcast; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; class PodcastController extends Controller { /** * Зберегти новий подкаст. */ public function store(Request $request): RedirectResponse { $podcast = Podcast::create(/* ... */); // Створити подкаст... ProcessPodcast::dispatch($podcast)->onConnection('sqs'); return redirect('/podcasts'); } }
Ви можете зв'язати методи onConnection та onQueue разом, щоб вказати з'єднання та чергу для завдання:
ProcessPodcast::dispatch($podcast)
->onConnection('sqs')
->onQueue('processing');
Альтернативно, ви можете вказати з'єднання для завдання, викликавши метод onConnection у конструкторі завдання:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Створити новий екземпляр завдання.
*/
public function __construct()
{
$this->onConnection('sqs');
}
}
Вказування Максимальної Кількості Спроб Завдань / Значень Тайм-ауту
Максимальна кількість спроб
Якщо одна з ваших завдань у черзі стикається з помилкою, ви, ймовірно, не хочете, щоб вона продовжувала повторюватися нескінченно. Тому Laravel надає різні способи вказати, скільки разів або як довго завдання може бути спробувано.
Один із підходів до визначення максимальної кількості спроб виконання завдання - це використання параметра --tries у командному рядку Artisan. Це буде застосовано до всіх завдань, оброблюваних обробником, якщо тільки завдання, що обробляється, не вказує кількість спроб, які можуть бути виконані:
php artisan queue:work --tries=3
Якщо завдання перевищує максимальну кількість спроб, воно буде вважатися "невдалим" завданням. Для отримання додаткової інформації про обробку невдалих завдань, зверніться до документації про невдалі завдання. Якщо --tries=0 вказано в команді queue:work, завдання буде повторюватися безкінечно.
Ви можете застосувати більш детальний підхід, визначивши максимальну кількість спроб виконання завдання безпосередньо в класі завдання. Якщо максимальна кількість спроб вказана в завданні, вона матиме пріоритет над значенням --tries, наданим у командному рядку:
<?php
namespace App\Jobs;
class ProcessPodcast implements ShouldQueue
{
/**
* Кількість спроб виконання завдання.
*
* @var int
*/
public $tries = 5;
}
Якщо вам потрібен динамічний контроль над максимальною кількістю спроб для конкретної роботи, ви можете визначити метод tries у цій роботі:
/**
* Визначте кількість спроб виконання завдання.
*/
public function tries(): int
{
return 5;
}
Спроби на основі часу
Як альтернативу визначенню, скільки разів завдання може бути спробувано перед тим, як воно зазнає невдачі, ви можете визначити час, після якого завдання більше не повинно бути спробувано. Це дозволяє спробувати завдання будь-яку кількість разів у межах заданого часового проміжку. Щоб визначити час, після якого завдання більше не повинно бути спробувано, додайте метод retryUntil до вашого класу завдання. Цей метод повинен повертати екземпляр DateTime:
use DateTime;
/**
* Визначте час, коли завдання має завершитися через тайм-аут.
*/
public function retryUntil(): DateTime
{
return now()->addMinutes(10);
}
Якщо визначені обидва retryUntil і tries, Laravel надає перевагу методу retryUntil.
Ви також можете визначити властивість tries або метод retryUntil у ваших поставлених у чергу слухачах подій та поставлених у чергу сповіщеннях.
Максимальна кількість винятків
Іноді ви можете захотіти вказати, що завдання може бути спробувано багато разів, але повинно завершитися невдачею, якщо повторні спроби викликані певною кількістю необроблених винятків (на відміну від звільнення безпосередньо методом release). Щоб досягти цього, ви можете визначити властивість maxExceptions у вашому класі завдання:
<?php
namespace App\Jobs;
use Illuminate\Support\Facades\Redis;
class ProcessPodcast implements ShouldQueue
{
/**
* Кількість спроб виконання завдання.
*
* @var int
*/
public $tries = 25;
/**
* Максимальна кількість необроблених винятків, яку дозволено перед тим, як статися збій.
*
* @var int
*/
public $maxExceptions = 3;
/**
* Виконати завдання.
*/
public function handle(): void
{
Redis::throttle('key')->allow(10)->every(60)->then(function () {
// Блокування отримано, обробка подкасту...
}, function () {
// Неможливо отримати блокування...
return $this->release(10);
});
}
}
У цьому прикладі завдання буде релізовано на десять секунд, якщо застосунок не зможе отримати блокування Redis, і буде продовжувати повторюватися до 25 разів. Однак завдання зазнає невдачі, якщо три необроблені винятки будуть викликані завданням.
Тайм-аут
Часто ви приблизно знаєте, скільки часу очікуєте, що ваші поставлені в чергу завдання займуть. З цієї причини Laravel дозволяє вказати значення "тайм-ауту". За замовчуванням значення тайм-ауту становить 60 секунд. Якщо завдання обробляється довше, ніж кількість секунд, вказана значенням тайм-ауту, обробник, що обробляє завдання, завершиться з помилкою. Зазвичай обробник буде автоматично перезапущено менеджером процесів, налаштованим на вашому сервері.
Максимальну кількість секунд, протягом яких можуть виконуватися завдання, можна вказати за допомогою параметра --timeout у командному рядку Artisan:
php artisan queue:work --timeout=30
Якщо завдання перевищує максимальну кількість спроб через постійне перевищення часу, воно буде позначене як невдале.
Ви також можете визначити максимальну кількість секунд, протягом яких завдання має дозволено виконуватися в самому класі завдання. Якщо тайм-аут вказано в завданні, він матиме пріоритет над будь-яким тайм-аутом, вказаним у командному рядку:
<?php
namespace App\Jobs;
class ProcessPodcast implements ShouldQueue
{
/**
* Кількість секунд, протягом яких завдання може виконуватися перед завершенням за часом.
*
* @var int
*/
public $timeout = 120;
}
Іноді процеси блокування вводу-виводу, такі як сокети або вихідні HTTP-з'єднання, можуть не враховувати ваш вказаний тайм-аут. Тому, використовуючи ці функції, завжди слід намагатися вказати тайм-аут за допомогою їх API. Наприклад, при використанні Guzzle завжди слід вказувати значення тайм-ауту з'єднання та запиту.
Розширення PHP pcntl повинно бути встановлено для того, щоб вказати тайм-аути завдань. Крім того, значення "timeout" завдання завжди повинно бути меншим за його значення "retry after". В іншому випадку, завдання може бути повторно спробувано до того, як воно фактично завершило виконання або вийшло за межі часу.
Помилка через перевищення часу очікування
Якщо ви хочете вказати, що завдання має бути позначене як неуспішне при перевищенні часу, ви можете визначити властивість $failOnTimeout у класі завдання:
/**
* Вкажіть, чи слід позначити завдання як невдале при перевищенні часу очікування.
*
* @var bool
*/
public $failOnTimeout = true;
Обробка Помилок
Якщо під час обробки завдання виникає виняток, завдання автоматично буде повернено назад у чергу, щоб його можна було спробувати знову. Завдання продовжуватиме повертатися, доки не буде досягнуто максимальної кількості спроб, дозволених вашим застосунком. Максимальна кількість спроб визначається перемикачем --tries, що використовується в команді Artisan queue:work. Крім того, максимальну кількість спроб можна визначити в самому класі завдання. Більше інформації про запуск обробника черги можна знайти нижче.
Ручне Випускання Завдання
Іноді ви можете захотіти вручну повернути завдання назад у чергу, щоб його можна було спробувати виконати пізніше. Ви можете зробити це, викликавши метод release:
/** * Виконати завдання. */ public function handle(): void { // ... $this->release(); }
За замовчуванням метод release поверне завдання назад у чергу для негайної обробки. Однак, ви можете вказати черзі не робити завдання доступним для обробки, поки не пройде задана кількість секунд, передавши ціле число або екземпляр дати в метод release:
$this->release(10);
$this->release(now()->addSeconds(10));
Ручне Завершення Завдання з Помилкою
Іноді вам може знадобитися вручну позначити завдання як "неуспішне". Для цього ви можете викликати метод fail:
/** * Виконати завдання. */ public function handle(): void { // ... $this->fail(); }
Якщо ви хочете позначити вашу задачу як невдалу через виняток, який ви перехопили, ви можете передати виняток методу fail. Або, для зручності, ви можете передати рядкове повідомлення про помилку, яке буде перетворено на виняток для вас:
$this->fail($exception); $this->fail('Щось пішло не так.');
Для отримання додаткової інформації про невдалі завдання, перегляньте документацію щодо обробки невдач завдань.
Помилки завдань при певних винятках
FailOnException job middleware дозволяє припинити повторні спроби, коли виникають певні винятки. Це дозволяє повторювати спроби при тимчасових винятках, таких як помилки зовнішнього API, але завершувати завдання назавжди при постійних винятках, таких як відкликання дозволів користувача:
<?php namespace App\Jobs; use App\Models\User; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\FailOnException; use Illuminate\Support\Facades\Http; class SyncChatHistory implements ShouldQueue { use InteractsWithQueue; public $tries = 3; /** * Створити новий екземпляр завдання. */ public function __construct( public User $user, ) {} /** * Виконати завдання. */ public function handle(): void { $user->authorize('sync-chat-history'); $response = Http::throw()->get( "https://chat.laravel.test/?user={$user->uuid}" ); // ... } /** * Отримати middleware, через яке має пройти завдання. */ public function middleware(): array { return [ new FailOnException([AuthorizationException::class]) ]; } }
Пакетна обробка завдань
Функція пакетної обробки завдань у Laravel дозволяє легко виконати пакет завдань, а потім виконати певну дію, коли пакет завдань завершить виконання. Перш ніж почати, вам слід створити міграцію бази даних для створення таблиці, яка міститиме метаінформацію про ваші пакети завдань, таку як відсоток їх завершення. Цю міграцію можна згенерувати за допомогою команди Artisan make:queue-batches-table:
php artisan make:queue-batches-table
php artisan migrate
Визначення пакетних завдань
Щоб визначити пакетну задачу, ви повинні створити чергову задачу як зазвичай; однак, ви повинні додати трейд Illuminate\Bus\Batchable до класу задачі. Цей трейд надає доступ до методу batch, який може бути використаний для отримання поточного пакету, в якому виконується задача:
<?php namespace App\Jobs; use Illuminate\Bus\Batchable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; class ImportCsv implements ShouldQueue { use Batchable, Queueable; /** * Виконати завдання. */ public function handle(): void { if ($this->batch()->cancelled()) { // Визначити, чи було скасовано пакет... return; } // Імпортувати частину CSV-файлу... } }
Відправлення Пакетів
Щоб відправити пакет завдань, слід використовувати метод batch фасаду Bus. Звичайно, пакетування є корисним, коли поєднується з зворотними викликами завершення. Тому ви можете використовувати методи then, catch і finally для визначення зворотних викликів завершення для пакету. Кожен з цих зворотних викликів отримає екземпляр Illuminate\Bus\Batch при виклику. У цьому прикладі ми уявимо, що ставимо в чергу пакет завдань, кожне з яких обробляє певну кількість рядків з файлу CSV:
use App\Jobs\ImportCsv;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
use Throwable;
$batch = Bus::batch([
new ImportCsv(1, 100),
new ImportCsv(101, 200),
new ImportCsv(201, 300),
new ImportCsv(301, 400),
new ImportCsv(401, 500),
])->before(function (Batch $batch) {
// Пакет було створено, але жодних завдань не додано...
})->progress(function (Batch $batch) {
// Одна задача успішно завершена...
})->then(function (Batch $batch) {
// Усі завдання виконано успішно...
})->catch(function (Batch $batch, Throwable $e) {
// Виявлено перший збій пакетного завдання...
})->finally(function (Batch $batch) {
// Пакет завершив виконання...
})->dispatch();
return $batch->id;
ID пакету, який можна отримати через властивість $batch->id, може бути використаний для запиту до командної шини Laravel для отримання інформації про пакет після його відправлення.
Оскільки зворотні виклики пакетів серіалізуються та виконуються пізніше чергою Laravel, не слід використовувати змінну $this у зворотних викликах. Крім того, оскільки пакетні завдання обгорнуті в транзакції бази даних, оператори бази даних, які викликають неявні коміти, не повинні виконуватися в межах завдань.
Називання пакетів
Деякі інструменти, такі як Laravel Horizon та Laravel Telescope, можуть надавати більш зручну для користувача інформацію для налагодження пакетів, якщо пакети мають назви. Щоб призначити довільну назву пакету, ви можете викликати метод name під час визначення пакету:
$batch = Bus::batch([ // ... ])->then(function (Batch $batch) { // Усі завдання успішно виконано... })->name('Import CSV')->dispatch();
Пакетне з'єднання та черга
Якщо ви хочете вказати з'єднання та чергу, які слід використовувати для пакетних завдань, ви можете скористатися методами onConnection та onQueue. Усі пакетні завдання повинні виконуватися в межах одного з'єднання та черги:
$batch = Bus::batch([ // ... ])->then(function (Batch $batch) { // Усі завдання успішно виконано... })->onConnection('redis')->onQueue('imports')->dispatch();
Ланцюги та Пакети
Ви можете визначити набір зв'язаних завдань у пакеті, розмістивши зв'язані завдання в масиві. Наприклад, ми можемо виконати два ланцюги завдань паралельно та виконати зворотний виклик, коли обидва ланцюги завдань завершать обробку:
use App\Jobs\ReleasePodcast;
use App\Jobs\SendPodcastReleaseNotification;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
Bus::batch([
[
new ReleasePodcast(1),
new SendPodcastReleaseNotification(1),
],
[
new ReleasePodcast(2),
new SendPodcastReleaseNotification(2),
],
])->then(function (Batch $batch) {
// ...
})->dispatch();
Навпаки, ви можете запускати партії завдань у межах ланцюга, визначаючи партії в межах ланцюга. Наприклад, ви можете спочатку запустити партію завдань для релізу декількох подкастів, а потім партію завдань для відправки сповіщень про реліз:
use App\Jobs\FlushPodcastCache;
use App\Jobs\ReleasePodcast;
use App\Jobs\SendPodcastReleaseNotification;
use Illuminate\Support\Facades\Bus;
Bus::chain([
new FlushPodcastCache,
Bus::batch([
new ReleasePodcast(1),
new ReleasePodcast(2),
]),
Bus::batch([
new SendPodcastReleaseNotification(1),
new SendPodcastReleaseNotification(2),
]),
])->dispatch();
Додавання Завдань до Пакетів
Іноді може бути корисно додати додаткові завдання до пакету зсередини пакетного завдання. Цей підхід може бути корисним, коли вам потрібно обробити тисячі завдань, які можуть зайняти занадто багато часу для відправки під час веб-запиту. Тому, замість цього, ви можете відправити початковий пакет завдань-"завантажувачів", які наповнюють пакет ще більшою кількістю завдань:
$batch = Bus::batch([ new LoadImportBatch, new LoadImportBatch, new LoadImportBatch, ])->then(function (Batch $batch) { // Усі завдання успішно виконано... })->name('Import Contacts')->dispatch();
У цьому прикладі ми використаємо завдання LoadImportBatch для наповнення партії додатковими завданнями. Щоб досягти цього, ми можемо використати метод add на екземплярі партії, до якого можна отримати доступ через метод batch завдання:
use App\Jobs\ImportContacts; use Illuminate\Support\Collection; /** * Виконати завдання. */ public function handle(): void { if ($this->batch()->cancelled()) { return; } $this->batch()->add(Collection::times(1000, function () { return new ImportContacts; })); }
Ви можете додавати завдання до пакету лише з завдання, яке належить до того ж пакету.
Інспектування пакетів
Екземпляр Illuminate\Bus\Batch, який надається для зворотних викликів завершення пакету, має різноманітні властивості та методи, щоб допомогти вам у взаємодії з певним пакетом завдань та його перевірці:
// UUID пакету...
$batch->id;
// Назва партії (якщо застосовно)...
$batch->name;
// Кількість завдань, призначених для пакету...
$batch->totalJobs;
// Кількість завдань, які не були оброблені чергою...
$batch->pendingJobs;
// Кількість завдань, які зазнали невдачі...
$batch->failedJobs;
// Кількість завдань, які були оброблені до цього часу...
$batch->processedJobs();
// Відсоток завершення партії (0-100)...
$batch->progress();
// Вказує, чи завершила партія виконання...
$batch->finished();
// Скасувати виконання пакету...
$batch->cancel();
// Вказує, чи партію було скасовано...
$batch->cancelled();
Повернення Пакетів З Маршрутів
Усі екземпляри Illuminate\Bus\Batch є серіалізованими в JSON, тобто ви можете повертати їх безпосередньо з одного з маршрутів вашого застосунку, щоб отримати JSON-вміст, що містить інформацію про пакет, включаючи його прогрес завершення. Це робить зручним відображення інформації про прогрес завершення пакета в інтерфейсі вашого застосунку.
Щоб отримати пакет за його ID, ви можете використовувати метод findBatch фасаду Bus:
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Route;
Route::get('/batch/{batchId}', function (string $batchId) {
return Bus::findBatch($batchId);
});
Скасування пакетів
Іноді вам може знадобитися скасувати виконання певної партії. Це можна зробити, викликавши метод cancel на екземплярі Illuminate\Bus\Batch:
/** * Виконати завдання. */ public function handle(): void { if ($this->user->exceedsImportLimit()) { return $this->batch()->cancel(); } if ($this->batch()->cancelled()) { return; } }
Як ви могли помітити в попередніх прикладах, пакетні завдання зазвичай повинні визначати, чи їх відповідний пакет було скасовано, перш ніж продовжити виконання. Однак, для зручності, ви можете призначити SkipIfBatchCancelled middleware до завдання замість цього. Як вказує його назва, цей middleware інструктує Laravel не обробляти завдання, якщо його відповідний пакет було скасовано:
use Illuminate\Queue\Middleware\SkipIfBatchCancelled; /** * Отримати middleware, через яке має пройти завдання. */ public function middleware(): array { return [new SkipIfBatchCancelled]; }
Помилки пакетної обробки
Коли пакетне завдання зазнає невдачі, буде викликано зворотний виклик catch (якщо призначено). Цей зворотний виклик викликається лише для першого завдання, яке зазнало невдачі в межах пакета.
Дозвіл на збої
Коли завдання в межах пакету зазнає невдачі, Laravel автоматично позначить пакет як "скасований". Якщо ви бажаєте, ви можете вимкнути цю поведінку, щоб невдача завдання не позначала пакет як скасований автоматично. Це можна зробити, викликавши метод allowFailures під час відправки пакету:
$batch = Bus::batch([ // ... ])->then(function (Batch $batch) { // Усі завдання успішно виконано... })->allowFailures()->dispatch();
Повторна спроба невдалих пакетних завдань
Для зручності Laravel надає Artisan команду queue:retry-batch, яка дозволяє легко повторно виконати всі невдалі завдання для заданої партії. Команда queue:retry-batch приймає UUID партії, невдалі завдання якої слід повторити:
php artisan queue:retry-batch 32dbc76c-4f82-4749-b610-a639fe0099b5
Обрізка пакетів
Без очищення таблиця job_batches може швидко накопичувати записи. Щоб пом'якшити це, ви повинні запланувати щоденне виконання Artisan команди queue:prune-batches:
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches')->daily();
За замовчуванням всі завершені партії, яким більше 24 годин, будуть видалені. Ви можете використовувати опцію hours при виклику команди, щоб визначити, як довго зберігати дані партій. Наприклад, наступна команда видалить всі партії, які завершилися понад 48 годин тому:
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches --hours=48')->daily();
Іноді ваша таблиця jobs_batches може накопичувати записи пакетів для пакетів, які ніколи не завершилися успішно, наприклад, пакети, де завдання зазнало невдачі і це завдання ніколи не було успішно повторено. Ви можете вказати команді queue:prune-batches видалити ці незавершені записи пакетів, використовуючи опцію unfinished:
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches --hours=48 --unfinished=72')->daily();
Аналогічно, ваша таблиця jobs_batches також може накопичувати записи пакетів для скасованих пакетів. Ви можете вказати команді queue:prune-batches видалити ці записи скасованих пакетів, використовуючи опцію cancelled:
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches --hours=48 --cancelled=72')->daily();
Зберігання пакетів у DynamoDB
Laravel також надає підтримку для зберігання метаінформації пакетів у DynamoDB замість реляційної бази даних. Однак вам потрібно буде вручну створити таблицю DynamoDB для зберігання всіх записів пакетів.
Зазвичай ця таблиця повинна називатися job_batches, але ви повинні назвати таблицю на основі значення конфігурації queue.batching.table у файлі конфігурації queue вашого застосунку.
Конфігурація пакетної таблиці DynamoDB
Таблиця job_batches повинна мати рядковий первинний ключ розділу з назвою application і рядковий первинний ключ сортування з назвою id. Частина ключа application міститиме назву вашого застосунку, як визначено значенням конфігурації name у файлі конфігурації app вашого застосунку. Оскільки назва застосунку є частиною ключа таблиці DynamoDB, ви можете використовувати ту саму таблицю для зберігання пакетів завдань для декількох застосунків Laravel.
Крім того, ви можете визначити атрибут ttl для вашої таблиці, якщо ви хочете скористатися автоматичним очищенням пакетів.
Конфігурація DynamoDB
Далі, встановіть AWS SDK, щоб ваш Laravel застосунок міг взаємодіяти з Amazon DynamoDB:
composer require aws/aws-sdk-php
Потім встановіть значення параметра конфігурації queue.batching.driver на dynamodb. Крім того, ви повинні визначити параметри конфігурації key, secret і region у межах масиву конфігурації batching. Ці параметри будуть використовуватися для автентифікації з AWS. При використанні драйвера dynamodb параметр конфігурації queue.batching.database не потрібен:
'batching' => [
'driver' => env('QUEUE_BATCHING_DRIVER', 'dynamodb'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => 'job_batches',
],
Обрізка пакетів у DynamoDB
Коли ви використовуєте DynamoDB для зберігання інформації про пакети завдань, типові команди обрізки, які використовуються для обрізки пакетів, збережених у реляційній базі даних, не працюватимуть. Натомість ви можете використовувати вбудовану функціональність TTL DynamoDB для автоматичного видалення записів старих пакетів.
Якщо ви визначили свою таблицю DynamoDB з атрибутом ttl, ви можете визначити параметри конфігурації, щоб вказати Laravel, як видаляти записи пакетів. Значення конфігурації queue.batching.ttl_attribute визначає назву атрибута, що містить TTL, тоді як значення конфігурації queue.batching.ttl визначає кількість секунд, після яких запис пакета може бути видалений з таблиці DynamoDB, відносно останнього часу оновлення запису:
'batching' => [ 'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'), 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 'table' => 'job_batches', 'ttl_attribute' => 'ttl', 'ttl' => 60 * 60 * 24 * 7, // 7 днів... ],
Черги Замикань
Замість відправлення класу завдання в чергу, ви також можете відправити замикання. Це чудово підходить для швидких, простих завдань, які потрібно виконати поза поточним циклом запиту. При відправленні замикань в чергу, вміст коду замикання криптографічно підписується, щоб його не можна було змінити під час передачі:
$podcast = App\Podcast::find(1);
dispatch(function () use ($podcast) {
$podcast->publish();
});
Щоб призначити ім'я для закриття в черзі, яке може бути використане інформаційними панелями звітності черги, а також відображатися командою queue:work, ви можете використовувати метод name:
dispatch(function () {
// ...
})->name('Publish Podcast');
Використовуючи метод catch, ви можете надати замикання, яке має бути виконане, якщо поставлене в чергу замикання не вдасться успішно завершити після вичерпання всіх налаштованих спроб повтору вашої черги:
use Throwable; dispatch(function () use ($podcast) { $podcast->publish(); })->catch(function (Throwable $e) { // Це завдання не виконалося... });
Оскільки зворотні виклики catch серіалізуються та виконуються пізніше чергою Laravel, ви не повинні використовувати змінну $this у зворотних викликах catch.
Запуск обробника черги
Команда queue:work
Laravel включає Artisan команду, яка запустить обробник черги та оброблятиме нові завдання, коли вони додаються до черги. Ви можете запустити обробник, використовуючи Artisan команду queue:work. Зверніть увагу, що після запуску команди queue:work, вона продовжуватиме працювати, доки її не буде вручну зупинено або ви не закриєте свій термінал:
php artisan queue:work
Щоб процес queue:work постійно працював у фоновому режимі, слід використовувати монітор процесів, такий як Supervisor, щоб гарантувати, що обробник черги не припиняє роботу.
Ви можете включити прапорець -v при виклику команди queue:work, якщо ви хочете, щоб оброблені ідентифікатори завдань, імена з'єднань та імена черг були включені у вивід команди:
php artisan queue:work -v
Пам'ятайте, обробники черги є довготривалими процесами і зберігають завантажений стан застосунку в пам'яті. В результаті, вони не помітять змін у вашій кодовій базі після того, як були запущені. Тому, під час процесу розгортання, обов'язково перезапустіть ваші обробники черги. Крім того, пам'ятайте, що будь-який статичний стан, створений або змінений вашим застосунком, не буде автоматично скинутий між завданнями.
Альтернативно, ви можете виконати команду queue:listen. Коли ви використовуєте команду queue:listen, вам не потрібно вручну перезапускати обробник, коли ви хочете перезавантажити оновлений код або скинути стан застосунку; однак, ця команда значно менш ефективна, ніж команда queue:work:
php artisan queue:listen
Запуск кількох обробників черги
Щоб призначити кілька обробників до черги та обробляти завдання одночасно, ви повинні просто запустити кілька процесів queue:work. Це можна зробити локально через кілька вкладок у вашому терміналі або в продакшені, використовуючи налаштування вашого менеджера процесів. При використанні Supervisor, ви можете використовувати значення конфігурації numprocs.
Вказання з'єднання та черги
Ви також можете вказати, яке з'єднання черги повинен використовувати обробник. Ім'я з'єднання, передане в команду work, повинно відповідати одному з з'єднань, визначених у вашому конфігураційному файлі config/queue.php:
php artisan queue:work redis
За замовчуванням, команда queue:work обробляє завдання лише для черги за замовчуванням на даному з'єднанні. Однак, ви можете ще більше налаштувати свого обробника черги, обробляючи лише певні черги для даного з'єднання. Наприклад, якщо всі ваші електронні листи обробляються в черзі emails на вашому з'єднанні черги redis, ви можете виконати наступну команду, щоб запустити обробник, який обробляє лише цю чергу:
php artisan queue:work redis --queue=emails
Обробка заданої кількості завдань
Опція --once може бути використана для того, щоб вказати обробнику обробити лише одне завдання з черги:
php artisan queue:work --once
Опція --max-jobs може бути використана для вказівки обробнику обробити задану кількість завдань, а потім завершити роботу. Ця опція може бути корисною у поєднанні з Supervisor, щоб ваші обробники автоматично перезапускалися після обробки заданої кількості завдань, звільняючи будь-яку пам'ять, яку вони могли накопичити:
php artisan queue:work --max-jobs=1000
Обробка всіх завдань у черзі, а потім вихід
Опція --stop-when-empty може бути використана для того, щоб вказати обробнику обробити всі завдання і потім завершити роботу коректно. Ця опція може бути корисною при обробці черг Laravel у контейнері Docker, якщо ви бажаєте вимкнути контейнер після того, як черга стане порожньою:
php artisan queue:work --stop-when-empty
Обробка завдань протягом заданої кількості секунд
Опція --max-time може бути використана для вказівки обробнику обробляти завдання протягом заданої кількості секунд, а потім завершити роботу. Ця опція може бути корисною у поєднанні з Supervisor, щоб ваші обробники автоматично перезапускалися після обробки завдань протягом заданого часу, звільняючи будь-яку пам'ять, яку вони могли накопичити:
# Обробляти завдання протягом однієї години, а потім завершити роботу... php artisan queue:work --max-time=3600
Тривалість сну обробника
Коли завдання доступні в черзі, обробник продовжуватиме обробляти завдання без затримки між ними. Однак опція sleep визначає, скільки секунд обробник буде "спати", якщо немає доступних завдань. Звісно, під час сну обробник не оброблятиме нові завдання:
php artisan queue:work --sleep=3
Режим обслуговування та черги
Поки ваш застосунок знаходиться в режимі обслуговування, жодні завдання в черзі не будуть оброблятися. Завдання продовжать оброблятися як зазвичай, щойно застосунок вийде з режиму обслуговування.
Щоб змусити ваших обробників черги обробляти завдання, навіть якщо режим обслуговування увімкнено, ви можете використовувати опцію --force:
php artisan queue:work --force
Міркування щодо ресурсів
Фонові обробники черги не "перезавантажують" фреймворк перед обробкою кожного завдання. Тому ви повинні звільняти будь-які важкі ресурси після завершення кожного завдання. Наприклад, якщо ви виконуєте маніпуляції з зображеннями за допомогою бібліотеки GD, ви повинні звільнити пам'ять за допомогою imagedestroy, коли закінчите обробку зображення.
Пріоритети черги
Іноді ви можете захотіти пріоритизувати, як обробляються ваші черги. Наприклад, у вашому конфігураційному файлі config/queue.php ви можете встановити чергу за замовчуванням для вашого з'єднання redis на low. Однак іноді ви можете захотіти відправити завдання в чергу з високим пріоритетом, наприклад:
dispatch((new Job)->onQueue('high'));
Щоб запустити обробник, який перевіряє, що всі завдання в черзі high оброблені перед переходом до будь-яких завдань у черзі low, передайте список імен черг, розділених комами, до команди work:
php artisan queue:work --queue=high,low
Обробники черги та розгортання
Оскільки обробники черги є довготривалими процесами, вони не помітять змін у вашому коді без перезапуску. Тому найпростіший спосіб розгорнути застосунок, що використовує обробники черги, - це перезапустити обробники під час процесу розгортання. Ви можете плавно їх перезапустити, виконавши команду queue:restart:
php artisan queue:restart
Ця команда накаже всім обробникам черги коректно завершити роботу після обробки їхньої поточної задачі, щоб жодна існуюча задача не була втрачена. Оскільки обробники черги завершать роботу, коли буде виконана команда queue:restart, ви повинні використовувати менеджер процесів, такий як Supervisor, для автоматичного перезапуску обробників черги.
Черга використовує кеш для зберігання сигналів перезапуску, тому перед використанням цієї функції слід переконатися, що драйвер кешу налаштований належним чином для вашого застосунку.
Термін дії та тайм-аути завдань
Термін дії завдання
У вашому файлі конфігурації config/queue.php кожне з'єднання черги визначає опцію retry_after. Ця опція вказує, скільки секунд з'єднання черги повинно чекати перед повторною спробою виконання завдання, яке обробляється. Наприклад, якщо значення retry_after встановлено на 90, завдання буде повернено назад у чергу, якщо воно обробляється протягом 90 секунд без звільнення або видалення. Зазвичай, ви повинні встановити значення retry_after на максимальну кількість секунд, яку ваші завдання повинні розумно займати для завершення обробки.
Єдине з'єднання черги, яке не містить значення retry_after, це Amazon SQS. SQS повторно спробує виконати завдання на основі Стандартного часу видимості, який керується в консолі AWS.
Час очікування обробника
Команда Artisan queue:work надає опцію --timeout. За замовчуванням значення --timeout становить 60 секунд. Якщо завдання обробляється довше, ніж кількість секунд, вказана у значенні timeout, обробник, що обробляє завдання, завершиться з помилкою. Зазвичай, обробник буде автоматично перезапущено менеджером процесів, налаштованим на вашому сервері:
php artisan queue:work --timeout=60
Опція конфігурації retry_after та опція CLI --timeout є різними, але працюють разом, щоб гарантувати, що завдання не втрачаються і що завдання обробляються успішно лише один раз.
Значення --timeout завжди повинно бути принаймні на кілька секунд коротшим за значення конфігурації retry_after. Це забезпечить, що обробник, який обробляє заморожене завдання, завжди буде завершений до повторної спроби виконання завдання. Якщо ваш параметр --timeout довший за значення конфігурації retry_after, ваші завдання можуть бути оброблені двічі.
Конфігурація Supervisor
У виробництві вам потрібен спосіб підтримувати процеси queue:work у робочому стані. Процес queue:work може зупинитися з різних причин, таких як перевищення часу очікування обробника або виконання команди queue:restart.
З цієї причини вам потрібно налаштувати монітор процесів, який може виявляти, коли ваші процеси queue:work завершуються, і автоматично перезапускати їх. Крім того, монітори процесів дозволяють вам вказати, скільки процесів queue:work ви хочете запускати одночасно. Supervisor — це монітор процесів, який часто використовується в середовищах Linux, і ми обговоримо, як його налаштувати в наступній документації.
Встановлення Supervisor
Supervisor є монітором процесів для операційної системи Linux, і автоматично перезапустить ваші процеси queue:work, якщо вони зазнають невдачі. Щоб встановити Supervisor на Ubuntu, ви можете скористатися наступною командою:
sudo apt-get install supervisor
Якщо налаштування та управління Supervisor здається вам занадто складним, розгляньте можливість використання Laravel Cloud, яка надає повністю керовану платформу для запуску Laravel обробників черги.
Налаштування Supervisor
Файли конфігурації Supervisor зазвичай зберігаються в каталозі /etc/supervisor/conf.d. У цьому каталозі ви можете створити будь-яку кількість конфігураційних файлів, які інструктують supervisor, як слід моніторити ваші процеси. Наприклад, давайте створимо файл laravel-worker.conf, який запускає та моніторить процеси queue:work:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/forge/app.com/artisan queue:work sqs --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/home/forge/app.com/worker.log
stopwaitsecs=3600
У цьому прикладі директива numprocs вкаже Supervisor запустити вісім процесів queue:work і контролювати всі з них, автоматично перезапускаючи їх у разі збою. Ви повинні змінити директиву command конфігурації, щоб відобразити бажане з'єднання черги та параметри обробника.
Ви повинні переконатися, що значення stopwaitsecs більше, ніж кількість секунд, витрачених вашим найдовшим завданням. В іншому випадку Supervisor може завершити завдання до того, як воно закінчить обробку.
Запуск Supervisor
Після створення файлу конфігурації, ви можете оновити конфігурацію Supervisor і запустити процеси, використовуючи наступні команди:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start "laravel-worker:*"
Для отримання додаткової інформації про Supervisor зверніться до документації Supervisor.
Робота з невдалими завданнями
Іноді ваші поставлені в чергу завдання будуть зазнавати невдачі. Не хвилюйтеся, не завжди все йде за планом! Laravel включає зручний спосіб вказати максимальну кількість спроб виконання завдання. Після того, як асинхронне завдання перевищило цю кількість спроб, воно буде вставлено в таблицю бази даних failed_jobs. Синхронно відправлені завдання, які зазнали невдачі, не зберігаються в цій таблиці, і їх винятки негайно обробляються застосунком.
Міграція для створення таблиці failed_jobs зазвичай вже присутня в нових застосунках Laravel. Однак, якщо ваш застосунок не містить міграції для цієї таблиці, ви можете використати команду make:queue-failed-table для створення міграції:
php artisan make:queue-failed-table
php artisan migrate
Коли ви запускаєте процес виконання черги, ви можете вказати максимальну кількість спроб виконання завдання, використовуючи параметр --tries у команді queue:work. Якщо ви не вкажете значення для опції --tries, завдання буде виконано лише один раз або стільки разів, скільки вказано у властивості $tries класу завдання:
php artisan queue:work redis --tries=3
Використовуючи опцію --backoff, ви можете вказати, скільки секунд Laravel повинен чекати перед повторною спробою виконання завдання, яке зіткнулося з винятком. За замовчуванням завдання негайно повертається в чергу, щоб його можна було спробувати знову:
php artisan queue:work redis --tries=3 --backoff=3
Якщо ви хочете налаштувати, скільки секунд Laravel повинен чекати перед повторною спробою виконання завдання, яке зіткнулося з винятком, на основі кожного завдання, ви можете зробити це, визначивши властивість backoff у вашому класі завдання:
/** * Кількість секунд очікування перед повторною спробою виконання завдання. * * @var int */ public $backoff = 3;
Якщо вам потрібна більш складна логіка для визначення часу відкладення завдання, ви можете визначити метод backoff у вашому класі завдання:
/** * Обчислити кількість секунд очікування перед повторною спробою виконати завдання. */ public function backoff(): int { return 3; }
Ви можете легко налаштувати "експоненційні" затримки, повертаючи масив значень затримки з методу backoff. У цьому прикладі затримка повторної спроби буде 1 секунда для першої спроби, 5 секунд для другої спроби, 10 секунд для третьої спроби, і 10 секунд для кожної наступної спроби, якщо залишаються ще спроби:
/** * Обчислити кількість секунд очікування перед повторною спробою виконати завдання. * * @return array<int, int> */ public function backoff(): array { return [1, 5, 10]; }
Очищення після невдалих завдань
Коли певна задача зазнає невдачі, ви можете захотіти надіслати сповіщення вашим користувачам або скасувати будь-які дії, які були частково виконані задачею. Щоб досягти цього, ви можете визначити метод failed у вашому класі задачі. Екземпляр Throwable, який спричинив невдачу задачі, буде передано до методу failed:
<?php
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Throwable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Створити новий екземпляр завдання.
*/
public function __construct(
public Podcast $podcast,
) {}
/**
* Виконати завдання.
*/
public function handle(AudioProcessor $processor): void
{
// Обробка завантаженого подкасту...
}
/**
* Обробка невдачі завдання.
*/
public function failed(?Throwable $exception): void
{
// Надіслати користувачу сповіщення про невдачу тощо...
}
}
Нова інстанція завдання створюється перед викликом методу failed; тому будь-які зміни властивостей класу, які могли відбутися в межах методу handle, будуть втрачені.
Повторна спроба невдалих завдань
Щоб переглянути всі невдалі завдання, які були вставлені у вашу таблицю бази даних failed_jobs, ви можете використовувати команду Artisan queue:failed:
php artisan queue:failed
Команда queue:failed виведе ID завдання, з'єднання, чергу, час збою та іншу інформацію про завдання. ID завдання може бути використаний для повторної спроби виконання завдання, яке зазнало невдачі. Наприклад, щоб повторити завдання, яке зазнало невдачі і має ID ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece, виконайте наступну команду:
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece
Якщо необхідно, ви можете передати кілька ID до команди:
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece 91401d2c-0784-4f43-824c-34f94a33c24d
Ви також можете повторно спробувати всі невдалі завдання для певної черги:
php artisan queue:retry --queue=name
Щоб повторити всі ваші невдалі завдання, виконайте команду queue:retry і передайте all як ID:
php artisan queue:retry all
Якщо ви хочете видалити невдалу задачу, ви можете скористатися командою queue:forget:
php artisan queue:forget 91401d2c-0784-4f43-824c-34f94a33c24d
Коли використовуєте Horizon, ви повинні використовувати команду horizon:forget для видалення невдалої задачі замість команди queue:forget.
Щоб видалити всі ваші невдалі завдання з таблиці failed_jobs, ви можете використовувати команду queue:flush:
php artisan queue:flush
Ігнорування Відсутніх Моделей
Коли модель Eloquent інжектується в завдання, модель автоматично серіалізується перед тим, як бути поміщеною в чергу, і повторно отримується з бази даних, коли завдання обробляється. Однак, якщо модель була видалена, поки завдання чекало на обробку обробником, ваше завдання може зазнати невдачі з ModelNotFoundException.
Для зручності, ви можете вибрати автоматичне видалення завдань з відсутніми моделями, встановивши властивість deleteWhenMissingModels вашого завдання в true. Коли ця властивість встановлена в true, Laravel тихо відкине завдання без викидання винятку:
/**
* Видалити завдання, якщо його моделі більше не існують.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
Обрізка невдалих завдань
Ви можете очистити записи в таблиці failed_jobs вашого застосунку, викликавши Artisan команду queue:prune-failed:
php artisan queue:prune-failed
За замовчуванням всі записи про невдалі завдання, яким більше 24 годин, будуть видалені. Якщо ви надасте опцію --hours до команди, будуть збережені лише записи про невдалі завдання, які були вставлені протягом останніх N годин. Наприклад, наступна команда видалить всі записи про невдалі завдання, які були вставлені більше ніж 48 годин тому:
php artisan queue:prune-failed --hours=48
Зберігання невдалих завдань у DynamoDB
Laravel також надає підтримку для зберігання записів про невдалі завдання у DynamoDB замість реляційної бази даних. Однак, ви повинні вручну створити таблицю DynamoDB для зберігання всіх записів про невдалі завдання. Зазвичай ця таблиця повинна називатися failed_jobs, але ви повинні назвати таблицю відповідно до значення конфігурації queue.failed.table у файлі конфігурації queue вашого застосунку.
Таблиця failed_jobs повинна мати рядковий первинний ключ розділу з назвою application і рядковий первинний ключ сортування з назвою uuid. Частина ключа application міститиме назву вашого застосунку, як визначено значенням конфігурації name у файлі конфігурації app вашого застосунку. Оскільки назва застосунку є частиною ключа таблиці DynamoDB, ви можете використовувати ту саму таблицю для зберігання невдалих завдань для декількох застосунків Laravel.
Крім того, переконайтеся, що ви встановили AWS SDK, щоб ваш Laravel застосунок міг взаємодіяти з Amazon DynamoDB:
composer require aws/aws-sdk-php
Далі, встановіть значення параметра конфігурації queue.failed.driver на dynamodb. Крім того, ви повинні визначити параметри конфігурації key, secret та region у масиві конфігурації невдалих завдань. Ці параметри будуть використовуватися для автентифікації з AWS. При використанні драйвера dynamodb параметр конфігурації queue.failed.database не потрібен:
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => 'failed_jobs',
],
Вимкнення зберігання невдалих завдань
Ви можете вказати Laravel відхиляти невдалі завдання без їх збереження, встановивши значення параметра конфігурації queue.failed.driver на null. Зазвичай це можна зробити через змінну середовища QUEUE_FAILED_DRIVER:
QUEUE_FAILED_DRIVER=null
Події невдалих завдань
Якщо ви хочете зареєструвати слухача подій, який буде викликаний, коли завдання не вдається, ви можете використовувати метод failing фасаду Queue. Наприклад, ми можемо прикріпити замикання до цієї події з методу boot класу AppServiceProvider, який включено в Laravel:
<?php namespace App\Providers; use Illuminate\Support\Facades\Queue; use Illuminate\Support\ServiceProvider; use Illuminate\Queue\Events\JobFailed; class AppServiceProvider extends ServiceProvider { /** * Зареєструвати будь-які сервіси застосунку. */ public function register(): void { // ... } /** * Ініціалізувати будь-які сервіси застосунку. */ public function boot(): void { Queue::failing(function (JobFailed $event) { // $event->connectionName // $event->job // $event->exception }); } }
Очищення Завдань З Черг
Коли використовуєте Horizon, ви повинні використовувати команду horizon:clear для очищення завдань з черги замість команди queue:clear.
Якщо ви хочете видалити всі завдання з черги за замовчуванням підключення за замовчуванням, ви можете зробити це за допомогою команди Artisan queue:clear:
php artisan queue:clear
Ви також можете надати аргумент connection і опцію queue, щоб видалити завдання з конкретного з'єднання та черги:
php artisan queue:clear redis --queue=emails
Очищення завдань з черг доступне лише для драйверів черг SQS, Redis та бази даних. Крім того, процес видалення повідомлень SQS займає до 60 секунд, тому завдання, надіслані в чергу SQS протягом 60 секунд після очищення черги, також можуть бути видалені.
Моніторинг Ваших Черг
Якщо ваша черга отримує раптовий наплив завдань, вона може бути перевантажена, що призведе до тривалого часу очікування завершення завдань. Якщо ви бажаєте, Laravel може сповістити вас, коли кількість завдань у черзі перевищує вказаний поріг.
Щоб розпочати, вам слід запланувати команду queue:monitor для запуску щохвилини. Команда приймає назви черг, які ви бажаєте моніторити, а також бажаний поріг кількості завдань:
php artisan queue:monitor redis:default,redis:deployments --max=100
Запланувати цю команду недостатньо, щоб викликати сповіщення, яке повідомляє вас про перевантажений стан черги. Коли команда стикається з чергою, яка має кількість завдань, що перевищує ваш поріг, буде відправлено подію Illuminate\Queue\Events\QueueBusy. Ви можете прослуховувати цю подію у AppServiceProvider вашого застосунку, щоб надіслати сповіщення вам або вашій команді розробників:
use App\Notifications\QueueHasLongWaitTime; use Illuminate\Queue\Events\QueueBusy; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Notification; /** * Ініціалізувати будь-які сервіси застосунку. */ public function boot(): void { Event::listen(function (QueueBusy $event) { Notification::route('mail', 'example@example.com') ->notify(new QueueHasLongWaitTime( $event->connection, $event->queue, $event->size )); }); }
Тестування
Коли тестуєте код, що відправляє завдання, ви можете захотіти вказати Laravel не виконувати саме завдання, оскільки код завдання може бути протестований безпосередньо і окремо від коду, що його відправляє. Звісно, щоб протестувати саме завдання, ви можете створити екземпляр завдання і викликати метод handle безпосередньо у вашому тесті.
Ви можете використовувати метод fake фасаду Queue, щоб запобігти фактичному додаванню завдань у чергу. Після виклику методу fake фасаду Queue, ви можете перевірити, що застосунок намагався додати завдання в чергу:
<?php use App\Jobs\AnotherJob; use App\Jobs\FinalJob; use App\Jobs\ShipOrder; use Illuminate\Support\Facades\Queue; test('orders can be shipped', function () { Queue::fake(); // Виконати відправлення замовлення... // Перевірити, що жодне завдання не було поставлено в чергу... Queue::assertNothingPushed(); // Перевірити, що завдання було поставлено в чергу з вказаною назвою... Queue::assertPushedOn('queue-name', ShipOrder::class); // Перевірити, що завдання було поставлено в чергу двічі... Queue::assertPushed(ShipOrder::class, 2); // Перевірити, що завдання не було поставлено в чергу... Queue::assertNotPushed(AnotherJob::class); // Перевірити, що замикання було поставлено в чергу... Queue::assertClosurePushed(); // Перевірити загальну кількість завдань, поставлених у чергу... Queue::assertCount(3); });
<?php namespace Tests\Feature; use App\Jobs\AnotherJob; use App\Jobs\FinalJob; use App\Jobs\ShipOrder; use Illuminate\Support\Facades\Queue; use Tests\TestCase; class ExampleTest extends TestCase { public function test_orders_can_be_shipped(): void { Queue::fake(); // Виконати відправлення замовлення... // Перевірити, що жодне завдання не було поставлено в чергу... Queue::assertNothingPushed(); // Перевірити, що завдання було поставлено в чергу з вказаною назвою... Queue::assertPushedOn('queue-name', ShipOrder::class); // Перевірити, що завдання було поставлено в чергу двічі... Queue::assertPushed(ShipOrder::class, 2); // Перевірити, що завдання не було поставлено в чергу... Queue::assertNotPushed(AnotherJob::class); // Перевірити, що замикання було поставлено в чергу... Queue::assertClosurePushed(); // Перевірити загальну кількість завдань, поставлених у чергу... Queue::assertCount(3); } }
Ви можете передати замикання до методів assertPushed або assertNotPushed, щоб перевірити, що завдання було відправлено, яке проходить заданий "тест на істинність". Якщо було відправлено принаймні одне завдання, яке проходить заданий тест на істинність, тоді перевірка буде успішною:
Queue::assertPushed(function (ShipOrder $job) use ($order) {
return $job->order->id === $order->id;
});
Імітація підмножини завдань
Якщо вам потрібно підробити лише конкретні завдання, дозволяючи іншим завданням виконуватися нормально, ви можете передати назви класів завдань, які слід підробити, до методу fake:
test('orders can be shipped', function () { Queue::fake([ ShipOrder::class, ]); // Виконати відправлення замовлення... // Перевірити, що завдання було поставлено в чергу двічі... Queue::assertPushed(ShipOrder::class, 2); });
public function test_orders_can_be_shipped(): void { Queue::fake([ ShipOrder::class, ]); // Виконати відправлення замовлення... // Перевірити, що завдання було поставлено в чергу двічі... Queue::assertPushed(ShipOrder::class, 2); }
Ви можете підробити всі завдання, крім набору вказаних завдань, використовуючи метод except:
Queue::fake()->except([
ShipOrder::class,
]);
Тестування Ланцюгів Завдань
Щоб протестувати ланцюги завдань, вам потрібно використовувати можливості фальсифікації фасаду Bus. Метод assertChained фасаду Bus може бути використаний для перевірки, що ланцюг завдань був відправлений. Метод assertChained приймає масив ланцюгових завдань як свій перший аргумент:
use App\Jobs\RecordShipment;
use App\Jobs\ShipOrder;
use App\Jobs\UpdateInventory;
use Illuminate\Support\Facades\Bus;
Bus::fake();
// ...
Bus::assertChained([
ShipOrder::class,
RecordShipment::class,
UpdateInventory::class
]);
Як ви можете бачити у наведеному вище прикладі, масив зв'язаних завдань може бути масивом імен класів завдань. Однак, ви також можете надати масив фактичних екземплярів завдань. При цьому Laravel забезпечить, що екземпляри завдань є того ж класу і мають ті ж значення властивостей, що й зв'язані завдання, відправлені вашим застосунком:
Bus::assertChained([
new ShipOrder,
new RecordShipment,
new UpdateInventory,
]);
Ви можете використовувати метод assertDispatchedWithoutChain, щоб перевірити, що завдання було відправлено без ланцюга завдань:
Bus::assertDispatchedWithoutChain(ShipOrder::class);
Тестування Змін Ланцюга
Якщо зв'язана задача додає або приєднує задачі до існуючого ланцюга, ви можете використовувати метод задачі assertHasChain, щоб перевірити, що задача має очікуваний ланцюг залишкових задач:
$job = new ProcessPodcast;
$job->handle();
$job->assertHasChain([
new TranscribePodcast,
new OptimizePodcast,
new ReleasePodcast,
]);
Метод assertDoesntHaveChain може бути використаний для перевірки, що залишковий ланцюг завдання порожній:
$job->assertDoesntHaveChain();
Тестування Зв'язаних Пакетів
Якщо ваш ланцюг завдань містить пакет завдань, ви можете перевірити, що ланцюгований пакет відповідає вашим очікуванням, вставивши визначення Bus::chainedBatch у вашу перевірку ланцюга:
use App\Jobs\ShipOrder;
use App\Jobs\UpdateInventory;
use Illuminate\Bus\PendingBatch;
use Illuminate\Support\Facades\Bus;
Bus::assertChained([
new ShipOrder,
Bus::chainedBatch(function (PendingBatch $batch) {
return $batch->jobs->count() === 3;
}),
new UpdateInventory,
]);
Тестування Пакетів Завдань
Метод assertBatched фасаду Bus може бути використаний для перевірки, що пакет завдань був відправлений. Замикання, передане методу assertBatched, отримує екземпляр Illuminate\Bus\PendingBatch, який може бути використаний для перевірки завдань у пакеті:
use Illuminate\Bus\PendingBatch;
use Illuminate\Support\Facades\Bus;
Bus::fake();
// ...
Bus::assertBatched(function (PendingBatch $batch) {
return $batch->name == 'import-csv' &&
$batch->jobs->count() === 10;
});
Ви можете використовувати метод assertBatchCount, щоб перевірити, що задана кількість пакетів була відправлена:
Bus::assertBatchCount(3);
Ви можете використовувати assertNothingBatched, щоб перевірити, що жодні пакети не були відправлені:
Bus::assertNothingBatched();
Тестування Взаємодії Завдань / Пакетів
Крім того, іноді вам може знадобитися протестувати взаємодію окремої задачі з її базовою партією. Наприклад, вам може знадобитися перевірити, чи скасувала задача подальшу обробку для своєї партії. Для цього вам потрібно призначити фейкову партію задачі за допомогою методу withFakeBatch. Метод withFakeBatch повертає кортеж, що містить екземпляр задачі та фейкову партію:
[$job, $batch] = (new ShipOrder)->withFakeBatch();
$job->handle();
$this->assertTrue($batch->cancelled());
$this->assertEmpty($batch->added);
Тестування Взаємодій Завдань / Черги
Іноді вам може знадобитися протестувати, що поставлена в чергу задача повертає себе назад у чергу. Або, можливо, вам потрібно протестувати, що задача видалила себе. Ви можете протестувати ці взаємодії з чергою, створивши екземпляр задачі та викликавши метод withFakeQueueInteractions.
Після того як взаємодії черги завдання були підроблені, ви можете викликати метод handle на завданні. Після виклику завдання, методи assertReleased, assertDeleted, assertNotDeleted, assertFailed, assertFailedWith та assertNotFailed можуть бути використані для перевірки взаємодій черги завдання:
use App\Exceptions\CorruptedAudioException;
use App\Jobs\ProcessPodcast;
$job = (new ProcessPodcast)->withFakeQueueInteractions();
$job->handle();
$job->assertReleased(delay: 30);
$job->assertDeleted();
$job->assertNotDeleted();
$job->assertFailed();
$job->assertFailedWith(CorruptedAudioException::class);
$job->assertNotFailed();
Події Завдань
Використовуючи методи before та after на Queue фасаді, ви можете вказати зворотні виклики, які будуть виконані до або після обробки завдання в черзі. Ці зворотні виклики є чудовою можливістю для виконання додаткового логування або збільшення статистики для інформаційної панелі. Зазвичай, ви повинні викликати ці методи з методу boot Сервіс-провайдера. Наприклад, ми можемо використовувати AppServiceProvider, який включено в Laravel:
<?php namespace App\Providers; use Illuminate\Support\Facades\Queue; use Illuminate\Support\ServiceProvider; use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; class AppServiceProvider extends ServiceProvider { /** * Зареєструвати будь-які сервіси застосунку. */ public function register(): void { // ... } /** * Ініціалізувати будь-які сервіси застосунку. */ public function boot(): void { Queue::before(function (JobProcessing $event) { // $event->connectionName // $event->job // $event->job->payload() }); Queue::after(function (JobProcessed $event) { // $event->connectionName // $event->job // $event->job->payload() }); } }
Використовуючи метод looping на Queue фасаді, ви можете вказати зворотні виклики, які виконуються перед тим, як обробник намагається отримати завдання з черги. Наприклад, ви можете зареєструвати закриття для відкату будь-яких транзакцій, які залишилися відкритими через попередньо невдале завдання:
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
Queue::looping(function () {
while (DB::transactionLevel() > 0) {
DB::rollBack();
}
});
