【Laravel】ジョブとキューの基本

## キューとは

ジョブを貯めておく箱。基本的には先入先出法(FIFO)で処理を実行する。

キューの候補として、MySQLといったRDBやRedisのようなNoSQL、Amazon SQSが候補として挙げられる。

## ジョブとは

キューに格納する処理そのものこと。ユーザが応答を待たず、バックグラウンドプロセスで動かしたいもの。

## 環境

Laravel 9.35.0

PHP 8.1.2

## 事前準備

今回は、データベースのテーブルをキューとして検証してみます。

以下のコマンドでキュー用のテーブルを作成します。

```shell
$ php artisan queue:table
$ php artisan migrate
```

.envファイルの以下の箇所をsyncからdatabaseに変更します。

```php
QUEUE_CONNECTION=database
```

## サンプル実装

例えば、ユーザ作成処理の後処理としてメールで完了通知を送るケースがあったとします。

まず、以下のコマンドでジョブクラスを作成します。

```shell
$ php artisan make:job SendMail
```

作成されたジョブクラスのhandleメソッドの中を以下のように修正します。

メール通知ジョブ

```php:App/Jobs/SendMail.php
<?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;
use Illuminate\Support\Facades\Log;

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

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

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        Log::debug('非同期処理を開始します。SendMail');
        // 時間がかかる処理を想定
        for ($i = 1; $i <= 5; $i++) {
            sleep(1);
            Log::debug('SendMail - 処理中['.$i.'s]');
        }

        Log::debug('非同期処理が完了しました。SendMail');
    }
}
```

ユーザ作成処理の中で、ジョブを溜めるようにdispatchメソッドを使用します。

```php
/**
 * @param  UserCreateInput $input
 * @return UserCreateOutput
 */
public function create(UserCreateInput $input): UserCreateOutput
{
    $user  = new User();
    $model = $user->create([
        'cognito_sub' => $input->cognito_sub,
        'name'        => $input->name,
        'profile'     => '',
        'icon'        => '',
    ]);
    // 完了通知を非同期処理で実施する
    SendMail::dispatch();

    return new UserCreateOutput(
        $model->id,
        $model->cognito_sub,
        $model->name,
        $model->profile,
        $model->icon,
    );
}
```

キューに溜まったジョブを監視処理するためにワーカーを起動させます。

以下のコマンドでワーカーを起動します。

```shell
$ php artisan queue:work
```

コンテナ内で実行中のプロセスを確認するには以下のコマンドを使用します。

```shell
$ docker container top {my-container}
```

プロセスを確認すると、ワーカーのプロセスが1件実行中であることがわかります。

| UID | PID | PPID | C | STIME | TTY | TIME | CMD |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|1000|58066|3782|2|23:11| ?| 00:00:01|php artisan queue:work
|   |   |   |   |   |   |   |   |


この状態でユーザ作成処理を呼び出した時、以下の流れで処理が行われます。

- ユーザを新規作成する
- キューにメール通知ジョブが登録される
- ワーカーが動いているので、非同期でメール通知ジョブのhanldeメソッドに記載した処理が実行される。
- メール通知ジョブの処理中に、ユーザ作成処理のレスポンスを返す

## 並列処理のサンプル

複数のジョブで並列処理を行うには、同一のワーカーを複数起動させる必要があります。

例えば、ユーザ作成処理の後処理としてメールとLINEで完了通知を送るケースがあったとします。

LINE通知ジョブ

```php:App/Jobs/SendLine.php
<?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;
use Illuminate\Support\Facades\Log;

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

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

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        Log::debug('非同期処理を開始します。SendLine');
        // 時間がかかる処理を想定
        for ($i = 1; $i <= 5; $i++) {
            sleep(1);
            Log::debug('SendLine - 処理中['.$i.'s]');
        }

        Log::debug('非同期処理が完了しました。SendLine');
    }
}
```

ユーザ作成処理

```php
/**
 * @param  UserCreateInput $input
 * @return UserCreateOutput
 */
public function create(UserCreateInput $input): UserCreateOutput
{
    $user  = new User();
    $model = $user->create([
        'cognito_sub' => $input->cognito_sub,
        'name'        => $input->name,
        'profile'     => '',
        'icon'        => '',
    ]);
    // 完了通知（メールおよびLINE）を非同期処理で実施する
    SendMail::dispatch();
    SendLine::dispatch();

    return new UserCreateOutput(
        $model->id,
        $model->cognito_sub,
        $model->name,
        $model->profile,
        $model->icon,
    );
}
```

別々のターミナルでワーカーを起動します。

```shell
$ php artisan queue:work
$ php artisan queue:work
```

プロセスを確認するとワーカーが2つ実行中であることがわかります。

| UID | PID | PPID | C | STIME | TTY | TIME | CMD |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|1000|61733|3782|1|00:28| ?| 00:00:01|php artisan queue:work
|1000|61762|3782|1|00:28| ?| 00:00:01|php artisan queue:work
|   |   |   |   |   |   |   |   |




この状態で、ユーザ作成処理を呼び出します。

laravel.logを確認すると以下の通りです。

```text:laravel.log
[2023-01-11 09:45:22] local.DEBUG: 非同期処理を開始します。SendMail  
[2023-01-11 09:45:23] local.DEBUG: 非同期処理を開始します。SendLine  
[2023-01-11 09:45:23] local.DEBUG: SendMail - 処理中[1s]  
[2023-01-11 09:45:24] local.DEBUG: SendLine - 処理中[1s]  
[2023-01-11 09:45:24] local.DEBUG: SendMail - 処理中[2s]  
[2023-01-11 09:45:25] local.DEBUG: SendLine - 処理中[2s]  
[2023-01-11 09:45:25] local.DEBUG: SendMail - 処理中[3s]  
[2023-01-11 09:45:26] local.DEBUG: SendLine - 処理中[3s]  
[2023-01-11 09:45:26] local.DEBUG: SendMail - 処理中[4s]  
[2023-01-11 09:45:27] local.DEBUG: SendLine - 処理中[4s]  
[2023-01-11 09:45:27] local.DEBUG: SendMail - 処理中[5s]  
[2023-01-11 09:45:27] local.DEBUG: 非同期処理が完了しました。SendMail  
[2023-01-11 09:45:28] local.DEBUG: SendLine - 処理中[5s]  
[2023-01-11 09:45:28] local.DEBUG: 非同期処理が完了しました。SendLine
```

メール通知処理とLINE通知処理が並列で実行されていることがわかります。

## ワーカーのデメリット

キューを処理するためには

```shell
$ php artisan queue:work
```

を実行しなければならない。

本番で使用する場合は、queue:workプロセスが停止されないようにする必要がある。

また、ジョブクラスの処理内容を変更した場合も、上記のコマンドを実行しなければならない。