Laravel Job Queueで バッチ運用した話

# やりたい事
- 毎日、スクレイピング処理を実行
- スクレイピングした結果をGoogleスプレッドシートに書き込む
- 大量の処理が走るので、PHPメモリエラーになる可能性が高い
- スプレッドシートの結果を毎日、バックアップする
- スプレッドシートの結果を毎日、メールで送る

# やった事
- ①スケジューラの設定
- ②バッチコマンドクラスの作成
- ③ジョブキューの仕組みを実装
- ④GASでスプレッドシート書き込みの処理を実装
- ⑤supervisorの設定

# 0.各キーワード説明
## Job Queueとは？
ジョブキューとはジョブをキューで管理するものです。
キューとはFIFO(First In First Out)を実現するデータ構造です。
キューに登録されたモノは、キューに登録した順に処理されます。
<img width="740" alt="スクリーンショット 2019-12-24 21.53.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85181/ab01d48e-ad5a-34ee-b19c-71f0ac3ee19e.png">

参考：http://tech.voyagegroup.com/archives/495474.html

# 全体構成
<img width="975" alt="スクリーンショット 2019-12-24 21.52.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85181/732c833c-fbc0-8d25-e228-b8f03219b09b.png">

# ①スケジューラの設定
Kernel.phpに朝9時でバッチを動くように設定   

```php
<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        //
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        // バッチを実行
        $schedule->command('batch:daily')->dailyAt('09:00');
    }

    /**
     * Register the commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        $this->load(__DIR__.'/Commands');

        require base_path('routes/console.php');
    }
}

```

   

# ②バッチコマンドクラスの作成
コマンドでの処理
- 本日分のジョブキューを詰める
- 前日分のジョブの実行結果をメールで送る

```php

<?php

namespace App\Console\Commands;

use App\Entities\JobStatus;
use App\Entities\Project;
use App\Jobs\ProcessGoogleSearch;
use App\Mail\DailyBatchResultMail;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Mail;

class dailyBatchCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'batch:daily';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '1日1回実行されるバッチ用コマンド';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     * @throws \Exception
     */
    public function handle()
    {
        $error = $this->SendJobStatusesByEmail();

        if ($error !== null) {
            throw new \Exception($error);
        }

        $error = $this->updateIsActive();

        if ($error !== null) {
            throw new \Exception($error);
        }

        $this->refreshQueue();
        $projects = Project::where('is_active','=', 1)->get();
        $this->registeredJobStatuses($projects);
        $this->pushQueue($projects);
    }
    
    // 以下、省略

}

```
# ③ジョブキューの仕組みを実装
※サンプルコードなので、細かい部分は省略。    

```php
<?php

namespace App\Jobs;

use App\Entities\JobStatus;
use App\Entities\Project;
use App\Enums\JobState;
use App\Services\CustomSearchService;
use App\Services\SearchService;
use App\Services\SpreadsheetService;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Log;

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

    /**
     * 最大試行回数
     *
     * @var int
     */
    public $tries = 0;

    /**
     * 詳細検索するかどうか(TRUE)
     */
    const IS_DETAIL_SEARCH_TRUE = 1;

    /**
     * 詳細検索するかどうか(FALSE)
     */
    const IS_DETAIL_SEARCH_FALSE = 0;

    private $project;
    private $jobStatus;
    private $searchService;
    private $customSearchService;
    private $spreadsheetService;

    /**
     * Create a new job instance.
     *
     * @param Project $project
     * @param JobStatus $jobStatus
     */
    public function __construct(Project $project, JobStatus $jobStatus)
    {
        $this->project = $project;
        $this->jobStatus = $jobStatus;

        $this->searchService = new SearchService();
        $this->customSearchService = new CustomSearchService();
        $this->spreadsheetService = new SpreadsheetService();
    }

    /**
     * Execute the job.
     *
     * @return void
     * @throws \Exception
     */
    public function handle()
    {
        // 一部、処理を省略
        // JobStatusの更新
        $jobStatus = $this->jobStatus;
        $jobStatus->status = \App\Enums\JobState::RUNNING;
        $jobStatus->started_at = Carbon::now();
        $jobStatus->save();

        $parameter = [];

        $keyword = $jobStatus->keyword;

        if (!empty($keyword)) {
            $searchCount = $this->project->search_count;

            // 検索を実行
            // サービスクラスの具体的な処理は省略
            $parameter['searchResults'][] = $this->customSearchService
                ->execute($keyword, $searchCount, self::IS_DETAIL_SEARCH_TRUE, $jobStatus);

            sleep(mt_rand(config('variables.google_search_min_wait'), config('variables.google_search_max_wait')));
        }

        // GAS側で使う
        $parameter['sheetName'] = $this->project->name;
        $parameter['searchCount'] = $searchCount;

        // スプレッドシート書き出し
        [$spreadSheetUrl, $error] = $this->spreadsheetService->outputSpreadsheet($parameter);

        // JobStatusの更新
        if (empty($error)) {
            $jobStatus->status = JobState::SUCCESS;
            $jobStatus->spreadsheet_url = $spreadSheetUrl;
        } else {
            $jobStatus->status = JobState::FAILED;
        }
        $jobStatus->finished_at = Carbon::now();
        $jobStatus->save();
    }

    /**
     * 失敗したジョブの処理
     *
     * @param \Exception $exception
     * @return void
     */
    public function failed(\Exception $exception)
    {
        // JobStatusの更新
        $jobStatus = JobStatus::where('project_id', $this->project->id)->first();
        $jobStatus->status = \App\Enums\JobState::FAILED;
        $jobStatus->finished_at = Carbon::now();
        $jobStatus->save();

        // 失敗の通知をユーザーへ送るなど…
        \Log::error($exception->getMessage());
    }
    
}


```

# ④GASでスプレッドシート書き込みの処理を実装
- PHPからパラメータを取得 
<img width="640" alt="スクリーンショット 2019-12-24 23.38.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85181/0899dc95-a59e-8bd6-f831-f6cdbc830c30.png">
  

- Excelファイルを作成して、メール送信
<img width="782" alt="スクリーンショット 2019-12-24 22.26.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85181/2bfa8c86-d703-7d8c-f057-af327c94becd.png">

- Excelファイルを作成して、Googleドライブにバックアップ
<img width="745" alt="スクリーンショット 2019-12-24 22.27.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85181/711e0248-a2d2-2981-6487-703fbe0f8318.png">

# ⑤supervisorの設定
<img width="505" alt="スクリーンショット 2019-12-24 22.28.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85181/17bd47f9-bc69-7735-4864-b8f381af9ddf.png">
<img width="223" alt="スクリーンショット 2019-12-24 22.28.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85181/7df8bc84-111e-ff99-64c8-9087ac835ecd.png">

# まとめ
- 大量の処理が走るような機能があった際に、Job Queueを使うとPHPメモリエラーを防げる可能性がある。
- メールの大量送信やスクレイピング、高速でAPIを叩くような処理には向いてそう。

# 参考記事
- https://speakerdeck.com/bumptakayuki/laravel-job-queuede-batutiyun-yong-sitahua   
自分の発表資料も参考により具体的に書いてみました。