Laravelのqueue（キュー）について理解する

:::note
## 概要
キューとは
- 特定の順序でデータを保存および管理するために使用されるコンピューターサイエンスの基本的な概念です
- 先入れ先出し(FIFO)の原則に従っており、キューに最初に追加された要素が最初に削除されます

今回はデータベースのテーブルでキューの仕組みを再現できるデータベースキュードライバを使用します。

Laravel Sailで検証します
リポジトリは[こちら](https://github.com/yamatai12/laravel-queue/blob/main/README.md)です。
エイリアスで`./vendor/bin/sail`を`sail`に省略しています。
```~/.zshrc
alias sail='[ -f sail ] && bash sail || bash vendor/bin/sail'
```
:::

# 背景
業務でLaravelを使っていてqueueを触ったのでまとめます。

# Queue（キュー）とは

https://www.geeksforgeeks.org/queue-data-structure/

特定の順序でデータを保存および管理するために使用されるコンピューターサイエンスの基本的な概念です。

先入れ先出し(FIFO)の原則に従っており、キューに最初に追加された要素が最初に削除されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/b3664b83-d007-b675-8a41-3e6fd56b3409.png)

# Queue（キュー）の基本的な操作

https://www.geeksforgeeks.org/basic-operations-for-queue-in-data-structure/?ref=oin_asr4

- enqueue() - キューに要素を挿入する
- dequeue() - キューから要素を削除する
- peek() または front() - 削除せずに、キューの前ノードで利用可能なデータ要素を取得します
- rear() - この操作は、後端にある要素を削除せずに返します
- isFull() - キューが満杯であるかどうかを検証します
- isEmpty() - キューが空であるかどうかを確認します
- size() - キューのサイズを返します： この操作は、キューのサイズ、つまりキューが含む要素の総数を返します

# LaravelのQueue（キュー）

https://laravel.com/docs/11.x/queues

Laravelではユーザの操作とそれに付随する重いタスク*を分離しユーザに早くレスポンスを返せます。
重いタスクはキューを利用して非同期に処理をします。
*例：アップロードされたCSVファイルの解析と保存、メール送信など

# キューの設定ファイル
queueの設定ファイルは以下です。
```queue-app/config/queue.php
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Default Queue Connection Name
    |--------------------------------------------------------------------------
    |
    | Laravel's queue supports a variety of backends via a single, unified
    | API, giving you convenient access to each backend using identical
    | syntax for each. The default queue connection is defined below.
    |
    */

    'default' => env('QUEUE_CONNECTION', 'database'),

    /*
    |--------------------------------------------------------------------------
    | Queue Connections
    |--------------------------------------------------------------------------
    |
    | Here you may configure the connection options for every queue backend
    | used by your application. An example configuration is provided for
    | each backend supported by Laravel. You're also free to add more.
    |
    | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
    |
    */

    'connections' => [

        'sync' => [
            'driver' => 'sync',
        ],

        'database' => [
            'driver' => 'database',
            'connection' => env('DB_QUEUE_CONNECTION'),
            'table' => env('DB_QUEUE_TABLE', 'jobs'),
            'queue' => env('DB_QUEUE', 'default'),
            'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
            'after_commit' => false,
        ],
```

ここからわかることは以下です。
- 環境変数`QUEUE_CONNECTION`が設定されていない場合、デフォルトで`database`ドライバを使用するようになっています
- キューのジョブは`jobs`テーブルに保存されます（`DB_QUEUE_TABLE`環境変数で変更可能）
- デフォルトのキュー名は`default`です（`DB_QUEUE`環境変数で変更可能）
- ジョブの再試行は90秒後に行われます（`DB_QUEUE_RETRY_AFTER`環境変数で変更可能）

# データベースキュードライバ(Database Queue Driver)
前の章で述べた`database`ドライバのことです。
Laravelのキューシステムで使用できるドライバの1つです。
テーブルの仕組みを使用してキューを再現しています。

今回は最終的にこのような仕組みを作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/7758c2f8-6527-02bf-f4e5-3f3f1e34c2f8.png)

# ジョブを保持するテーブルをつくる
defaultで`0001_01_01_000002_create_jobs_table.php`というマイグレーションファイルが作られています。
```database/migrations/0001_01_01_000002_create_jobs_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('jobs', function (Blueprint $table) {
            $table->id();
            $table->string('queue')->index();
            $table->longText('payload');
            $table->unsignedTinyInteger('attempts');
            $table->unsignedInteger('reserved_at')->nullable();
            $table->unsignedInteger('available_at');
            $table->unsignedInteger('created_at');
        });

        Schema::create('job_batches', function (Blueprint $table) {
            $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();
        });

        Schema::create('failed_jobs', function (Blueprint $table) {
            $table->id();
            $table->string('uuid')->unique();
            $table->text('connection');
            $table->text('queue');
            $table->longText('payload');
            $table->longText('exception');
            $table->timestamp('failed_at')->useCurrent();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('jobs');
        Schema::dropIfExists('job_batches');
        Schema::dropIfExists('failed_jobs');
    }
};

```
これがない場合は、以下でmigrationファイルを作成できます。
```terminal
sail artisan make:queue-table
```

# Jobクラスを作る
Laravelではキューで実行されるタスクの単位をJobと言います。

```terminal
sail artisan make:job Job
```

```queue-app/app/Jobs/Job.php
<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class Job implements ShouldQueue
{
    use Queueable;

    /**
     * Create a new job instance.
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        //Jobが実行されたか確認
        logger('job handled');
    }
}

```

# ジョブをキューにプッシュする

```queue-app/routes/web.php
Route::get('/', function () {
    Job::dispatch();
    return view('welcome');
});
```

dispatchメソッドで
ジョブ自身の`dispatch`メソッドを使ってJobをキューにプッシュすることができます。
ディスパッチメソッドに渡される引数はジョブのコンストラクタに渡されます。

jobsテーブルを見ると、queが`default`のjobがプッシュされていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/07e571ad-a851-7aec-46fb-617471710339.png)

# キューのワーカーを起動する
キューのワーカーを起動すると、キューにプッシュされたジョブを処理することができます。

```terminal
sail artisan queue:work
```

キューにプッシュされたジョブが実行される時にJobクラス内のhandleメソッドが実行されます。
今回だと以下のようにログを出力する処理が実行されます。

```queue-app/app/Jobs/Job.php
/**
 * Execute the job.
*/
    public function handle(): void
    {
        //Jobが実行されたか確認
        logger('job handled');
    }
```

ログが出力されていることが確認できました

```queue-app/storage/logs/laravel.log
[2024-11-09 04:45:39] local.DEBUG: job handled  
```

terminalを見るとジョブがRunning,Doneとなっていて処理中、実行完了したと表示されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/1b4a6a3e-2469-1de1-ae78-86bb389cec4a.png)


テーブルからもレコードが削除されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/83c94923-f785-8f6f-cb0a-affbffd284e3.png)

# キュー属性を指定してキューを区別して管理する

`onQueue`メソッドでキューの属性を指定できます。

```queue-app/routes/web.php
<?php
use App\Jobs\Job;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    Job::dispatch()->onQueue('job');
    return view('welcome');
});
```

テーブルを見るとqueueカラムが`job`になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/731e7a15-c5c1-505d-bd5d-6a3d670eb5dd.png)

この後、以下を実行してもこのジョブは実行されません。
```
sail artisan queue:work
```

このキューワーカーはキュー属性が`default`のキューのジョブを実行します。

`--queue=キューの属性`を指定します。
デフォルトは`--queue=default`になっています。

キュー属性を`job`に指定します。
```
sail artisan queue:work --queue=job
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/1c8dede4-e2c1-23f7-b55a-295210283365.png)

これでキュー属性が`job`のキューがデキューされてからジョブが実行されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/baedf9c1-6b6c-dabd-6c6a-68ccf475170d.png)

ジョブを複数のキューにプッシュすることでジョブの処理に優先順位をつけたり区別できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/7758c2f8-6527-02bf-f4e5-3f3f1e34c2f8.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/e6363b93-a4b1-13a9-22eb-dbc43dee1a06.png)

# ジョブの実装中に詰まったこと
以下は自分がデバッグ中に詰まったことでdocにもありましたので共有します

:::note warn
- キューワーカーは起動した際のアプリケーションの状態をメモリに保存する為、起動中はコードの変更を反映できません
  修正した際はキューワーカーを再起動する必要があります
- この手間を省く方法として`queue:listen`コマンドを実行することもできます
  しかし、`queue:work`コマンドよりも大幅に効率が悪くなります
:::

# 最後に
- queueの概要がつかめました
- 今度は実際にバックグラウンドで実行するような処理を実装してみます
- jobsテーブルにジョブがプッシュされて、ワーカーが検知してデキューされてからジョブが実行されるという一連の処理の流れを確認することができました

# 参考

https://qiita.com/norishio22/items/ba371a5fe56ec50773fb

https://qiita.com/esu_eichi/items/a755b80fec896512eac8

https://books.mdn.co.jp/books/3221303041/

https://www.geeksforgeeks.org/queue-data-structure/

https://laravel.com/docs/11.x/queues