Laravelのキューを使って非同期処理を実装(Job/Queue)




はじめに

今回はLaravelで非同期処理を実装してみました。
ジョブ、キュー、ワーカー、キューイングといった単語が出てきます。


目標

適当な非同期処理をジョブ、キュー、ワーカーを使って実装し、
Laravelでキューイングの流れを把握します。


背景

仕事で、定期実行でないバックグラウンド処理がしたくて、キューイングにたどり着きました。
サイトからCSV(1万行以上)をアップロードして、
その個々のデータに対して追加の処理をする状況でした。
アップロードしながら追加で処理してDBへ保存…
なんて、ユーザーからしたらたまったもんじゃないですからね💦


開発環境

  • Windows 10
  • Laravel 8.21.x
  • PHP 8.x
  • Laravelプロジェクト作成済み




準備する

ジョブやキューを使用するにあたって追加の操作が必要です。
今回、キューはデータベースを使うことにしました。


ジョブ用のテーブルの作成

php artisan queue:table
php artisan queue:failed-table  // これいらないかも
php artisan migrate

jobsfailed_jobsというテーブルができたことを確認します。


.env修正

QUEUE_CONNECTION=database
または
QUEUE_DRIVER=database

Laravel 8.x なんですが、バージョンによって違うかもです。
ほかにredisbeanstalkdsqssyncが選択できます。 デフォルトはsync(同期処理)です。


config/queue.php修正(確認)

'default' => env('QUEUE_CONNECTION', 'database'),  // <= ここも変えた

'connections' => [ ..... ],

'failed' => [ ..... ],

connectionsでそれぞれのキューの設定ができます。
今はそのままで行きます。




ジョブを作成する

php artisan make:job SampleJob

上記のコマンドでapp/Jobs/SampleJob.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 SampleJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    // ジョブのリトライ回数
    public $tries = 3;

    // 引数の入れ物
    private $arr

    public function __construct(array $arr)
    {
        $this->arr    = $arr;

        /** 
         * 以下のようにして遅延時間を設定できる。
         * dispatch時に指定が無ければ10秒後に実行される。
         * デフォルトは即時実行(delay(0))
         */
        $this->delay(10);
    }

    public function handle()
    {

        // ここに非同期処理の内容をかく。

        Log::info("SampleJob実行", $this->arr);
    }
}

Logファサードは第二引数に配列が渡せます。(もちろん文字列も)
配列で渡すと、JSON形式でログに書き込まれます。




トリガーを作成する

ジョブをキューに追加(エンキュー)するだけの簡単なAPIを用意しました。
php artisan make:controller SampleController --apiREST APIを作成し、以下のようにしました。

use App\Jobs\SampleJob;

// ... 省略 ...

class SampleController extends Controller

    public function index()
    {
        $arr = ['message' => 'てすとだよ', 'name' => 'キューイング'];
        SampleJob::dispatch($arr)
            ->delay(5)
            ->onQueue('sample');
        return $arr;
    }

}


ルーティングroutes/api.php

Route::apiResource('sample', 'SampleController');

にしました。
これでGET /api/sampleすると、ジョブがキューに追加されます。


  • dispatch( )

エンキューしてます。引数もここで渡します。

  • delay( int )

何秒後にジョブを実行するのか指定できます。
未指定の場合、ジョブのコンストラクタで$this->delay(10)としたので、10秒後に実行されます。
つまり疑似的にデフォルト遅延時間が設定でき、上書きもできます。
(今回は「5秒後」に上書きしてます)

  • onQueue( string )

キューの名前を指定します。
データベースのjobsテーブルのカラム定義を見てみるとそれっぽいのがあります。
未指定の場合、defaultという名前のキューに追加されます。




実行する

ジョブをキューに追加

ポストマンなどでAPIを実行GET /api/sampleしてDBのjobsテーブルを見ると
queue = 'sample'のジョブが1つあることが確認できると思います。

これでOKですが、
このままだとジョブはずっと待機状態のまま処理されません。

ワーカーを起動

コマンドプロンプト等で以下のコマンドを打ち、
ジョブを処理するワーカーを起動します。

php artisan queue:work --queue=sample

オプション--queueでキュー名を指定します。(onQueueの値)
未指定の場合は--queue=defaultになります。
ワーカーを起動してからAPIを実行しても大丈夫です。

  • 複数のキューを指定する場合
php artisan queue:work --queue=sample,default,other

のようにカンマで区切ります。


  • ジョブSampleJob.phpを変更した場合

ワーカーを再起動し、変更を読み込ませる必要があります。
開発中は面倒だと思うので、以下のように起動すれば再起動しなくてもよくなります。
PCによっては負荷がかかりすぎる場合もあるので、様子を見ながら。

php artisan queue:listen --queue=sample,default,other


  • ワーカーを裏で常駐させる場合

またやります。


動作を確認する

ワーカーを起動したコマンドプロンプトに何か出てると思います。

成功した場合

f:id:ryzenblog:20210116205145p:plain
ジョブ成功

storage/logs/laravel.logあたりにログが書き込まれています。

失敗した場合

f:id:ryzenblog:20210116205218p:plain
ジョブ失敗

※画像は一部加工しています


3回やっても失敗したのでFailedになりました。
リトライ回数はジョブ作成時に
public $tries = 3;
としましたね。
失敗したジョブはfailed_jobsテーブルに入ります。
さっき急いで画像撮ってきたからちょっと恥ずかしい…笑


失敗したジョブの再実行(おまけ)

失敗したジョブのIDを指定してコマンドで再度キューに突っ込み、
ワーカーに処理させます。

  1. 失敗したジョブの確認
php artisan queue:failed

+-----------------------------+------------+--------+--------------------+-------------+
| ID                          | Connection | Queue  | Class              | Failed At   |
+-----------------------------+------------+--------+--------------------+-------------+
| 18516a07-4a14-43fa-XXXX-... | database   | sample | App\Jobs\SampleJob | 失敗した日時 |
+-----------------------------+------------+--------+--------------------+-------------+


  1. キューに追加
# 選んで追加
artisan queue:retry 18516a07-4a14-43fa-XXXX-...

# 全部追加
artisan queue:retry all

The failed job [18516a07-4a14-43fa-XXXX-...] has been pushed back onto the queue!

この時指定するジョブIDはfailed_jobsテーブルのuuidカラムの値です。
そしてジョブがfailed_jobsからjobsに移動してます。

ちなみに、以下をすると逆のことができます。

# 失敗ジョブを選んで消す
php artisan queue:forget {JOB_ID(uuid)}

# 失敗ジョブを全部消す
php artisan queue:flush


  1. ワーカー起動

起動してあればそのままで、
コマンドプロンプトに何か表示されてると思います。




まとめ

Laravelでジョブ、キュー、ワーカーを使って非同期処理するときの流れは

1. ジョブ用のテーブルをマイグレーション
2. .envconfig/queue.phpで設定まわりを整え
3. app\Jobsにジョブを作成し
4. ジョブをキューに追加するトリガーを用意し
5. ワーカーを起動

でした。
Redisを使った場合や、ワーカーを裏で常駐させる方法も、やりたいところです。
動かしながらやったからか、すんなり理解できてよかったです。



最後までお読みいただき、ありがとうございました👋🏻
誤字脱字は見つけ次第修正します。 こんなに長くなるはずじゃなかった…

Laravelの変数を外部JSに渡す方法(PHP-Vars-To-Js-Transformer)

  
  

はじめに

目標

PHP-Vars-To-Js-Transformerを使って、
Laravelの変数を、別ファイルのJavascriptに渡して処理します。
  
  

前提

ダイジェストでお送りいたします。
申し訳ありませんが、ベストプラクティスあれこれは考慮してません🙇🏻‍♀️
また、環境によって不要なステップの記載も飛ばしてます💦
  
  

背景

Google Maps PlatformのAPIを使う場面がありました。
マップを表示してピンを立てるときに、DBのデータも合わせて使いたかったので、今回は上記のライブラリを使用しました。
フロントエンドの知識が乏しいと、なかなかつらいですね。
めもらんだむを残しておきます。
    
 
 

開発環境

  
  

導入

インストールしてデフォルト設定を適用

  
# インストール
composer require laracasts/utilities

# 設定ファイルが作られる → config/javascript.php
php artisan vendor:publish --provider="Laracasts\Utilities\JavaScript\JavaScriptServiceProvider"

  
  

注意

  • Laravel5.5以降は、サービスプロバイダに登録しません。(今回のケース)
  • Laravel4は ~1.0 、Laravel5は ~2.0 を指定してインストールします。

PHP-Vars-To-Js-Transformer公式を参照してください。
  
  
  

使い方

成果物の全体像

こんな状態になります。

プロジェクトルート/
  ├ app/
  │  └ Http/
  │    └ Controllers/
  │      └ SampleController.php
  │
  ├ config/
  │  └ javascript.php
  │
  ├ public/
  │  └ top/
  │    └ sample.js
  │
  ├ routes/
  │  └ wep.php
  │
  └ resources/
     └ views/
       └ top/
         └ sample.blade.php

  
  

コントローラ

app/Http/Controllers/SampleController.php

  
use JavaScript;

class SampleController extends Controller {
  public function geolocation() {
    $model = app("App\Models\User");
    $data = $model::select(["id", "name"])->get()->toArray();
    JavaScript::put(["laravel_data" => $data]);

    return view('top.sample');  // => このViewを設定ファイルconfig/javascript.phpに追加する
  }
}

  
  

ルーティング

routes/wep.php

Route::get('/test', 'SampleController@geolocation');

   
  

設定ファイル

config/javascript.php

return [
    'bind_js_vars_to_this_view' => ['footer', 'top.sample'],  // putしたいViewを追加
    // ... 省略
];

  
この設定ファイルを更新したら、キャッシュをクリアします。
(ついでにほかのキャッシュもまとめてクリアしてます。)

php artisan view:clear
php artisan config:clear

     
  

ビュー

resources/views/top/sample.blade.php

<html>
  <head> ... </head>
  <body>
    <script type="text/javascript" src="{{asset('top/sample.js')}}"></script>
    <script async defer src="https://GoogleMapsPlatformAPI&callback=topSample"></script>
    <div id="googlemap"></div>
  </body>
</html>

   
  

外部JSファイル

public/top/sample.js
ここでいろいろ使うことができます。

function topSample() {
	console.log(laravel_data);  // OK
	// ... いろんな処理
}

  
  

確認

ページにアクセスすればJSが走ってくれると思います。
  
   
   

まとめ

キャッシュの関係で、ルーティングやJSの変更が読み込まれないときがあります。
適宜キャッシュクリアが必要なんですが、シェルでまとめておくと便利でした。
ビューを触るならLaravel-Mixでまとめて管理すると良い、みたいな記事もたくさん見かけました。また今度ですね。

ちなみに、GoogleMapを表示するときは
CSSでwidth:600px , height:600pxみたいにちゃんと指定します。
  

   
初投稿お読みいただき、ありがとうございました👋🏻
拙い部分はご容赦くださればと思います💦
誤字脱字は後日修正しますね。
 
  
    

参考リンク

PHP-Vars-To-Js-Transformer