学習備忘ログ

よく使うコードや設定のメモ

【Laravel】バッチ処理の結果をDBに保存する方法

課題

  • バッチ処理が成否を素早く確認したい
  • またサーバが落ちてしまった場合に何が処理できていなかったのかの確認も素早くしたい
  • ログ出力だと確認に時間がかかる。

対処方法

  • バッチ処理結果を保存するDBを作成。今回はJobsLogとする
  • 以下のようなクラスの作成
<?php

class JobsLog extends Eloquent
{
    protected $table = 'jobs_log';

    protected $casts = [
        'status' => 'int'
    ];

    const STATUS_PROCESS = 1;
    const STATUS_SUCCESS = 2;
    const STATUS_FAILED = 0;
    const STATUS_NOT_RUN = 3;
    const UNSENT_MAIL = 0;
    const SENT_MAIL = 1;
    const WEEK_MAP = [
        0 => 'SU',
        1 => 'MO',
        2 => 'TU',
        3 => 'WE',
        4 => 'TH',
        5 => 'FR',
        6 => 'SA',
    ];
    const TYPE_DAILY = 1;
    const TYPE_WEEKLY = 2;
    const TYPE_MONTHLY = 3;
    const TYPE_MINUTELY = 4;

    protected $dates = [
        'start_time',
        'end_time'
    ];

    protected $fillable = [
        'job_name',
        'status',
        'start_time',
        'end_time',
        'message',
        'send_mail',
        'created_at',
    ];

    public static function createJobLog(String $jobName)
    {
        JobsLog::firstOrCreate(
            [
                'job_name' => $jobName,
                'created_at' => Carbon::now()->format('Y-m-d'),
            ],
            [
                'status' => JobsLog::STATUS_PROCESS,
                'start_time' => Carbon::now(),
            ]
        );
    }

    public static function updateJobLog(String $jobName)
    {
        JobsLog::where([
            'job_name' => $jobName,
            'created_at' => Carbon::now()->format('Y-m-d'),
            'status' => JobsLog::STATUS_PROCESS,
        ])->update([
            'status' => JobsLog::STATUS_SUCCESS,
            'end_time' => Carbon::now(),
        ]);
    }

    public static function jobFail(String $jobName, $message)
    {
        JobsLog::where([
            'job_name' => $jobName,
            'created_at' => Carbon::now()->format('Y-m-d'),
            'status' => JobsLog::STATUS_PROCESS,
        ])->update([
            'status' => JobsLog::STATUS_FAILED,
            'end_time' => Carbon::now(),
            'message' => $message,
        ]);
    }

    public static function createLogJobNotRun(String $jobName)
    {
        JobsLog::firstOrCreate(
            [
                'job_name' => $jobName,
                'created_at' => Carbon::now()->format('Y-m-d'),
            ],
            [
                'status' => JobsLog::STATUS_NOT_RUN,
                'send_mail' => JobsLog::SENT_MAIL,
            ]
        );
    }

    //実行するべきバッチ処理のリストを取得する。
    public static function listJobRun()
    {
        $listJob = [];

        $media = (env('GET_MEDIAS')) ? true : false;
        $getLocalPost = (env('GET_LOCAL_POSTS')) ? true : false;
       
        if ($media) {
            $key = (new MediasPreserveScheduler())->getCmd();
            $listJob[$key] = array(
                'status' => $media,
                'type' => JobsLog::TYPE_DAILY,
                'time' => config('schedule.get_medias'),
                'dayOfWeek' => '',
                'dayOfMonth' => ''
            );
        }
        // local post
        if ($getLocalPost) {
            $key = (new LocalPostsPreserveScheduler())->getCmd();
            $listJob[$key] = array(
                'status' => $getLocalPost,
                'type' => JobsLog::TYPE_DAILY,
                'time' => config('schedule.local_posts_get'),
                'dayOfWeek' => '',
                'dayOfMonth' => ''
            );
        }
        
         return $listJob;
    }
}
  • バッチ処理がサーバダウンで処理されていなかった場合にslackに通知するクラス
<?php

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

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Check the to-do list and send a failure message';

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

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        try {
            $runJobs = JobsLog::listJobRun();
            $jobs = JobsLog::whereDate('created_at', '=', date('Y-m-d'));
            $this->failJobs($jobs);
            if (!empty($runJobs)) {
                $this->ranJobs($runJobs, $jobs);
            }
            Log::info('jobs:check_log: success');
        } catch (\Throwable $e) {
            echo $e->getMessage();
            Log::error($e->getMessage());
        }
    }
    
    public function failJobs($jobs)
    {
        $jobs = (clone $jobs)->where('status', JobsLog::STATUS_FAILED)->get();
        foreach ($jobs as $job) {
            if ($job->send_mail == JobsLog::UNSENT_MAIL) {
                $msg = 'Command: '. $job->job_name .' ('. env('APP_URL') . ') 》not run ' . "\n" . $job->message;
                Notification::route('slack', env('SLACK_WEBHOOK_URL'))
                    ->notify(new UpdateStoreByUsingCsvFileNotification($msg));
                Log::error($msg);
                $job->update([
                    'send_mail' => JobsLog::SENT_MAIL,
                ]);
            }
        }
    }

    public function ranJobs($runJobs, $jobs)
    {
        foreach ($runJobs as $key => $runJob) {
            // run daily
            if ($runJob['type'] == JobsLog::TYPE_DAILY || $runJob['type'] == JobsLog::TYPE_MINUTELY) {
                $timeRunSub2 = Carbon::parse($runJob['time'])->addMinutes(2);
                $timeCurrent = Carbon::now();
                if ($timeRunSub2->greaterThan($timeCurrent)) {
                    continue;
                }
                if ((clone $jobs)->where('job_name', $key)->exists()) {
                    continue;
                }
                $msg = 'Command: '. $key .' ('. env('APP_URL') . ') 》not run';
                Notification::route('slack', env('SLACK_WEBHOOK_URL'))
                    ->notify(new UpdateStoreByUsingCsvFileNotification($msg));
                Log::error($msg);
                JobsLog::createLogJobNotRun($key);
            }
            // run weekly
            if ($runJob['type'] == JobsLog::TYPE_WEEKLY) {
                $dayOfTheWeek = Carbon::now()->dayOfWeek;
                if (JobsLog::WEEK_MAP[$dayOfTheWeek] != $runJob['dayOfWeek']) {
                    continue;
                }
                $timeRunSub2 = Carbon::parse($runJob['time'])->addMinutes(2);
                $timeCurrent = Carbon::now();
                if ($timeRunSub2->greaterThan($timeCurrent)) {
                    continue;
                }
                if ((clone $jobs)->where('job_name', $key)->exists()) {
                    continue;
                }
                $msg = 'Command: '. $key .' ('. env('APP_URL') . ') 》not run';
                Notification::route('slack', env('SLACK_WEBHOOK_URL'))
                    ->notify(new UpdateStoreByUsingCsvFileNotification($msg));
                Log::error($msg);
                JobsLog::createLogJobNotRun($key);
            }
        }
    }
}
  • Kernel.php
<?php

        if (env('GET_MEDIAS')) {
            $cmd = (new MediasPreserveScheduler())->getCmd();
            $schedule->command('medias:get')->dailyAt(config('schedule.get_medias'))
//                     ->everyMinute()
                ->sendOutputTo(storage_path('logs/' . strval(date('Y-m-d')) . '_medias' . '.log'))
                ->before(function () use ($cmd) {
                    JobsLog::createJobLog($cmd); //スタート時点のログを追加
                })
                ->after(function () use ($cmd) {
                    JobsLog::updateJobLog($cmd);//終了のログを追加
                })->runInBackground();
        }

        if (env('IKKATSU_JOB_CHECK')) {
            $schedule->command('jobs:check_log')
                ->everyFiveMinutes();
        }
  • MediasPreserveScheduler.phpの処理
<?php

class MediasPreserveScheduler extends Command
{

    /**
     * The name and signature of the console command.
     * @var string
     */
    protected $signature = 'medias:get';

    /**
     * The console command description.
     * @var string
     */
    protected $description = 'get medias information per month';

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

    /**
     * Execute the console command.
     * @return mixed
     * @throws GuzzleException
     */
    public function handle()
    {
        try {
              //処理
        } catch (Throwable $e) {
            report($e);
            JobsLog::jobFail($this->getCmd(), $e->getMessage()); //処理が失敗した場合のログを追加
        }
    }

    public function getCmd()
    {
        return $this->signature;
    }
}