dockerでLaravelキュー環境を整える

## 概要
Laravelでキュー処理を使うことがあり、開発環境としてdockerでローカル環境を整備する。
その際に調べた内容をメモします。

## ファイル構成
以下のようなファイル構成で構築してみる。
※ ソースコード：[Github-reflet/laravel5.6](https://github.com/reflet/laravel5.6)

```
├ docker                           ... docker関連ファイルを配置するフォルダ
│ ├ php
│ │  ├ .dockerignore
│ │  ├ Dockerfile                   ... PHP環境を整備します
│ │  ├ php.ini
│ │  └ www.conf
│ │
│ └ supervisor
│    ├ application.conf             ... Laravelワーカーを定義します
│    ├ Dockerfile                   ... PHPコンテナのイメージをベースにカスタマイズする
│    └ supervisord.conf             ... supervisorの設定ファイル
│
├ src                              ... Laravelのソースコードを配置するフォルダ
│ ├ app
│ └ config
│    └ logging.php                 ... job実行ログを追加する
│
└ docker-compose.yml               ... PHPコンテナやsupervisorコンテナを定義する
```

## Supervisorコンテナ
supervisorをインストールしたコンテナを用意します。
※ `PHPコンテナのdockerイメージ(local/php:7.3)`をベースにして作ります

```Dockerfile:./docker/supervisor/Dockerfile
FROM my-laravel5.6/php:7.3

USER root
RUN apt-get install -y supervisor

COPY ./docker/supervisor/supervisord.conf /etc/supervisor/supervisord.conf
COPY ./docker/supervisor/application.conf /etc/supervisor/conf.d/application.conf

CMD ["/usr/bin/supervisord"]
```
設定ファイルはこんな感じにしました。

```conf:./docker/supervisor/supervisord.conf
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700

[supervisord]
nodaemon=true
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
childlogdir=/var/log/supervisor

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock

[include]
files = /etc/supervisor/conf.d/*.conf
```
Laravelのワーカーはこんな感じで設定してみました。

```conf:./docker/supervisor/application.conf
; [program:php-fpm]
; command = /usr/local/bin/docker-php-entrypoint php-fpm -D
; autostart = true

[program:laravel-sync]
command=php /var/www/www.example.com/artisan queue:work --tries=1 --queue=laravel-sync
process_name=%(program_name)s_%(process_num)02d
numprocs=1
autostart=true
autorestart=true
user=root
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
; stdout_logfile=/var/log/supervisor/laravel-sync.log

[program:laravel-async]
command=php /var/www/www.example.com/artisan queue:work --tries=1 --queue=laravel-async
process_name=%(program_name)s_%(process_num)02d
numprocs=5
autostart=true
autorestart=true
user=root
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
; stdout_logfile=/var/log/supervisor/laravel-async.log

; [program:crond]
; command = /usr/sbin/crond
; user = root
; autostart = true
; stdout_logfile=/dev/stdout
; stdout_logfile_maxbytes=0
; stderr_logfile=/dev/stderr
; stderr_logfile_maxbytes=0
```

## docker-compose.ymlファイル
下記のようにPHPコンテナの後に起動するように定義しました。

```yaml:docker-compose.yml
version: '3'
services:
  httpd:
    image: my-laravel5.6/apache:2.4
    ...

  php:
    image: my-laravel5.6/php:7.3
    build:
      context: ./
      dockerfile: ./docker/php/Dockerfile
    ports:
      - '9000:9000'
    volumes:
      - ./src:/var/www/www.example.com:cached

  supervisor:
    image: my-laravel5.6/supervisor:7.3
    build:
      context: ./
      dockerfile: ./docker/supervisor/Dockerfile
    volumes:
      - ./src:/var/www/www.example.com:cached
    depends_on:
      - php
```

## サーバ起動
dockerイメージをビルドして、dockerコンテナを起動します。

```bash
# dockerイメージ作成
$ docker-compose build

# dockerコンテナ起動
$ docker-compose up -d
```

## ログ出力
ジョブ実行ログを出力したいので、ロギングを追加する。

```php:./src/config/logging.php
'channels' => [
    ...

    // laravel job log.
    'job' => [
        'driver'     => 'daily',
        'path'       => storage_path('logs/job.log'),
        'level'      => 'info',
        'permission' => 0666,
        'days'       => 7,
    ],

    ...
]
```

## DDL - Data Definition Language
データベースをQueueで使う場合は、テーブルが必要となるので、下記を実行する。

```bash
$ docker-compose exec php php artisan queue:table
$ docker-compose exec php php artisan queue:failed-table
$ docker-compose exec php php artisan migrate
```
## 環境設定 (env)
今回は、データベースをコネクションとして使うので以下のようにします。

```env:src/.env
QUEUE_CONNECTION=database
```

## テスト準備
動作確認するため、コマンドを１つとジョブを７つ作成してみる。

```bash
$ docker-compose exec php php artisan make:command StartJobsCommand
$ docker-compose exec php php artisan make:job SampleJobA
$ docker-compose exec php php artisan make:job SampleJobB
$ docker-compose exec php php artisan make:job SampleJobC
$ docker-compose exec php php artisan make:job SampleJobD
$ docker-compose exec php php artisan make:job SampleJobE
$ docker-compose exec php php artisan make:job SampleJobF
$ docker-compose exec php php artisan make:job SampleJobG
```
ジョブを実行するコマンドはこのようにしました。
※ ワーカー(`laravel-sync`): プロセス1つ → **順番に実行される**
※ ワーカー(`laravel-async`): プロセス5つ → 5つまで **並列実行される**

```php:./app/Console/Commands/StartJobsCommand.php
<?php

namespace App\Console\Commands;

use Carbon\Carbon;
use Illuminate\Console\Command;
use App\Jobs\SampleJobA;
use App\Jobs\SampleJobB;
use App\Jobs\SampleJobC;
use App\Jobs\SampleJobD;
use App\Jobs\SampleJobE;
use App\Jobs\SampleJobF;
use App\Jobs\SampleJobG;

class StartJobsCommand extends Command
{
    protected $signature = 'job:start';

    ...

    public function handle()
    {
        $this->info(sprintf('[%s] job:start command start.', Carbon::now()->format('Y-m-d H:i:s')));

        // SampleJobAを同期(laravel-sync)で実行する
        SampleJobA::dispatch()->onQueue('laravel-sync')->delay(Carbon::now());
        $this->info(sprintf('[%s] job:start command - sample job A.', Carbon::now()->format('Y-m-d H:i:s')));

        // SampleJobBを同期(laravel-sync)で実行する
        SampleJobB::dispatch()->onQueue('laravel-sync')->delay(Carbon::now());
        $this->info(sprintf('[%s] job:start command - sample job B.', Carbon::now()->format('Y-m-d H:i:s')));

        // SampleJobCを非同期(laravel-async)で実行する
        SampleJobC::dispatch()->onQueue('laravel-async')->delay(Carbon::now());
        $this->info(sprintf('[%s] job:start command - sample job C.', Carbon::now()->format('Y-m-d H:i:s')));

        ...

        // SampleJobGを非同期(laravel-async)に向けて発行
        SampleJobG::dispatch()->onQueue('laravel-async')->delay(Carbon::now());
        $this->info(sprintf('[%s] job:start command - sample job G.', Carbon::now()->format('Y-m-d H:i:s')));
    }
}
```
ジョブについては、以下のように記載しています。

```php:./src/app/Jobs/SampleJobA.php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Log;

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

    public function __construct()
    {
        //
    }

    public function handle()
    {
        $Log = Log::channel('job');
        $Log->info(self::class . ' start.');
        for ($i = 1; $i <= 5; $i++) {
            $Log->info(self::class . '::' . $i);
            sleep(1);
        }
        $Log->info(self::class . ' complete.');
    }
}
```

## 動作確認
supervisorの状態を確認してみる。

```bash
$ docker-compose exec supervisor service supervisor status
supervisord is running
```
ワーカーの起動状況を確認してみる （laravel-sync: 1つ, laravel-async: ５つ）

```bash
$ docker-compose exec supervisor supervisorctl status
laravel-async:laravel-async_00   RUNNING   pid 12, uptime 0:20:12
laravel-async:laravel-async_01   RUNNING   pid 11, uptime 0:20:12
laravel-async:laravel-async_02   RUNNING   pid 10, uptime 0:20:12
laravel-async:laravel-async_03   RUNNING   pid 9, uptime 0:20:12
laravel-async:laravel-async_04   RUNNING   pid 13, uptime 0:20:12
laravel-sync:laravel-sync_00     RUNNING   pid 14, uptime 0:20:12
```
ジョブを実行してみる。

```bash
$ docker-compose exec php php artisan job:start
[2021-06-25 17:52:42] job:start command start.
[2021-06-25 17:52:42] job:start command - sample job A.  # ← 直列実行
[2021-06-25 17:52:42] job:start command - sample job B.  # ← 直列実行 (Aの後に実行される)
[2021-06-25 17:52:42] job:start command - sample job C.  # ← 並列実行
[2021-06-25 17:52:42] job:start command - sample job D.  # ← 並列実行
[2021-06-25 17:52:42] job:start command - sample job E.  # ← 並列実行
[2021-06-25 17:52:42] job:start command - sample job F.  # ← 並列実行
[2021-06-25 17:52:42] job:start command - sample job G.  # ← 並列実行
```
dockerのログを見てみる。

```bash
$ docker-compose logs supervisor
supervisor_1  | [2021-06-25 16:05:19][3] Processing: App\Jobs\SampleJobC # ← 並列(ジョブC)が開始される
supervisor_1  | [2021-06-25 16:05:20][1] Processing: App\Jobs\SampleJobA # ← 直列(ジョブA)が開始される
supervisor_1  | [2021-06-25 16:05:20][4] Processing: App\Jobs\SampleJobD # ← 並列(ジョブD)が開始される
supervisor_1  | [2021-06-25 16:06:20][4] Processed:  App\Jobs\SampleJobD # ← 並列(ジョブD)が終了した
supervisor_1  | [2021-06-25 16:06:20][3] Processed:  App\Jobs\SampleJobC # ← 並列(ジョブC)が終了した
supervisor_1  | [2021-06-25 16:06:20][1] Processed:  App\Jobs\SampleJobA # ← 直列(ジョブA)が終了した
supervisor_1  | [2021-06-25 16:06:20][2] Processing: App\Jobs\SampleJobB # ← 直列(ジョブB)が開始される
supervisor_1  | [2021-06-25 16:07:20][2] Processed:  App\Jobs\SampleJobB # ← 直列(ジョブB)が終了した
```
jobテーブルを見てみる。
![jobs-table.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142279/e0d87cf1-b8d8-36db-7f85-ec0e5cae49a9.png)

Laravelのログを見てみる。

```log:./src/storage/logs/job-2021-06-29.log
[2021-06-29 16:06:12] local.INFO: App\Jobs\SampleJobA start.  
[2021-06-29 16:06:15] local.INFO: App\Jobs\SampleJobC start.  
[2021-06-29 16:06:15] local.INFO: App\Jobs\SampleJobD start.  
[2021-06-29 16:06:15] local.INFO: App\Jobs\SampleJobE start.  
[2021-06-29 16:06:15] local.INFO: App\Jobs\SampleJobF start.  
[2021-06-29 16:06:15] local.INFO: App\Jobs\SampleJobG start.   

...

[2021-06-29 16:06:18] local.INFO: App\Jobs\SampleJobA complete.  
[2021-06-29 16:06:18] local.INFO: App\Jobs\SampleJobC complete.  
[2021-06-29 16:06:18] local.INFO: App\Jobs\SampleJobD complete.  
[2021-06-29 16:06:18] local.INFO: App\Jobs\SampleJobE complete.  
[2021-06-29 16:06:18] local.INFO: App\Jobs\SampleJobF complete.  
[2021-06-29 16:06:18] local.INFO: App\Jobs\SampleJobG complete.  

[2021-06-29 16:06:18] local.INFO: App\Jobs\SampleJobB start.  
[2021-06-29 16:06:18] local.INFO: App\Jobs\SampleJobB::1   
[2021-06-29 16:06:19] local.INFO: App\Jobs\SampleJobB::2  
[2021-06-29 16:06:20] local.INFO: App\Jobs\SampleJobB::3  
[2021-06-29 16:06:21] local.INFO: App\Jobs\SampleJobB::4  
[2021-06-29 16:06:22] local.INFO: App\Jobs\SampleJobB::5  
[2021-06-29 16:06:23] local.INFO: App\Jobs\SampleJobB complete.  
```

問題ないようです。

## 参考資料
* [Laravel 5.6 キュー](https://readouble.com/laravel/5.6/ja/queues.html)
* [Supervisor を Docker で使う](https://docs.docker.jp/engine/admin/using_supervisord.html)
* [slashfan/docker-supervisor-php-workers](https://github.com/slashfan/docker-supervisor-php-workers)
* [Dockerでsupervisordを使うときにログを標準出力する](https://qiita.com/sugiki/items/86515393664cb5ce16ff)
* [Laravelでキューを使った並列処理](https://qiita.com/Nishi53454367/items/871614343ea77020db17)
* [Supervisorの基本コマンド](https://qiita.com/mokkos/items/473978ef0699df3173f5)
* [Dockerでsupervisorを使う時によくハマる点まとめ](https://techracho.bpsinc.jp/morimorihoge/2017_06_05/40936)
* [LaravelでRedisとSupervisorを導入してQueue/Jobを扱う【docker-compose】](https://tech.windii.jp/backend/laravel/laravel-redis-supervisor-queue)
* [AWS Elastic Beanstalk使ってバックグラウンド処理のサーバ構築【Laravel】](https://tech.macloud.jp/entry/advent_calendar_2019_12_17)
* [Laravel の Queue で非同期処理を実装する](https://www.1x1.jp/blog/2014/08/laravel-queue-guide.html)
* [「キュー」は何しにLaravelへ？](https://kore1server.com/285/%E3%80%8C%E3%82%AD%E3%83%A5%E3%83%BC%E3%80%8D%E3%81%AF%E4%BD%95%E3%81%97%E3%81%ABLaravel%E3%81%B8%EF%BC%9F)

以上