Larave 7.0でキュー・ジョブ・スケジューラー触ってみる。取得したツイートが存在するか毎夜毎夜チェックするジョブとスケジューラー。
技術スタック Laravel

Larave 7.0でキュー・ジョブ・スケジューラー触ってみる。取得したツイートが存在するか毎夜毎夜チェックするジョブとスケジューラー。

2020.07.11

こんにちはjunです。仕事でLaravelを触ることになり必死にドキュメントを読んだりして対策をしています。フルスタックフレームワークと言われているほど、Laravelは機能が豊富なので想像していた物が作れて感動しています。

そこで今回はドキュメントをみてもイマイチパッとしない、ジョブ(Job)、キュー(Queue)、スケジューラー(Scheduler)を触りながら機能を確かめていきたいと思います。

作りたい物

Laravelを用いて外部APIと通信して、取得したデータをDBに入れたりブラウザ上で操作するアプリケーションを作成していたときにある問題にぶつかりました。TwitterAPIを用いてツイートの情報を保存していたのですが、非公開もしくは削除されたため表示できないツイートがありました。

削除されたツイートを破棄するシステムを構築する必要があります。システム自体は簡単でDBから保存されたツイート一つ一つのIDを取得してTwitterAPIをを投げて、ツイートが存在するかの結果を確認すればいいだけです。スクレイピングするような感じです。しかしこの方法には課題があります。

保存されたツイートが大量にある場合、Twitterのサーバーに大量のリクエストを短時間に送信してしまい、Dosとなってしまいます。そのため各リクエスト毎に最低1秒程でも緩急を置く必要があります。その場合、その処理が終了する時間は保存されたツイート量によってはかなり長くなります。

大体、このような長くなりがちな処理はキューとして、サーバー上で非同期に行わせるのがベストです。そしてLarvelではジョブを簡単に実装できる「タスクスケジュール」という物があります。

今回は24時になると保存したツイートの存在を確認してくれる自動実行ジョブを構築したいと思います。

タスクスケージューラーを理解する

公式のドキュメントを見てみましょう。おおよそですが実装の手順としては

  1. ジョブの定義(実際の処理ロジック)をapp/job/ 配下に作成する。
  2. 行うジョブやその実行時間は app/console/kernel.php に記述。
  3. cronに毎分Larvelスケジューラーを実行させるコードを記述。

こんな感じです。細かく解説していきます。

ジョブを作成する

キューを使えるようにする

まずはLaracelでジョブを実行できる環境を整えます。「キュー」に関するドキュメントを見ると以下のコマンドを唱えてジョブを記録するテーブルを作成します。

php artisan queue:table

php artisan migrate

このコマンドを唱えると「jobs」「failed_jobs」というテーブルが作成されます。ジョブとキューはセットで使われることが多いので上記のコマンドを唱えておきましょう

ジョブファイルを作成する

ジョブファイルは app/job 配下に作成します。しかし最初はこのjobディレクトリは存在しないので以下のコマンドを唱えて、ジョブファイルのテンプレートとディレクトリを作ってもらいます。

php artisan make:job YOUR_JOB_NAME

「YOUR_JOB_NAME」の部分にジョブの名前を入力してください。他のmakeコマンドと同じ感じですね。このコマンドを唱えるとjobディレクトリとjobクラスが書かれたファイルが配下に作成されます。そのファイルにジョブの実行処理を書いていきます。

TwitterにAPIを送るジョブを記述

今回はジョブ名を「CheckNotFoundUrls」として作成します。ジョブファイルは作成するとまず下記のように書かれています。

<?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;

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

    public function __construct()
    {
        //
    }

    public function handle()
    {
        //ここに処理を記述
    }
}

handle()に処理内容を記述します。ジョブを実行するときはこのクラスを読み込み、ジョブインスタンスを作成し、dispatch()するとジョブが実行されます。まあ、とりあえずTwitterにAPIを飛ばすコードを書きます。コードは載せますが詳細の説明は省きます。

<?php
namespace App\Jobs;

//Twitter API ライブラリ
use Abraham\TwitterOAuth\TwitterOAuth;

use App\Models\Tweets; // ツイート情報が格納されたテーブルに接続するモデル
use App\Models\JobReport; // 自作ジョブの結果を記録しておくテーブル

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

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

    public function __construct()
    {
        //
    }

    public function handle()
    {
        try{
            // ツイート情報があるテーブルからツイート情報(JSON)と必要なレコードを取得
            $datas = Tweets::select('tweet_rawdata','id','user_id')->get();
            $deletedTweetOwner = array();
    
            foreach($datas as $val){
                //'tweet_rawdata'カラムはjsonなので一旦decode。
                //jsonからid_strというツイートIDを取得
                $decodeTweetData = json_decode($val->tweet_rawdata);
                $tweet_id = $decodeTweetData->id_str;

                //APIに接続するための準備(ライブラリ)をして、APIを送信
                $connection = new TwitterOAuth(env('TW_CONSUMER_KEY'), env('TW_CONSUMER_KEY_SECRET'), env('TW_ACCESS_TOKEN'), env('TW_ACCESS_TOKEN_SECRET'));
                $connection->setTimeouts(10, 15);
                $content = $connection->get("statuses/show",['id'=>intval($tweet_id)]);
    
                //請求したツイートIDのツイートが存在しないという結果ならばレコードのIDを $deletedTweetOwnerにユーザーIDを記録
                if(property_exists($content,'errors')){
                    switch($content->errors[0]->code){
                        case 144:
                            Tweets::destroy($val->id);
                            array_push($deletedTweetOwner,$val->id);
                        break;
                    }
                }
                //Dosにならないように1秒止める
                sleep(1.0);
            }
  
        }catch(\Exception $e){
            //処理が失敗したら管理者用のレポートにエラ〜メッセージを記録する。
            JobReport::reportException(0,$e->getMessage());
        }
    }
}

ちなみにTwitterへのAPIアクセスには Abraham\TwitterOAuth というTwttiter公式が紹介するサードパーティライブラリを使用しています。composerで読み込んでuseすればすぐに使え、上記のようにouthインスタンスを作成するだけで使えます。簡単なのでLarvelでTwitterAPIを使う時などにおすすめです。

とりあえずこれでジョブは完成しました。試しにチェックをしてみたいですが、なんとかしてこのジョブを実行する必要があります。しかしLaravelには定義したジョブを実行するコマンドが見当たりません。(もしかしたら見落としてるだけかも)

コマンドでジョブを叩いて実行したいので、以下のカスタムコマンドを自作します。

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class DispatchJob extends Command
{
    protected $signature = 'job:dispatch {job}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Dispatch job';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        $class = '\\App\\Jobs\\' . $this->argument('job');
        dispatch(new $class());
    }
}

このstack overflowの記事を引用しています。ありがとうございます。

このファイルをapp/Console/Commands 配下に作成します。Console/Commands 配下ではphp artisan を用いたコマンドを自分で作成することができます。ここでは php artisan job:dispatch JOB_NAME と唱えればJOB_NAMEで書かれたジョブクラスのhandle()を実行してくれます。

これを用いて以下のように唱えます。

php artisan job:dispatch CheckNotFoundUrls

するとprintなどを一時的につけて、結果を見ると無事に404のツイートの数が検知され、削除を実行してくれました。

ジョブが実行される時間を定める

それでは次に上で作成したジョブをスケジューラーに登録します。スケジューラーで実行させるジョブは app/Console/Kernel.php にジョブインスタンスを作って実行します。

app/Console/Kernel.php には schedule() というメソッドがあり php artisan run:schedule を実行すると schedule() が実行されます。

先ほどのジョブクラスをこのKernel.phpで使えるようにして、ジョブインスタンスを作成します。

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Jobs\CheckNotFoundUrls; //ここで先ほど作成したジョブクラス
use DateTime;

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)
    {
        //実行するjobをキューに投入する。そして実行時間を設定。
        $schedule->job(new CheckNotFoundUrls(),'checkUrl')->diary()
    }
}

$schedule->()の第一引数にジョブインスタンス、第二引数にキュー名を入れます。キューというのは実行登録したジョブを一時的にためておく場所のことです。私もよくわかっていませんが、ジョブの実行数を制限したり、どの順に実行をしていくかなども決められるそうです。とりあえず今回は404チェック用のジョブキューとして checkUrl としておきます。

そして行末にdiary()などがありますが、公式ドキュメントの通り実行する時間を指定できます。cron通りの時刻設定もできますし、1日ごと、週末ごと、月初に1回などよくある時間設定はメソッドを指定すれば設定できます。

上記のコードではdiary()なので毎日0時になるとジョブが実行されます。

cronを設定する

ジョブを定義し、その時間も定めたのでもう自動で実行されそうな気もしますが、これだけでは動きません。指定の時間になったら特定のコマンドつまりphp artisan schedule:run を唱えてphpを動かさないといけません。

ここはサーバー(Linux)のcronというものを用いて、常にphp artisan schedule:run を唱えるように設定します。cronとは時間設定基づいてコマンドを定期的に唱えてくれるUNIX系のOSに入っているプログラムです。

cronに以下の設定を記述します。

~ $ crontab -e #これでcronの設定ファイルを編集できます。
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

行の最初にある(* * * * *)は時間の設定をしています。この書き方の場合は毎分実行するようになります。つまり毎分Laravelのスケジューラーを叩いているだけなんです。

cd /path-to-your-project はLaravelプロジェクトのルートへ移動するという意味です。そしてプロジェクトルートでスケジュールを実行します。

以上でスケジューラー実装完了

以上でスケジューラーの実装が完了しました。今回のような404を毎日チェックしたり、定期的に時間を設定して行う処理などをアプリ内で行う機能を実装する時に便利です。

ユーザーごとに実行するかどうか、いつ実行するかの情報を格納しておくことで一般ユーザーが定期的な処理を行う処理を設定することもできます。

Copyright © 2021 jun. All rights reserved.