Laravelで非同期処理を行いたいときの選択肢はキューワーカーかプロセスの非同期実行になりがちで、キューワーカーは正攻法ではあるがインフラやアプリケーションコードが大掛かりになるし、プロセスの非同期実行はそれ用のスクリプトを書く必要があり、ちょっとした非同期処理を行いたいときには少し面倒だったりする。
ということで調べたら defer() というヘルパー関数があって、プロダクションで使ってみたらめっちゃ便利に使えたのでメモ
defer() とは?
defer() は Laravel 11.23 から利用できるヘルパー関数で、HTTPレスポンスをクライアントに返した後に任意の処理を実行できる。
具体的には以下のような形で呼び出して、擬似的な非同期処理を実行できる。
defer(function() {
// なんか重い処理
});
return response()->json($data);
擬似的な と表現したのは、別プロセス/スレッドで行うような非同期処理ではなく、HTTPレスポンスを送信したあとに同一プロセス/スレッド内で実行するため。
どう動いているのか
deferで呼ばれた関数は Illuminate\Support\Defer\DeferredCallback でラップされたあとに、 Illuminate\Support\Defer\DeferredCallbackCollection にarray pushされる。
Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks のミドルウェアの terminate() では DeferredCallbackCollection の invokeWhen の処理を呼び出す。
https://github.com/laravel/framework/blob/66d16cb899e007dc8f5f8db33445cfbc9b69a20c/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php#L25-L37
ミドルウェアの terminate() はレスポンスを送信したあとに動くので、擬似的な非同期処理を実現できるという感じ。
HTTPレスポンス送信後に接続が切れて処理が中断されないのか?
という疑問があったので調べてみました。結論、送信後は接続が切れても処理は中断されないみたい。
mod_php
flush() したあとにクライアントから接続が切られても後続の処理は実行されていそう。
クライアントから接続が flush() 前に切られていたら後続処理も実行されないが、そもそもレスポンスも返さないのでまぁしょうがない。
プロダクションのmod_phpな環境で数秒程度かかる処理にdefer()を使っているが問題なく動いている。web記事だとfastcgiでしか使えない?みたいなことが書かれてあったりするけどmod_phpでも普通に動いてます。
fastcgi
fastcgiの場合は fastcgi_finish_request() が呼ばれるが、ドキュメントによるとクライアントにflushしつつ、後続処理を実行できるらしい。
注意
同一プロセス/スレッドで動く制約上、長時間かかるような処理だとプロセス/スレッドを占有し続けてしまうため、バッチやキューワーカーで対応するのが良い。それ以前にロードバランサやWebサーバーのTimeoutに引っかかりそうだけど…。
同様の理由で数秒程度であってもリクエスト数が多い場合でも同じく占有する可能性があるので別の方法を検討したほうが良さそう。ただし、プロセス/スレッド数/インスタンス数を増やすという手もあるのでケースバイケース。
参考URL
https://www.amitmerchant.com/the-magic-behind-laravels-new-defer-helper/