Ruby
Rails
AdventCalendar
rack

rack_after_reply でレスポンス後に処理を行う

More than 5 years have passed since last update.

Rails Advent Calendar 10 日目です。

Rails で、S3 へのアップロードや画像処理、メール送信など、時間のかかる処理をする際に、レスポンスをすぐに返し、処理自体はレスポンス後に行いたい場合があります。

rack_after_reply (https://github.com/oggy/rack_after_reply) は rack サーバを拡張する gem で、env["rack_after_reply.callbacks"] に Proc オブジェクトを追加することで、レスポンス後に実行してくれます。

# Gemfile

gem "rack_after_reply"

# app/controllers/photos_controller.rb

def create
@photo = Photo.new(params[:photo])
if @photo.valid?
env["rack_after_reply.callbacks"] << lambda {
# 時間のかかる処理
sleep 10
@photo.save!
}
# 202 Accepted を返す
format.html { render status: :accepted }
else
format.html { render action: :new }
end
end

上の例では、即座にレスポンスが返り、(およそ) 10 秒後にレコードが追加されます。

ちなみに、この例では HTTP ステータスコードとして、201 や 302 ではなく、202 (Accepted) を返していて、これは「リクエストは受理したけど、まだ処理が完了していない」ことを示すステータスコードです。

本来、202 を返すときは「リソースの現在の状態」と、「最新の状態をモニタできる URL、またはリソースの作成にかかる見込み時間」を付与すべきですが、ここではなにもしてません。


他のライブラリとの比較

こういった用途では resque や delayed_job が有名ですが、これらはいづれも、データストアにジョブ登録 -> 別プロセスで処理という方法をとるため、ジョブ実行に必要なすべての情報をデータストアにシリアライズして保存しなければなりません。

この処理は、特にファイルのアップロード (Rails では一時ファイルになる) が絡むと面倒で、carrierwave などのライブラリを利用しているとさらに複雑になります。

rack_after_reply は、だいたい下記のような特徴をもつので、実装の容易さ・簡潔さなら rack_after_reply、分散のしやすさ・汎用性・疎結合性なら resque / delayed_job といった感じでしょうか。


メリット


  • 遅延処理しない場合に比べ、ほとんどコードを変える必要が無い

  • 別のプロセスやデータストアが必要ないため構成がシンプル

  • メモリ上のオブジェクトや一時ファイルをそのまま処理できる


デメリット


  • 時間のかかる処理をフロントエンドと同じプロセスが行うので分散しにくい

  • 処理の途中で失敗した場合のリトライがしにくい


仕組み

最初 Rack ミドルウェアかなと思ったけど、Rack ミドルウェアではレスポンス後に処理を行うことはできない。中身をみてみると、代表的な Rack サーバが各リクエストを処理するメソッドをフックして、コールバックを実行しているようです。

対応している Rack サーバは README 参照。

いま見たところ Mongrel, Passenger, Thin, Unicorn, WEBrick が挙がっています。


テスト

「仕組み」でも書いたとおり、Rack サーバの拡張なので、普通にテスト書いてもうまくいきません。というか、そもそも env["rack_after_replay.callbacks"]nil になって例外が起きます。

ひとまず、遅延処理を行う部分を下記のようにすれば、テスト時は rack_after_reply を使わないようになります。運用時と実行されるタイミングが変わってしまうので、あまりよい方法ではないですが...。

slow_proc = lambda {

# 時間のかかる処理
@photo.save!
}

if env["rack_after_reply.callbacks"]
env["rack_after_reply.callbacks"] << slow_proc
else # テスト時
slow_proc.call
end