はじめに
Webアプリケーションが提供する機能の中で、通常のWebリクエスト中に実行するのでは時間がかかりすぎる処理が発生する場合があります。そこで対象の処理が完了してから、リクエスト元のユーザーに対してレスポンスを返していた場合、ユーザーエクスペリエンスを大きく損ねてしまいます。
しかし時間がかかりすぎる処理をキューに移動することで、ユーザーに対しより良いユーザーエクスペリエンスを提供することが可能です。
Queue
以下のようにキューを利用する場合、時間がかかる処理を同期的に待たずにレスポンスを返すことができます。
Queueを使った処理の実行
今回はLaravel9でQueueを使い、上記で説明したような時間がかかる処理をバックグラウンドで実行する手順を説明しようと思います。
例としてサンプルコードでは、ユーザーが予約サイトで予約のキャンセルした際に、お店の担当者に予約がキャンセルされた旨をメールで送信する処理をバックグラウンドで実行していきます。
- ユーザーから予約キャンセルのリクエストを受け取る
- ビジネスロジックに基づき予約状態をキャンセルに変更する
- 時間がかかる処理(Job)をQueueに移動する
- メールの送信完了を待たずに予約のキャンセルを行なったユーザーに対し、レスポンスを返す
上記で3
の処理をキューに移動することで、メールの送信完了を待たずに、ユーザーへ予約のキャンセルが完了したことを通知することができます。
ここでお気づきだと思うのですが、上記の3
の処理を同期処理で実行しても機能自体を作成することはできます。つまり、予約のキャンセルを行なった際に、お店の担当者にキャンセルメールの送信処理が完了したら、ユーザーに対しキャンセル完了の通知を行うということです。
しかし本来ユーザーは予約自体がキャンセル済みになれば、キャンセルが完了した事を通知してほしいはずです。
なのでここではそのメール送信処理をキューに移動し、ユーザーには予約のキャンセルが完了した段階で、その旨を通知するということをやっていきます。
環境
- php8.1.3
- Laravel9.2.0
- mysql5.7
やってみる
Laravelではdatabase・Redis・Amazon SQSなど様々なキュードライバを使って機能を実装することが可能ですが、今回はdatabaseをキュードライバとして使用していきます。
キュードライバの事前準備
database
キュードライバを使用する為に、以下のコマンドを実行します。
php artisan queue:table
php artisan migrate
また.env
ファイルのQUEUE_CONNECTION
という環境変数にdatabase
を指定します。
QUEUE_CONNECTION=database
ジョブクラスの生成
ジョブをキューに投入するクラスであるジョブクラスを生成します。
php artisan make:job SendReserveCancelMail
ジョブクラスは、ジョブがキューにより処理されるときに通常呼び出すhandle
メソッドのみを持ちます。
以下の例では、予約先のお店の担当者に予約がキャンセルになった旨を通知するメールを送信する処理を実行しています。ジョブクラスのコンストラクタで実際にメールを送信する際に必要なモデルを取得し、利用しています。また、クラス内の変数をprivate
で宣言すると、テスト実行時に失敗してしまうので気をつけてください。
メールクラスの生成は本記事に関連が薄いので割愛します。確認したい方はこちらを参照してください
<?php
namespace App\Jobs;
use App\Mail\ReserveCanceled;
use App\Models\User;
use App\Models\Operator;
use App\Models\Reserve;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendReserveCancelMail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $user;
public $reserve;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(User $user, Operator $operator, Reserve $reserve)
{
$this->user = $user;
$this->operator = $operator;
$this->reserve = $reserve;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
Mail::to($this->operator->email)
->send(new ReserveCanceled($this->user, $this->reserve));
}
}
ジョブのディスパッチ
ジョブクラスを作成したら、Controllerなどでジョブをディスパッチします。以下の例では、ユーザーの予約をキャンセルする処理が完了したらジョブをディスパッチしています。ジョブは、ジョブ自体でdispatch
メソッドを使用してディスパッチできます。dispatch
メソッドに渡した引数は、ジョブのコンストラクタに渡されます。
ここではジョブクラスのコンストラクタで引数として宣言していた3つのモデルをdispatch
メソッドに渡しています。
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Requests\StoreCancelRequest;
use App\Jobs\SendReserveCancelMail;
use App\Models\User;
use App\Models\Operator;
use App\Models\Reserve;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
final class CancelController extends Controller
{
/**
* cancel reserve
*
* @param \App\Models\Reserve $reserve
* @param \App\Http\Requests\StoreCancelRequest $request
* @return \Illuminate\Http\JsonResponse
*/
protected function store(Reserve $reserve, StoreCancelRequest $request): JsonResponse
{
// 予約をキャンセルする処理だったりのビジネスロジックを処理します
$operator = Operator::where(...)->first();
SendReserveCancelMail::dispatch(Auth::user(), $operator, $reserve);
return response()->json(Response::HTTP_OK);
}
}
キューワーカの実行
ここまででジョブを実行するプログラムは書けましたが、ジョブを実行するにはキューワーカを実行させる必要があります。
Laravelは、キューワーカを開始し、キューに投入された新しいジョブを処理するArtisanコマンドを用意しています。queue:work
Artisanコマンドを使用してワーカを実行します。
php artisan queue:work
これで、ここまでで用意したCancelController
のstore
メソッドが実行されると、キューによってメールが送信される処理が実行されるはずです。
キューワーカに関しては本番環境などで、queue:work
プロセスをバックグラウンドで永続的に実行し続けるには、Supervisor
などのプロセスモニタを使用して、キューワーカの実行が停止しないようにする必要があります。
またドキュメントに以下のような記載がありますが、キューで処理されるプログラムを修正した際には、必ず再起動してください。これに気づかずに時間を無駄にすると、とても辛い気持ちになります。
キューワーカは長期間有効なプロセスであり、起動した時点のアプリケーションの状態をメモリに保存することを忘れないでください。その結果、起動後にコードベースの変更に気付くことはありません。したがって、デプロイメントプロセス中で、必ずキューワーカを再起動してください。さらに、アプリケーションによって作成または変更された静的状態は、ジョブ間で自動的にリセットされないことに注意してください。