🗂

【Laravel】QueueでBatchを使う方法と通常Jobとの違い

2023/06/16に公開

https://readouble.com/laravel/10.x/ja/queues.html

上記のLaravelのQueueに関するトピック。
LaravelのQueueで通常のJobを実行させる方法については理解しているものとして省略する。
今回は公式ドキュメントのBatchの項目が読んだだけでは、個人的には分かりづらい部分があったので実際に試してみた内容をまとめる。

Jobとの共通点・相違点

共通点の1つ目は、処理を定義するファイルは同じものを使えば良いこと。

Jobを定義する際はphp artisan make:jobを叩くことで生成されるShouldQueueファイルにコンストラクタ、処理内容、振る舞い等を定義する。そして、Batchを定義する際もShouldQueueインターフェースを実装したクラスに記載すれば良い。

共通点の2つ目は、実行する際の要領。

Jobを実行させるには、まずphp artisan queue:workでワーカープロセスを常駐させる。そしてこのワーカープロセスが動いている状態でdispachされたJobが順番に実行されていく。queueの名前についてはdipatchメソッドにonQueueメソッドをチェインすることで任意の名前を指定できる。

Batchについても同様にworkerプロセスを立ち上げた状態で、--queueオプションで指定した名前のBatchを順番に実行していく。dispatchする方法もJobのときと同じで、Batchを定義したクラスを呼び出してdipatchメソッドを実行すれば良い。

相違点は、Batchステータスなどのメタ情報を管理するjob_batchesというテーブルを作成するのが前提となること

php artisan queue:table

php artisan migrate

Jobについては複数のQueueドライバが存在し、データベースに専用テーブルを作ってJobを保持する方法や、Jobを保持せずその場で処理を行なう同期(sync)というパターンもある。

databaseをQueueドライバにする際は上記コマンドでjobsという名前のテーブルが作成されるが、こちらは実行や再試行待ちのJobのみが記録される。失敗したファイルはLaravelプロジェクト作成時にデフォルトでマイグレーションファイルが存在する、failed_jobsテーブルに蓄積されていくが、成功したものは自身で処理を実装しないとjobsテーブルからは消えてしまって残らない。

Queueについてはjob_batchesテーブルを作成して運用することが前提となっている。

php artisan queue:batches-table

php artisan migrate

特に何も設定していなくてもBusファサードのbatchメソッド内にそのBatchで実行したいJobクラスをまとめておくだけで、job_batchesテーブルのレコードは勝手に更新される。

job_batchesテーブルのマイグレーションファイルでスキーマを確認すれば、メタ情報にどんな項目が含まれるか何となく分かるかと。

$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();

最低限の動作を確認する

適当に作成したファイルで最低限の使い方だけ試したので、そのやり方を残しておく。

テーブルの作成

php artisan queue:table
php artisan queue:batches-table
php artisan migrate

Jobファイルの作成

php artisan make:job SampleJob
<?php

namespace App\Jobs;

use Illuminate\Bus\Batchable; //追加
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

class SampleJob implements ShouldQueue
{
    use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels; //Batchableトレイトを追加

    public function __construct()
    {
        //
    }

    public function handle(): void
    {
        Log::info('ジョブを実行しました。');
    }
}

Batchableトレイトを追加しておかないと、呼び出し元Batchに紐づくJobだと認識されずにエラーが発生する。

Batchファイルの作成

php artisan make:job SampleBatch

一旦公式例の通りにBusファサードを使った振る舞いを定義してみる。

    public function handle(): string
    {
        $batch = Bus::batch([
            new SampleJob(),
        ])->then(function (Batch $batch) {
        })->catch(function (Batch $batch, Throwable $e) {
        })->finally(function (Batch $batch) {
        })->onQueue('SampleJob')->dispatch();
        
        return $batch->id;
    }

onQueueメソッドは各Jobクラスではなくbatchメソッドにチェインさせる。
配下に紐づく各Jobについて個別にQueueを指定できない模様。

BatchをQueueに投入する

Batchのディスパッチを実行するartisanコンソールを作成する。

php artisan make:command ExecSampleBatchCommand

    protected $signature = 'app:exec-sample-batch-command';

    public function handle()
    {
        SampleBatch::dispatch()->onQueue('SampleBatch');
    }

.envでQueueドライバをデータベースに設定。

QUEUE_CONNECTION=database

キャッシュクリアを実行すると作成したコンソールを実行し、QueueにBatchを投入する。

php artisan optimize:clear
php artisan app:exec-sample-batch-command

通常のJobと同じようにjobsテーブルにデータが作成される。

ワーカープロセスを立ち上げて処理を開始する

ワーカーの立ち上げるとQueueにスタックされたBatchとJobが実行される。
--queueオプションでBatchとJobの両方のQueue名を指定する必要がある。

php artisan queue:work --queue=SampleBatch,SampleJob
2023-06-16 11:14:19 App\Jobs\SampleBatch .... RUNNING
2023-06-16 11:14:19 App\Jobs\SampleBatch ....209.97ms DONE
2023-06-16 11:14:19 App\Jobs\SampleJob .... RUNNING
2023-06-16 11:14:19 App\Jobs\SampleJob .... 54.06ms DONE

実行結果を確認する

Busファサードが実行された時点でjob_batchesテーブルにレコードが生成される。
先ほど実行したBatchについては、配下のjobが1件で失敗したものがないためfailed_jobsのカウントは0件、failed_job_idsは空の配列となっている。

finished_atについては、配下に未実行のJobが存在すれば記録されずにNULLとなる。
ただし、全てのJobを実行してその中に失敗したものが含まれている場合、Batchは実行したものとみなされる。

Jobファイルで例外を投げて失敗させてみる

先ほどのSampleJobクラスで例外を投げて失敗させて見る。

    public function handle(): void
    {
        throw new \Exception('ジョブに失敗しました。');
    }
php artisan queue:work --queue=SampleBatch,SampleJob
2023-06-16 11:37:04 App\Jobs\SampleBatch ..... RUNNING
2023-06-16 11:37:05 App\Jobs\SampleBatch ..... 205.03ms DONE
2023-06-16 11:37:05 App\Jobs\SampleJob ..... RUNNING
2023-06-16 11:37:05 App\Jobs\SampleJob ..... 43.41ms FAIL

failed_jobsのカウントが1となり、失敗したjobのIDがfailed_job_idsの配列に増えている。
また、配下のJobが失敗するとBatchは自動的に「キャンセル済み」という扱いになり、cancelled_atにタイムスタンプが格納されている。

配下のJobの失敗によって自動的にキャンセル済みにマークしない場合は、以下のようにBusファサードにallowFailuresメソッドをチェインする。

    public function handle(): string
    {
        $batch = Bus::batch([
            new SampleJob(),
        ])->then(function (Batch $batch) {
        })->catch(function (Batch $batch, Throwable $e) {
        })->finally(function (Batch $batch) {
        })->onQueue('SampleJob')->allowFailures()->dispatch();
        
        return $batch->id;
    }

以上

Discussion