初めてLaravelのJob Queueを触ったことをまとめてみる

## はじめに
 **Qiita Engineer Festa 2024** が開催されているせっかくの機会なので、
最近の開発の際に初めて触った、Laravelの**Job Queue**について
まとめてみようと思います！！！

## 前提条件
MacBook Pro Apple M2
PHP 8.0.27
Laravel 9.52.15

## そもそもJob Queueとは
>Webアプリケーションの構築中に、アップロードされたCSVファイルの解析や保存など、通常のWebリクエスト中に実行するのでは時間がかかりすぎるタスクが発生する場合があります。幸運なことに、Laravelを使用すると、バックグラウンドで処理したい仕事をキューへ投入するジョブクラスが簡単に作成できます。時間のかかるタスクをキューに移動することで、アプリケーションはWebリクエストに驚異的な速度でレスポンスし、顧客により良いユーザーエクスペリエンスを提供できます。

公式ドキュメントにはこのように書いてあります:point_up:

今回業務で扱う前までは
会員登録後の確認メール送信なんかで使ってるんだっけ？？くらいの認識でした。

**非同期で処理を実行したい**ときにはとっておきの機能です！！

そこで今回、私がJob Queueを採用する主な理由としては、
OpenAIに対してAPI通信を行うため、**レスポンス待ちによってユーザー体験が下がることを防ぐ**ためです。


```
+------------------+
| User Request     |　クライアントサイドからリクエストを飛ばす
+------------------+
         |
         v
+---------------------+
| Application Server  |
+---------------------+
         | 1. Jobの生成
         | 2. JobをQueueに追加
         v
+-------------------+      +------------------+
| Job Queue         | ---> | Client Receives  |　クライアントサイドには
+-------------------+      | Immediate        |　即座にレスポンスを返す
         |                 | Response         |
         v                 +------------------+
+-------------+
| Worker      |　QueueからJobを取り出し、処理を実行
+-------------+
         |
         v
+------------------+
| Success or Error | 成功時：メール送信や通知、DBへの保存など・・
+------------------+ 失敗時：failed_jobsテーブルへレコード追加
         |
         v
+----------------------+
| User Notification    |
+----------------------+

```

## 実装手順
### Queueテーブルの作成
まず、jobを保持するための`Queue用のテーブル`と
jobの実行が失敗した際にレコードが追加される`failed_jobsテーブル`を作成します。
```
php artisan queue:table
php artisan queue:failed-table

php artisan migrate
```


<table>
  <caption><strong>jobsテーブル</strong></caption>
  <tr>
    <th>#</th> <th>column_name</th> <th>備考</th>
  </tr>
  <tr>
    <td>1</td> <td>id</td>　<td>ユニークID</td>
  </tr>
  <tr>
    <td>2</td> <td>queue</td>　<td>ジョブが属するキューの名前</td>
  </tr>
  <tr>
    <td>3</td> <td>payload</td>　<td>ジョブの実行に必要なデータを含むJSON形式の文字列<br>dispatchメソッドの引数の部分が入る</td>
  </tr>
  <tr>
    <td>4</td> <td>attempt</td>　<td>ジョブの実行試行回数</td>
  </tr>
  <tr>
    <td>5</td> <td>reserved_at</td>　<td>処理が予約された日時のタイムスタンプ</td>
  </tr>
  <tr>
    <td>6</td> <td>available_at</td>　<td>ジョブが次に実行可能となる日時スタンプ</td>
  </tr>
  <tr>
    <td>7</td> <td>created_at</td>　<td>作成日時</td>
  </tr>
</table>


### envファイルの変更
`QUEUE_CONNECTION`をsyncからdatabaseに変更することでqueueテーブルにjobが積まれるようになります。
```
QUEUE_CONNECTION=database
```
### Jobクラスの作成
アプリケーションのすべてのqueueableジョブは、`app/Jobs`ディレクトリに保存します。
このディレクトリがない場合でもコマンドを実行すると自動で作成されます。
```
php artisan make:job HogeJob
```

### 非同期処理の追加（dispatchメソッド）
Jobクラスを作成すると、**dispatchメソッドによってJobをディスパッチすることができます**。非同期処理を差し込みたい箇所に処理を追加します。

※dispatchメソッドの引数はコンストラクタに渡されます。

▼ 例：HogeController storeメソッド内
``` HogeController.php
<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use App\Jobs\HogeJob;
use App\Models\Hoge;
use Illuminate\Http\Request;
 
class HogeController extends Controller
{
    /**
     * Store a new hogehoge.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $hoge = Hoge::create(/* ... */);
 
        // ...
 
        HogeJob::dispatch($hoge); //Jobクラス名（HogeJob）
    }
}

```

``` php 
    // Queueに積まれてから10分後に実行という設定も可能
    HogeJob::dispatch($hoge)->delay(now()->addMinutes(10));
```
### Jobクラスに処理を追加
Jobクラス内では、**Job実行に対して幾つかの条件（プロパティ）を設定**することができます。

`Jobが失敗した際に再試行する最大回数`、`タイムアウトになるまでの最大秒数`などなど
[公式ドキュメント](https://laravel.com/docs/9.x/queues#timeout) にたくさん書いてあります！
``` HogeJob.php
namespace App\Jobs;

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

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

    /**
     * ジョブが失敗した場合に再試行する最大回数を指定
     *
     * @var int
     */
    public $tries = 3;

    /**
     * ジョブがタイムアウトになるまでの最大秒数を指定
     *
     * @var int
     */
    public $timeout = 120;

    /**
     * hogeインスタンス
     *
     * @var \App\Models\Hoge
     */
    protected $hoge;

    /**
     * 新しいジョブインスタンスの生成
     *
     * @return void
     */
    public function __construct()
    {
        $this->hoge = $hoge;
    }

    /**
     * ジョブの実行
     *
     * @return void
     */
    public function handle()
    {
        // 実行内容を記述
    }

    /**
     * ジョブの失敗を処理
     *
     * @param Throwable $exception
     * @return void
     */
    public function failed(Throwable $exception): void
    {
        // ジョブ失敗時にslackで通知
        $this->sendFailedJobSlackNotification($exception); //例
    }
}
```

### Jobの実行
下記コマンドを叩くことで、Queueに積まれたJobを実行することができます。※1

```
php artisan queue:work
```

## タスクスケジュールで定期実行
今回ステージングおよび本番環境では、起動コマンドをタスクスケジュールに設定して運用します。

毎分起動コマンドを実行し、`withoutOverlapping`をつけ重複実行しないよう設定します。

※本番環境では、[Supervisor](https://readouble.com/laravel/11.x/ja/queues.html#supervisor-configuration)を使用して自動で再起動する仕組みを構築することが、ドキュメントでは薦められています。
``` app/Kernel.php

protected function schedule(Schedule $schedule)
{
    // 毎分実行、重複起動無し おすすめ理由自動生成するためのjobを実行
    $schedule->command('queue:work')
        ->everyMinute()
        ->withoutOverlapping();

    // 5分ごと実行、重複起動無し おすすめ理由自動生成失敗したjobを再実行
    $schedule->command('queue:retry all')
        ->everyFiveMinutes()
        ->withoutOverlapping();
}
```

## その他実行コマンド
```
// すべてのワーカを再起動
php artisan queue:restart

// 再起動せずコード反映　※2
php artisan queue:listen

// 失敗したジョブをすべて再試行
php artisan queue:retry all

// failed_jobsのレコードを全て削除
php artisan queue:flush
```

:::note warn
※1 キューワーカは存続期間の長いプロセスであるため、再起動しない限り、コードの変更が反映されません！
コード修正した場合は、再起動コマンドを実行してください。

※2 listenコマンドはコードの反映を即座に反映しますが、workコマンドより大幅にパフォーマンスが下がります。
:::


## 最後に
今回のJob Queueの処理は最低限のコードで書いたので、
改めて調べてみるともっと色々なことができそうだし、カスタマイズができそうです。

サービスプロバイダなんかも使うと、複数のQueueを使い分けることができたり、
ジョブの処理前、後、失敗時を切り分けてロジックを組めたりできそう！

最後までお読みいただきありがとうございました！


## 参考記事
https://readouble.com/laravel/8.x/ja/queues.html

https://laravel.com/docs/9.x/queues#timeout

https://qiita.com/KyuKyu/items/1316ab89703519427c56

https://zenn.dev/hata8341/scraps/fcad0dba91ca73