- はじめに
- ジョブの作成
- ジョブミドルウェア
- ジョブのディスパッチ
- ジョブバッチ
- クロージャのキューイング
- キューワーカーの実行
- Supervisorの設定
- 失敗したジョブの対処
- キューからのジョブ削除
- キューの監視
- テスト
- ジョブイベント
#はじめに
ウェブアプリケーションを構築する際、アップロードされたCSVファイルの解析や保存など、通常のウェブリクエスト中に実行すると時間がかかる処理があるかもしれません。Laravelでは、こうした処理をバックグラウンドで実行できるキュージョブとして簡単に作成できます。時間のかかる処理をキューに移すことで、アプリケーションは高速にウェブリクエストに応答し、ユーザーにより良い体験を提供できます。
Laravelのキューは、Amazon SQS、Redis、リレーショナルデータベースなど、さまざまなキューバックエンドに対して統一されたキューイングAPIを提供します。
Laravelのキュー設定は、アプリケーションの config/queue.php 設定ファイルに保存されています。このファイルには、データベース、Amazon SQS、Redis、Beanstalkd ドライバーなど、フレームワークに含まれる各キュードライバーのコネクション設定が含まれています。また、ローカル開発時にジョブを即時実行する同期ドライバーや、キュージョブを破棄する null ドライバーも含まれています。
Laravelは現在、Redisベースのキュー用に美しいダッシュボードと設定システムを備えたHorizonを提供しています。詳細はHorizonドキュメントをご覧ください。
#コネクションとキューの違い
Laravelキューを使い始める前に、「コネクション」と「キュー」の違いを理解することが重要です。config/queue.php の connections 配列は、Amazon SQS、Beanstalk、Redisなどのバックエンドキューサービスへのコネクションを定義します。しかし、1つのキューコネクションには複数の「キュー」が存在し、それぞれが異なるジョブのスタックや山のように扱えます。
queue 設定ファイルの各コネクション設定例には queue 属性があります。これは、そのコネクションにジョブを送信した際のデフォルトキューです。つまり、ジョブをどのキューに送るか明示しなければ、そのコネクション設定の queue 属性で定義されたキューにジョブが配置されます。
use App\Jobs\ProcessPodcast;
// このジョブはデフォルトコネクションのデフォルトキューに送信されます...
ProcessPodcast::dispatch();
// このジョブはデフォルトコネクションの "emails" キューに送信されます...
ProcessPodcast::dispatch()->onQueue('emails');
アプリケーションによっては複数のキューにジョブを送る必要がなく、単一のシンプルなキューだけを使う場合もあります。しかし、複数のキューにジョブを送ることは、ジョブの処理優先度やセグメント分けをしたい場合に特に有用です。Laravelのキューワーカーは処理するキューを優先度順に指定できるためです。例えば、high キューにジョブを送れば、そのキューを優先的に処理するワーカーを動かせます。
php artisan queue:work --queue=high,default
#ドライバーの注意点と前提条件
#データベース
database キュードライバーを使うには、ジョブを格納するためのデータベーステーブルが必要です。このテーブルを作成するマイグレーションは、queue:table Artisanコマンドで生成できます。マイグレーションが作成されたら、migrate コマンドでデータベースをマイグレートしてください。
php artisan queue:table
php artisan migrate
最後に、アプリケーションの .env ファイルで QUEUE_CONNECTION 変数を更新し、database ドライバーを使うように設定するのを忘れないでください。
QUEUE_CONNECTION=database
#Redis
redis キュードライバーを使うには、config/database.php 設定ファイルでRedisデータベースコネクションを設定してください。
redis キュードライバーは serializer と compression のRedisオプションをサポートしていません。
Redisクラスタ
RedisキューコネクションがRedisクラスタを使う場合、キュー名には キーのハッシュタグ を含める必要があります。これは、特定のキューのすべてのRedisキーが同じハッシュスロットに配置されることを保証するためです。
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => '{default}',
'retry_after' => 90,
],
ブロッキング
Redisキューを使う場合、block_for 設定オプションで、ジョブが利用可能になるまでドライバーがどのくらい待つかを指定できます。待機後、ワーカーループを繰り返してRedisを再ポーリングします。
キューの負荷に応じてこの値を調整することで、Redisを常にポーリングするより効率的にできます。例えば、5 に設定すると、ジョブが利用可能になるまで最大5秒間ブロックします。
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'retry_after' => 90,
'block_for' => 5,
],
block_for を 0 に設定すると、ジョブが利用可能になるまでキューワーカーが無期限にブロックします。この状態では、SIGTERM などのシグナルが次のジョブ処理まで処理されません。
#その他のドライバー前提条件
以下の依存関係は、各キュードライバーで必要です。Composerパッケージマネージャーでインストールできます。
- Amazon SQS:
aws/aws-sdk-php ~3.0 - Beanstalkd:
pda/pheanstalk ~4.0 - Redis:
predis/predis ~1.0または phpredis PHP拡張
#ジョブの作成
#ジョブクラスの生成
デフォルトでは、アプリケーションのキュー可能なジョブはすべて app/Jobs ディレクトリに保存されます。app/Jobs ディレクトリが存在しない場合、make:job Artisanコマンドを実行すると自動的に作成されます。
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\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessPodcast implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* 新しいジョブインスタンスを作成します。
*/
public function __construct(
public Podcast $podcast,
) {}
/**
* ジョブを実行します。
*/
public function handle(AudioProcessor $processor): void
{
// アップロードされたポッドキャストを処理します...
}
}
この例では、Eloquentモデルをキュージョブのコンストラクタに直接渡せることに注目してください。ジョブが SerializesModels トレイトを使っているため、Eloquentモデルとそのロード済みリレーションはジョブ処理時に適切にシリアライズ・アンシリアライズされます。
キュージョブのコンストラクタでEloquentモデルを受け取る場合、キューにシリアライズされるのはモデルの識別子だけです。ジョブが実際に処理される際、キューシステムは自動的にデータベースから完全なモデルインスタンスとロード済みリレーションを再取得します。このモデルシリアライズの方法により、ジョブのペイロードが小さくなります。
#handle メソッドの依存性注入
handle メソッドはジョブがキューで処理される際に呼ばれます。handle メソッドの依存性は型宣言でき、Laravelのサービスコンテナが自動的に注入します。
コンテナがhandleメソッドに依存関係を注入する方法を完全に制御したい場合、コンテナのbindMethodメソッドを使用できます。bindMethodメソッドは、ジョブとコンテナを引数として受け取るコールバックを受け付けます。コールバック内では、handleメソッドを任意の方法で呼び出して構いません。通常は、App\Providers\AppServiceProvider サービスプロバイダ の boot メソッドからこのメソッドを呼び出してください:
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モデルのコレクションや配列を受け取る場合、そのコレクション内のモデルはジョブがデシリアライズされて実行される際にリレーションシップが復元されません。これは大量のモデルを扱うジョブで過剰なリソース使用を防ぐためです。
#ユニークジョブ
ユニークジョブには locks をサポートするキャッシュドライバーが必要です。現在、memcached、redis、dynamodb、database、file、array のキャッシュドライバーがアトミックロックをサポートしています。さらに、ユニークジョブの制約はバッチ内のジョブには適用されません。
特定のジョブのインスタンスがキューに常に1つだけ存在するようにしたい場合があります。その場合、ジョブクラスに 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 ジョブは商品IDによってユニークです。同じ商品IDのジョブが既に処理中の場合、新たなディスパッチは無視されます。さらに、既存のジョブが1時間以内に処理されなければ、ユニークロックが解除され、同じユニークキーの別ジョブをキューに投入できます。
複数のウェブサーバーやコンテナからジョブをディスパッチする場合は、すべてのサーバーが同じ中央キャッシュサーバーと通信していることを確認してください。そうしないと、Laravelがジョブのユニーク性を正確に判断できません。
#処理開始までジョブをユニークに保つ
デフォルトでは、ユニークジョブは処理完了またはすべてのリトライ失敗後に「ロック解除」されます。ただし、処理開始直前にロックを解除したい場合があります。その場合、ShouldBeUnique ではなく ShouldBeUniqueUntilProcessing インターフェイスを実装してください:
<?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 ジョブミドルウェアを使ってください。
#暗号化ジョブ
Laravelでは、ジョブのデータのプライバシーと整合性を 暗号化 によって保証できます。始めるには、ジョブクラスに ShouldBeEncrypted インターフェイスを追加するだけです。このインターフェイスを追加すると、Laravelはジョブをキューにプッシュする前に自動的に暗号化します:
<?php
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
class UpdateSearchIndex implements ShouldQueue, ShouldBeEncrypted
{
// ...
}
#ジョブミドルウェア
ジョブミドルウェアは、キューに入ったジョブの実行前後にカスタムロジックを挟むことで、ジョブ自体の冗長なコードを減らせます。例えば、以下の handle メソッドは Laravel の Redis レートリミット機能を使い、5秒に1回だけジョブを処理できるようにしています:
use Illuminate\Support\Facades\Redis;
/**
* ジョブを実行します。
*/
public function handle(): void
{
Redis::throttle('key')->block(0)->allow(1)->every(5)->then(function () {
info('ロックを取得しました...');
// ジョブを処理...
}, function () {
// ロックを取得できませんでした...
return $this->release(5);
});
}
このコードは有効ですが、handle メソッド内に Redis のレートリミットロジックが混在しているため、コードが煩雑になります。また、他のレートリミットしたいジョブでも同じロジックを繰り返す必要があります。
handle メソッド内でレート制限を行う代わりに、レート制限を担当するジョブミドルウェアを定義できます。Laravel はジョブミドルウェアのデフォルトの配置場所を定めていないため、アプリケーション内の任意の場所にジョブミドルウェアを配置できます。ここでは例としてミドルウェアを 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 メソッドから返すことでジョブに割り当てられます。このメソッドは make:job Artisanコマンドでスキャフォールドされたジョブには存在しないため、手動でジョブクラスに追加する必要があります:
use App\Jobs\Middleware\RateLimited;
/**
* ジョブが通過すべきミドルウェアを取得します。
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited];
}
ジョブミドルウェアは、キュー可能なイベントリスナー、メール送信クラス、通知にも割り当て可能です。
#レートリミット
先ほど独自のジョブ用レート制限ミドルウェアの書き方を示しましたが、Laravelには実際にジョブをレート制限するために利用できるレート制限ミドルウェアが組み込まれています。ルートのレートリミッターと同様に、ジョブのレートリミッターはRateLimiterファサードのforメソッドを使って定義します。
例えば、ユーザーが1時間に1回だけデータをバックアップできるようにし、プレミアム顧客には制限を設けない場合、AppServiceProvider の boot メソッドで RateLimiter を定義できます:
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);
});
}
上記の例では1時間あたりのレートリミットを定義しましたが、perMinute メソッドを使えば分単位のレートリミットも簡単に定義できます。また、by メソッドには任意の値を渡せますが、通常は顧客ごとにレートリミットを区切るために使います:
return Limit::perMinute(50)->by($job->user->id);
レートリミットを定義したら、Illuminate\Queue\Middleware\RateLimited ミドルウェアをジョブに割り当てられます。ジョブがレートリミットを超えるたびに、このミドルウェアは適切な遅延をつけてジョブをキューに戻します。
use Illuminate\Queue\Middleware\RateLimited;
/**
* ジョブが通過すべきミドルウェアを取得します。
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited('backups')];
}
レートリミットでジョブをキューに戻しても、ジョブの attempts 合計は増加します。ジョブクラスの tries や maxExceptions プロパティを適切に調整するか、retryUntil メソッドを使ってジョブの再試行期間を定義してください。
ジョブがレートリミットされた際に再試行させたくない場合は、dontRelease メソッドを使えます:
/**
* ジョブが通過すべきミドルウェアを取得します。
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new RateLimited('backups'))->dontRelease()];
}
Redis を使用している場合は、Redis に最適化されており基本的なレート制限ミドルウェアより効率的な Illuminate\Queue\Middleware\RateLimitedWithRedis ミドルウェアを利用できます。
#ジョブの重複防止
Laravel には任意のキーに基づいてジョブの重複を防止できる Illuminate\Queue\Middleware\WithoutOverlapping ミドルウェアが含まれています。これは、キューに入ったジョブが同時に一つだけ修正すべきリソースを変更する場合に役立ちます。
例えば、ユーザーのクレジットスコアを更新するキュージョブがあり、同じユーザーIDのクレジットスコア更新ジョブの重複を防ぎたいとします。これを実現するには、ジョブの middleware メソッドから WithoutOverlapping ミドルウェアを返します:
use Illuminate\Queue\Middleware\WithoutOverlapping;
/**
* ジョブが通過すべきミドルウェアを取得します。
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new WithoutOverlapping($this->user->id)];
}
同じタイプの重複ジョブはキューに戻されます。また、戻されたジョブが再試行されるまでの秒数を指定することも可能です:
/**
* ジョブが通過すべきミドルウェアを取得します。
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->releaseAfter(60)];
}
重複ジョブを即座に削除して再試行させたくない場合は、dontRelease メソッドを使用できます:
/**
* ジョブが通過すべきミドルウェアを取得します。
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->dontRelease()];
}
WithoutOverlapping ミドルウェアは Laravel のアトミックロック機能を利用しています。ジョブが予期せず失敗したりタイムアウトした場合にロックが解放されないことがあるため、expireAfter メソッドでロックの有効期限を明示的に設定できます。以下の例では、ジョブ処理開始から3分後に WithoutOverlapping ロックを解放するよう指示しています:
/**
* ジョブが通過すべきミドルウェアを取得します。
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->expireAfter(180)];
}
WithoutOverlapping ミドルウェアは ロック をサポートするキャッシュドライバーが必要です。現在、memcached、redis、dynamodb、database、file、array のキャッシュドライバーがアトミックロックをサポートしています。
#ジョブクラス間でのロックキー共有
デフォルトでは、WithoutOverlapping ミドルウェアは同じクラスのジョブの重複のみ防止します。つまり、異なるジョブクラスが同じロックキーを使っていても重複は防止されません。ただし、shared メソッドを使うことでジョブクラス間でキーを共有するよう Laravel に指示できます:
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 ミドルウェアが含まれています。ジョブが指定回数の例外を投げると、それ以降のジョブ実行は指定時間が経過するまで遅延されます。このミドルウェアは不安定なサードパーティサービスと連携するジョブに特に有効です。
例えば、サードパーティAPIと連携するキュージョブが例外を投げ始めた場合を想定します。例外をスロットリングするには、ジョブの middleware メソッドから ThrottlesExceptions ミドルウェアを返します。通常、このミドルウェアは 時間ベースの試行 を実装したジョブと組み合わせて使います:
use DateTime;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* ジョブが通過すべきミドルウェアを取得します。
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new ThrottlesExceptions(10, 5)];
}
/**
* ジョブのタイムアウト時間を決定します。
*/
public function retryUntil(): DateTime
{
return now()->addMinutes(5);
}
ミドルウェアの最初のコンストラクタ引数はスロットリングされる前にジョブが投げられる例外の数、2番目の引数はスロットリング後に再試行されるまでの分数です。上記の例では、ジョブが5分以内に10回例外を投げた場合、5分間待ってから再試行します。
ジョブが例外を投げても例外の閾値に達していない場合、通常は即座に再試行されます。ただし、ミドルウェアをジョブに付与する際に backoff メソッドを呼び出すことで、遅延させる分数を指定できます:
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* ジョブが通過すべきミドルウェアを取得します。
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 5))->backoff(5)];
}
内部的にこのミドルウェアは Laravel のキャッシュシステムを使ってレート制限を実装し、ジョブのクラス名をキャッシュの「キー」として利用します。by メソッドを使ってこのキーを上書きできます。これは複数のジョブが同じサードパーティサービスと連携し、共通のスロットリング「バケット」を共有したい場合に便利です:
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* ジョブが通過すべきミドルウェアを取得します。
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 10))->by('key')];
}
Redis を使用している場合は、Redis に最適化されており基本的な例外スロットリングミドルウェアより効率的な Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis ミドルウェアを利用できます。
#ジョブのディスパッチ
ジョブクラスを書いたら、ジョブ自身の dispatch メソッドを使ってディスパッチできます。dispatch メソッドに渡した引数はジョブのコンストラクタに渡されます:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
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\Http\Controllers\Controller;
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');
}
}
Amazon SQS キューサービスの最大遅延時間は15分です。
#レスポンス送信後のディスパッチ
代わりに、dispatchAfterResponse メソッドは HTTP レスポンスがユーザーのブラウザに送信された後にジョブのディスパッチを遅延させます(ウェブサーバーが FastCGI を使用している場合)。これにより、キューに入ったジョブが実行中でもユーザーはアプリケーションをすぐに利用開始できます。通常、メール送信など1秒程度のジョブにのみ使うべきです。これらのジョブは現在の HTTP リクエスト内で処理されるため、キューワーカーが動作していなくても処理されます:
use App\Jobs\SendNotification;
SendNotification::dispatchAfterResponse();
クロージャをdispatchして、HTTPレスポンスがブラウザに送信された後にそれを実行するためにdispatchヘルパにafterResponseメソッドをチェーンすることもできます:
use App\Mail\WelcomeMessage;
use Illuminate\Support\Facades\Mail;
dispatch(function () {
Mail::to('[email protected]')->send(new WelcomeMessage);
})->afterResponse();
#同期ディスパッチ
ジョブを即座に(同期的に)ディスパッチしたい場合は、dispatchSync メソッドを使えます。この方法ではジョブはキューに入らず、現在のプロセス内で即時に実行されます:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
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();
#ジョブチェーン
ジョブチェーンを使うと、メインジョブが正常に実行された後に順番に実行するキューイングされたジョブのリストを指定できます。チェーン内のどれかのジョブが失敗すると、残りのジョブは実行されません。キューイングされたジョブチェーンを実行するには、Bus ファサードの chain メソッドを使います。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();
#チェーンの失敗時処理
ジョブチェーンで、チェーン内のジョブが失敗した場合に呼び出すクロージャを 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\Http\Controllers\Controller;
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\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessPodcast implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* 新しいジョブインスタンスを作成します。
*/
public function __construct()
{
$this->onQueue('processing');
}
}
#特定の接続へのディスパッチ
アプリケーションが複数のキュー接続を使う場合、ジョブをプッシュする接続を onConnection メソッドで指定できます:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
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\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessPodcast implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* 新しいジョブインスタンスを作成します。
*/
public function __construct()
{
$this->onConnection('sqs');
}
}
#最大ジョブ試行回数 / タイムアウト値の指定
#最大試行回数
キューイングされたジョブがエラーを起こしている場合、無限にリトライし続けるのは望ましくありません。そこで、Laravel はジョブの最大試行回数や試行可能な期間を指定する方法を提供しています。
最大試行回数を指定する一つの方法は、Artisan コマンドラインの --tries オプションを使うことです。これは、処理するすべてのジョブに適用されますが、ジョブ自身が試行回数を指定している場合はそちらが優先されます:
php artisan queue:work --tries=3
ジョブが最大試行回数を超えると「失敗」したジョブと見なされます。失敗ジョブの扱いについては、失敗ジョブのドキュメントを参照してください。queue:work コマンドに --tries=0 を指定すると、ジョブは無制限にリトライされます。
より詳細に制御したい場合は、ジョブクラス自体に最大試行回数を定義できます。ジョブに最大試行回数が指定されている場合、コマンドラインの --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);
}
キューイングされたイベントリスナーにも 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 ロックが取得できなければジョブは10秒間リリースされ、最大25回までリトライされます。ただし、未処理例外が3回発生するとジョブは失敗します。
#タイムアウト
キューイングされたジョブがどのくらいの時間で終わるかおおよそ分かっている場合、Laravel は「タイムアウト」値を指定できます。デフォルトのタイムアウトは60秒です。ジョブの処理がタイムアウト時間を超えると、ジョブを処理しているワーカーはエラーで終了します。通常、ワーカーはサーバー上のプロセスマネージャー設定によって自動的に再起動されます。
Artisan コマンドラインの --timeout オプションでジョブの最大実行秒数を指定できます:
php artisan queue:work --timeout=30
ジョブがタイムアウトを繰り返して最大試行回数を超えた場合、失敗とみなされます。
ジョブクラス自体に最大実行秒数を定義することもできます。ジョブにタイムアウトが指定されている場合、コマンドラインの設定より優先されます:
<?php
namespace App\Jobs;
class ProcessPodcast implements ShouldQueue
{
/**
* ジョブがタイムアウトするまでの実行可能秒数。
*
* @var int
*/
public $timeout = 120;
}
時には、ソケットや外部HTTP接続のようなIOブロッキング処理が指定したタイムアウトを尊重しない場合があります。そのため、これらの機能を使う場合は、APIを使ってタイムアウトを指定することも常に試みるべきです。例えば、Guzzleを使う場合は、接続タイムアウトとリクエストタイムアウトの値を必ず指定してください。
ジョブのタイムアウトを指定するには、pcntl PHP拡張がインストールされている必要があります。また、ジョブの "timeout" 値は常にその "retry after" 値より小さくする必要があります。そうしないと、ジョブが実際に完了またはタイムアウトする前に再試行される可能性があります。
#タイムアウト時の失敗扱い
ジョブがタイムアウト時に失敗済みとしてマークされるようにしたい場合は、ジョブクラスに $failOnTimeout プロパティを定義できます。
/**
* ジョブがタイムアウト時に失敗としてマークされるかどうかを示します。
*
* @var bool
*/
public $failOnTimeout = true;
#エラー処理
ジョブの処理中に例外が発生すると、ジョブは自動的にキューに戻され、再試行されます。ジョブはアプリケーションで許可された最大試行回数に達するまで再度リリースされ続けます。最大試行回数は、queue:work Artisanコマンドの --tries オプションで定義されます。あるいは、ジョブクラス自体で最大試行回数を定義することも可能です。キューワーカーの実行に関する詳細は以下を参照してください。
#ジョブの手動リリース
ジョブを手動でキューに戻して後で再試行したい場合があります。その場合は、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('Something went wrong.');
失敗したジョブの詳細については、ジョブ失敗の処理に関するドキュメントを参照してください。
#ジョブバッチ処理
Laravelのジョブバッチ機能を使うと、複数のジョブをまとめて実行し、バッチ処理が完了した際に何らかのアクションを実行できます。始める前に、ジョブバッチのメタ情報(完了率など)を格納するテーブルを作成するためのデータベースマイグレーションを作成してください。このマイグレーションは queue:batches-table Artisanコマンドで生成できます。
php artisan queue:batches-table
php artisan migrate
#バッチ可能なジョブの定義
バッチ可能なジョブを定義するには、通常通りキュー可能なジョブを作成し、ジョブクラスに Illuminate\Bus\Batchable トレイトを追加します。このトレイトは、ジョブが実行されている現在のバッチを取得するための batch メソッドを提供します。
<?php
namespace App\Jobs;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ImportCsv implements ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* ジョブを実行します。
*/
public function handle(): void
{
if ($this->batch()->cancelled()) {
// バッチがキャンセルされているか確認...
return;
}
// CSVファイルの一部をインポート...
}
}
#バッチのディスパッチ
ジョブのバッチをディスパッチするには、Bus ファサードの batch メソッドを使います。バッチ処理は主に完了コールバックと組み合わせて使うため、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) {
// 1つのジョブが正常に完了した...
})->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();
#チェーンとバッチ
バッチ内にチェーンジョブのセットを定義できます。例えば、2つのジョブチェーンを並行して実行し、両方のチェーンが完了したらコールバックを実行することが可能です。
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 ジョブを使ってバッチに追加のジョブを補充します。これを実現するには、ジョブの batch メソッドでアクセスできるバッチインスタンスの add メソッドを使います。
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ペイロードを取得できます。これにより、アプリケーションのUIでバッチの進捗情報を表示しやすくなります。
バッチIDでバッチを取得するには、Bus ファサードの findBatch メソッドを使います。
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Route;
Route::get('/batch/{batchId}', function (string $batchId) {
return Bus::findBatch($batchId);
});
#バッチのキャンセル
特定のバッチの実行をキャンセルしたい場合は、Illuminate\Bus\Batch インスタンスの cancel メソッドを呼び出します。
/**
* ジョブを実行します。
*/
public function handle(): void
{
if ($this->user->exceedsImportLimit()) {
return $this->batch()->cancel();
}
if ($this->batch()->cancelled()) {
return;
}
}
前の例で気づいたかもしれませんが、バッチ処理されたジョブは通常、対応するバッチがキャンセルされているかどうかを実行を続ける前に確認する必要があります。ただし、利便性のために、ジョブに SkipIfBatchCancelled ミドルウェア を割り当てることもできます。このミドルウェアは名前の通り、対応するバッチがキャンセルされている場合にジョブを処理しないようLaravelに指示します。
use Illuminate\Queue\Middleware\SkipIfBatchCancelled;
/**
* Get the middleware the job should pass through.
*/
public function middleware(): array
{
return [new SkipIfBatchCancelled];
}
#バッチ失敗
バッチ処理されたジョブが失敗すると、catch コールバック(割り当てられている場合)が呼び出されます。このコールバックは、バッチ内で最初に失敗したジョブに対してのみ実行されます。
#失敗を許可する
バッチ内のジョブが失敗すると、Laravelは自動的にバッチを「キャンセル済み」とマークします。もしジョブの失敗でバッチが自動的にキャンセルされる動作を無効にしたい場合は、バッチをディスパッチする際に allowFailures メソッドを呼び出すことで実現できます。
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// すべてのジョブが正常に完了しました...
})->allowFailures()->dispatch();
#失敗したバッチジョブの再試行
利便性のために、Laravelは指定したバッチの失敗したすべてのジョブを簡単に再試行できる queue:retry-batch Artisanコマンドを提供しています。queue:retry-batch コマンドは、再試行したいバッチのUUIDを受け取ります。
php artisan queue:retry-batch 32dbc76c-4f82-4749-b610-a639fe0099b5
#バッチのプルーニング
プルーニングを行わないと、job_batches テーブルにレコードが非常に早く蓄積されてしまいます。これを防ぐために、queue:prune-batches Artisanコマンドを毎日実行するようにスケジューリングしてください。
$schedule->command('queue:prune-batches')->daily();
デフォルトでは、24時間以上経過した完了済みのバッチがすべてプルーニングされます。コマンド呼び出し時に hours オプションを使って、バッチデータの保持期間を指定できます。例えば、以下のコマンドは48時間以上前に完了したバッチを削除します。
$schedule->command('queue:prune-batches --hours=48')->daily();
時には、jobs_batches テーブルに、ジョブが失敗し再試行されなかったために完了しなかったバッチのレコードが蓄積されることがあります。queue:prune-batches コマンドに unfinished オプションを指定すると、これらの未完了バッチレコードをプルーニングできます。
$schedule->command('queue:prune-batches --hours=48 --unfinished=72')->daily();
同様に、キャンセルされたバッチのレコードも jobs_batches テーブルに蓄積されることがあります。queue:prune-batches コマンドに cancelled オプションを指定すると、これらのキャンセル済みバッチレコードをプルーニングできます。
$schedule->command('queue:prune-batches --hours=48 --cancelled=72')->daily();
#DynamoDBでのバッチ保存
Laravelは、リレーショナルデータベースの代わりに DynamoDB にバッチのメタ情報を保存することもサポートしています。ただし、すべてのバッチレコードを保存するためのDynamoDBテーブルは手動で作成する必要があります。
通常、このテーブルは job_batches と名付けますが、アプリケーションの queue 設定ファイル内の queue.batching.table 設定値に基づいてテーブル名を決めるべきです。
#DynamoDBバッチテーブルの設定
job_batches テーブルは、文字列型のプライマリパーティションキー application と文字列型のプライマリソートキー id を持つ必要があります。application キーには、アプリケーションの app 設定ファイル内の name 設定値で定義されたアプリケーション名が入ります。アプリケーション名がDynamoDBテーブルのキーの一部であるため、同じテーブルを複数のLaravelアプリケーションのジョブバッチ保存に使えます。
さらに、自動バッチプルーニングを利用したい場合は、テーブルに ttl 属性を定義できます。
#DynamoDBの設定
次に、LaravelアプリケーションがAmazon DynamoDBと通信できるようにAWS SDKをインストールしてください。
composer require aws/aws-sdk-php
そして、queue.batching.driver 設定値を dynamodb に設定します。加えて、batching 設定配列内に key、secret、region の設定を定義してください。これらはAWS認証に使われます。dynamodb ドライバーを使う場合、queue.batching.database 設定は不要です。
'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',
],
#DynamoDBでのバッチプルーニング
DynamoDB を使ってジョブバッチ情報を保存する場合、リレーショナルデータベースで使う一般的なプルーニングコマンドは使えません。代わりに、DynamoDBのネイティブTTL機能を利用して古いバッチのレコードを自動的に削除できます。
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();
});
catch メソッドを使うと、キューに入れたクロージャがすべての設定された再試行回数を使い果たしても正常に完了しなかった場合に実行されるクロージャを指定できます。
use Throwable;
dispatch(function () use ($podcast) {
$podcast->publish();
})->catch(function (Throwable $e) {
// このジョブは失敗しました...
});
catch コールバックはシリアライズされて後でLaravelキューによって実行されるため、catch コールバック内で $this 変数を使わないでください。
#キューワーカーの実行
#queue:work コマンド
Laravelには、キューワーカーを起動してキューに追加された新しいジョブを処理するArtisanコマンドが含まれています。queue:work Artisanコマンドを使ってワーカーを起動できます。queue:work コマンドが起動すると、手動で停止するかターミナルを閉じるまで実行し続けます。
php artisan queue:work
queue:work プロセスを常にバックグラウンドで実行し続けるには、Supervisor のようなプロセス監視ツールを使い、キューワーカーが停止しないようにしてください。
処理されたジョブIDをコマンドの出力に含めたい場合は、queue:work コマンド実行時に -v フラグを付けられます。
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 コマンドは指定された接続のデフォルトキューのジョブのみを処理します。ただし、特定の接続に対して特定のキューだけを処理するようにワーカーをさらにカスタマイズできます。例えば、すべてのメールが redis キュー接続の emails キューで処理されている場合、以下のコマンドでそのキューだけを処理するワーカーを起動できます。
php artisan queue:work redis --queue=emails
#指定したジョブ数の処理
--once オプションを使うと、ワーカーにキューから1つのジョブだけを処理させることができます。
php artisan queue:work --once
--max-jobs オプションを使うと、指定したジョブ数を処理した後にワーカーを終了させることができます。このオプションは、Supervisor と組み合わせて使うと、指定したジョブ数処理後にワーカーを自動再起動し、蓄積したメモリを解放するのに便利です。
php artisan queue:work --max-jobs=1000
#すべてのキュージョブを処理して終了
--stop-when-empty オプションを使うと、ワーカーにすべてのジョブを処理した後に正常終了させることができます。このオプションは、キューが空になったらDockerコンテナをシャットダウンしたい場合に便利です。
php artisan queue:work --stop-when-empty
#指定秒数だけジョブを処理
--max-time オプションを使うと、指定秒数だけジョブを処理してからワーカーを終了させることができます。このオプションは、Supervisor と組み合わせて使うと、指定時間処理後にワーカーを自動再起動し、蓄積したメモリを解放するのに便利です。
# 1時間ジョブを処理してから終了...
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 接続のデフォルト queue を low に設定している場合があります。しかし、場合によっては次のようにジョブを high 優先のキューに投入したいことがあるでしょう:
dispatch((new Job)->onQueue('high'));
high キューのジョブがすべて処理されてから low キューのジョブを処理するワーカーを起動するには、work コマンドにカンマ区切りのキュー名リストを渡します。
php artisan queue:work --queue=high,low
#キューワーカーとデプロイ
キューワーカーは長時間動作するプロセスのため、コードの変更を認識するには再起動が必要です。したがって、キューワーカーを使ったアプリケーションの最も簡単なデプロイ方法は、デプロイ時にワーカーを再起動することです。queue:restart コマンドを実行すると、すべてのワーカーをグレースフルに再起動できます。
php artisan queue:restart
このコマンドは、現在処理中のジョブが完了した後にすべてのキューワーカーにグレースフルに終了するよう指示するため、既存のジョブが失われることはありません。queue:restart コマンド実行時にキューワーカーは終了するため、Supervisor のようなプロセスマネージャーを使って自動的にワーカーを再起動することを推奨します。
キューは再起動シグナルを保存するために cache を使用するため、この機能を使う前にキャッシュドライバーが正しく設定されていることを確認してください。
#ジョブの有効期限とタイムアウト
#ジョブの有効期限
config/queue.php の設定ファイルでは、各キュー接続に retry_after オプションが定義されています。このオプションは、ジョブの再試行までにキュー接続が待機する秒数を指定します。例えば、retry_after が 90 に設定されている場合、90秒間リリースや削除されずに処理されているジョブはキューに戻されます。通常、retry_after はジョブが合理的に完了する最大秒数に設定すべきです。
retry_after を持たない唯一のキュー接続は Amazon SQS です。SQS は AWS コンソールで管理される Default Visibility Timeout に基づいてジョブを再試行します。
#ワーカーのタイムアウト
queue:work Artisan コマンドは --timeout オプションを提供します。デフォルトでは --timeout は60秒です。指定した秒数を超えてジョブが処理されている場合、そのジョブを処理しているワーカーはエラーで終了します。通常、ワーカーはサーバー上で設定された プロセスマネージャー によって自動的に再起動されます。
php artisan queue:work --timeout=60
retry_after 設定オプションと --timeout CLI オプションは異なりますが、ジョブの損失を防ぎ、ジョブが一度だけ正常に処理されるよう連携して動作します。
--timeout の値は常に retry_after 設定値より数秒短くする必要があります。これにより、処理が停止したジョブを処理中のワーカーが、ジョブが再試行される前に必ず終了します。--timeout が retry_after より長い場合、ジョブが二重に処理される可能性があります。
#Supervisor の設定
本番環境では、queue:work プロセスを常に稼働させる必要があります。queue:work プロセスは、ワーカーのタイムアウト超過や queue:restart コマンドの実行など、さまざまな理由で停止することがあります。
そのため、queue:work プロセスが終了したことを検知し自動的に再起動するプロセス監視ツールを設定する必要があります。さらに、プロセス監視ツールは同時に実行する queue:work プロセスの数を指定できます。Supervisor は Linux 環境でよく使われるプロセス監視ツールで、以下のドキュメントで設定方法を説明します。
#Supervisor のインストール
Supervisor は Linux OS 用のプロセス監視ツールで、queue:work プロセスが失敗した場合に自動的に再起動します。Ubuntu に Supervisor をインストールするには、以下のコマンドを使用します。
sudo apt-get install supervisor
Supervisor の設定や管理が難しい場合は、Laravel Forge の利用を検討してください。Forge は本番の Laravel プロジェクトに Supervisor を自動でインストール・設定します。
#Supervisor の設定
Supervisor の設定ファイルは通常 /etc/supervisor/conf.d ディレクトリに保存されます。このディレクトリ内に、プロセスの監視方法を指示する設定ファイルを複数作成できます。例えば、queue:work プロセスを起動・監視する laravel-worker.conf ファイルを作成しましょう。
[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 に対して8つの 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 アプリケーションに含まれています。ただし、もしマイグレーションがない場合は、queue:failed-table コマンドでマイグレーションを作成できます。
php artisan queue:failed-table
php artisan migrate
キュー・ワーカープロセスを実行する際、queue:workコマンドの--triesスイッチを使ってジョブを試行する最大回数を指定できます。--triesオプションの値を指定しない場合、ジョブは一度だけ、またはジョブクラスの$triesプロパティで指定された回数だけ試行されます:
php artisan queue:work redis --tries=3
--backoff オプションを使うと、例外が発生したジョブを再試行するまでに Laravel が待機する秒数を指定できます。デフォルトでは、ジョブは即座にキューに戻され再試行されます。
php artisan queue:work redis --tries=3 --backoff=3
ジョブごとに例外発生後の待機秒数を設定したい場合は、ジョブクラスに backoff プロパティを定義してください。
/**
* ジョブを再試行するまでの待機秒数。
*
* @var int
*/
public $backoff = 3;
より複雑なロジックで待機時間を決めたい場合は、ジョブクラスに backoff メソッドを定義できます。
/**
* ジョブを再試行するまでの待機秒数を計算する。
*/
public function backoff(): int
{
return 3;
}
backoff メソッドで待機秒数の配列を返すと、「指数的」なバックオフを簡単に設定できます。この例では、1回目の再試行は1秒、2回目は5秒、3回目は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\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Throwable;
class ProcessPodcast implements ShouldQueue
{
use InteractsWithQueue, Queueable, SerializesModels;
/**
* 新しいジョブインスタンスを作成する。
*/
public function __construct(
public Podcast $podcast,
) {}
/**
* ジョブを実行する。
*/
public function handle(AudioProcessor $processor): void
{
// アップロードされたポッドキャストを処理する...
}
/**
* ジョブ失敗時の処理。
*/
public function failed(?Throwable $exception): void
{
// ユーザーへの失敗通知など...
}
}
failed メソッドが呼ばれる前にジョブの新しいインスタンスが生成されるため、handle メソッド内でのクラスプロパティの変更は失われます。
#失敗したジョブの再試行
failed_jobs テーブルに記録されたすべての失敗ジョブを確認するには、queue:failed Artisan コマンドを使用します。
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 を使用している場合は、queue:forget ではなく horizon:forget コマンドを使って失敗ジョブを削除してください。
failed_jobs テーブルからすべての失敗ジョブを削除するには、queue:flush コマンドを使用します。
php artisan queue:flush
#モデルが見つからない場合の無視
ジョブに Eloquent モデルを注入すると、ジョブがキューに入る前にモデルはシリアライズされ、ジョブ処理時にデータベースから再取得されます。しかし、ジョブがワーカーで処理待ちの間にモデルが削除されると、ModelNotFoundException でジョブが失敗することがあります。
利便性のために、ジョブの deleteWhenMissingModels プロパティを true に設定すると、モデルが見つからないジョブを自動的に削除できます。このプロパティを true に設定すると、Laravel は例外を投げずにそのジョブを静かに破棄します:
/**
* モデルが存在しない場合にジョブを削除する。
*
* @var bool
*/
public $deleteWhenMissingModels = true;
#失敗ジョブのプルーニング
queue:prune-failed Artisan コマンドを実行すると、アプリケーションの failed_jobs テーブルのレコードをプルーニング(整理)できます。
php artisan queue:prune-failed
デフォルトでは、24時間以上前の失敗ジョブレコードがすべてプルーニングされます。--hours オプションを指定すると、直近N時間以内に挿入された失敗ジョブのみが保持されます。例えば、以下のコマンドは48時間以上前の失敗ジョブを削除します。
php artisan queue:prune-failed --hours=48
#失敗ジョブを DynamoDB に保存する
Laravel は失敗ジョブのレコードをリレーショナルデータベースの代わりに DynamoDB に保存することもサポートしています。ただし、失敗ジョブを保存する DynamoDB テーブルは手動で作成する必要があります。通常、このテーブル名は failed_jobs ですが、アプリケーションの queue 設定ファイル内の queue.failed.table 設定値に基づいて名前を付けるべきです。
failed_jobs テーブルは、文字列のプライマリパーティションキー application と文字列のプライマリソートキー uuid を持つ必要があります。キーの application 部分には、アプリケーションの app 設定ファイル内の name 設定値で定義されたアプリケーション名が入ります。アプリケーション名が DynamoDB テーブルのキーの一部であるため、同じテーブルを複数の Laravel アプリケーションの失敗したジョブの保存に使えます。
さらに、Laravel アプリケーションが Amazon DynamoDB と通信できるように AWS SDK をインストールしてください:
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',
],
#失敗したジョブの保存を無効化する
queue.failed.driver 設定オプションの値を null に設定すると、失敗したジョブを保存せずに破棄するよう Laravel に指示できます。通常は QUEUE_FAILED_DRIVER 環境変数で設定します。
QUEUE_FAILED_DRIVER=null
#失敗したジョブのイベント
ジョブが失敗したときに呼び出されるイベントリスナーを登録したい場合は、Queue ファサードの failing メソッドを使えます。例えば、Laravel に含まれる AppServiceProvider の boot メソッドからクロージャをこのイベントに紐付けることができます:
<?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 を使う場合は、queue:clear コマンドではなく horizon:clear コマンドを使ってキューからジョブをクリアしてください。
デフォルト接続のデフォルトキューからすべてのジョブを削除したい場合は、queue:clear Artisan コマンドを使えます:
php artisan queue:clear
特定の接続とキューからジョブを削除したい場合は、connection 引数と queue オプションを指定できます:
php artisan queue:clear redis --queue=emails
キューからジョブをクリアする機能は SQS、Redis、データベースのキュードライバーでのみ利用可能です。さらに、SQS のメッセージ削除処理は最大60秒かかるため、キュークリア後60秒以内に SQS キューに送信されたジョブも削除される可能性があります。
#キューの監視
キューに急激にジョブが増えると処理が追いつかず、ジョブの完了まで長時間待つことがあります。Laravel は、キューのジョブ数が指定した閾値を超えたときに通知を送ることができます。
まず、queue:monitor コマンドを 毎分実行 するようスケジュールしてください。このコマンドは監視したいキュー名とジョブ数の閾値を受け取ります:
php artisan queue:monitor redis:default,redis:deployments --max=100
このコマンドをスケジュールするだけでは、キューが過負荷状態であることを通知するアラートは発生しません。ジョブ数が閾値を超えたキューを検出すると、Illuminate\Queue\Events\QueueBusy イベントが発行されます。このイベントをアプリケーションの EventServiceProvider でリッスンして、通知を送ることができます:
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', '[email protected]')
->notify(new QueueHasLongWaitTime(
$event->connection,
$event->queue,
$event->size
));
});
}
#テスト
ジョブをディスパッチするコードをテストするとき、ジョブのコード自体は別で直接テストできるため、Laravel に実際にジョブを実行させたくない場合があります。もちろん、ジョブ自体をテストする場合は、ジョブインスタンスを生成して handle メソッドを直接呼び出せます。
Queue ファサードの fake メソッドを使用すると、ジョブが実際にキューにプッシュされるのを防げます。Queue ファサードの fake メソッドを呼び出した後、アプリケーションがジョブをキューにプッシュしようとしたことをアサートできます:
<?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);
// ジョブが2回プッシュされたことをアサート...
Queue::assertPushed(ShipOrder::class, 2);
// ジョブがプッシュされなかったことをアサート...
Queue::assertNotPushed(AnotherJob::class);
// クロージャがキューにプッシュされたことをアサート...
Queue::assertClosurePushed();
// プッシュされたジョブの合計数をアサート...
Queue::assertCount(3);
}
}
assertPushed または assertNotPushed メソッドにクロージャを渡すと、指定した「真偽テスト」を通過するジョブがプッシュされたかどうかをアサートできます。少なくとも1つのジョブがテストを通過すればアサートは成功します:
Queue::assertPushed(function (ShipOrder $job) use ($order) {
return $job->order->id === $order->id;
});
#一部のジョブだけをフェイクする
特定のジョブだけをフェイクし、他のジョブは通常通り実行したい場合は、フェイクするジョブのクラス名を fake メソッドに渡せます:
public function test_orders_can_be_shipped(): void
{
Queue::fake([
ShipOrder::class,
]);
// 注文の発送処理を実行...
// ジョブが2回プッシュされたことをアサート...
Queue::assertPushed(ShipOrder::class, 2);
}
except メソッドを使うと、指定したジョブ以外はすべてフェイクできます:
Queue::fake()->except([
ShipOrder::class,
]);
#ジョブチェーンのテスト
ジョブチェーンをテストするには、Bus ファサードのフェイク機能を使います。Bus ファサードの assertChained メソッドは、ジョブチェーン がディスパッチされたことをアサートします。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);
#チェーンされたバッチのテスト
ジョブチェーンにバッチジョブが含まれる場合、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,
]);
#ジョブバッチのテスト
Bus ファサードの assertBatched メソッドは、ジョブバッチ がディスパッチされたことをアサートします。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);
#ジョブイベント
Queue ファサード の before と after メソッドを使うと、キューのジョブ処理前後に実行するコールバックを指定できます。これらのコールバックは追加のログ記録やダッシュボード用の統計増分に便利です。通常は サービスプロバイダー の boot メソッドから呼び出します。例えば、Laravel に含まれる AppServiceProvider を使えます:
<?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()
});
}
}
Queue ファサード の looping メソッドを使うと、ワーカーがキューからジョブを取得しようとする前に実行するコールバックを指定できます。例えば、前回失敗したジョブで開いたままのトランザクションをロールバックするクロージャを登録できます:
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
Queue::looping(function () {
while (DB::transactionLevel() > 0) {
DB::rollBack();
}
});