Rails等で毎日APIを納品している界隈の皆様方は、リクエスト毎にグローバルな変数を持ちたい時はたまにあるかと思います。例えば、ログを吐くためにRack::Requestオブジェクトをロガーから参照したいとか。モデルやサービス層である処理が呼ばれる毎にリクエスト者のAuditログを残したい場合などそうなりそうです。
class PostEditService
def initialize(post, request)
@post, @request = post, request
end
def call(new_body)
@post.body = some_normalize(new_body)
#...
Rails.logger.info("[audit] ip: #{@request.ip}, ua: #{@request.user_agent}")
end
end
サンプルのコードはすごい適当ですが、本当に何も考えずに実装するとこんなかんじで引数で引き回す実装が思い浮かびます。しかし、これではService層に本質と関係ないログを残すためにいちいちrequestオブジェクトをControllerから渡すのはいまいちです。では、グローバルな所から参照できるようになると引き回さずに済んで良さそうです。
こちらも何も考えずに実装すると、こんな感じになっちゃうかもしれません。
class GlobalRequestStore
cattr_accessor :request
end
# ....
class ApplicationController
before_action :store_request
private
def store_request
GlobalRequestStore.request = request
end
end
# ...
class PostEditService
def call(new_body)
#...
request = GlobalRequestStore.request
Rails.logger.info("[audit] ip: #{request.ip}, ua: #{request.user_agent}")
end
end
これだと、controllerから保持するrequestオブジェクトは、RailsのModule#cattr_accessorによってGlobalRequestStoreのクラス変数(@@request)に格納されるだけでスレッドセーフではないです。
じゃあスレッドセーフなら良いのかということで、こういう実装を考えてみます。
class ThreadLocalRequestStore
def self.request=(req)
Thread.current[:request] = req
end
def self.request
Thread.current[:request]
end
end
# ....
class ApplicationController
before_action :store_request
private
def store_request
ThreadLocalRequestStore.request = request
end
end
# ...
class PostEditService
def call(new_body)
#...
request = ThreadLocalRequestStore.request
Rails.logger.info("[audit] ip: #{request.ip}, ua: #{request.user_agent}")
end
end
これはThread#[]=メソッドを使っていて、currentのスレッドのローカルなら領域にデータを格納できてます。しかしこれでもまだダメで、1リクエストごとにRubyプロセスが終了してくれなければ、同じスレッドを使って何らかのリクエスト以外の処理が実行された時に前回のリクエストでThreadLocalRequestStore
に格納した値が削除されずに残ってしまっているため、参照されてしまいます。
そこでRequestStore gemの登場です。このgemも上記と同様Thread.current#[]=を利用していて、さらにRack::Middlewareの層で、リクエスト毎にThread.currentの値をクリアしています。まぁ仕組みは単純です良いです。
module RequestStore
class Middleware
def initialize(app)
@app = app
end
def call(env)
@app.call(env)
ensure
RequestStore.clear!
end
end
end
class ApplicationController
before_action :store_request
private
def store_request
RequestStore.store[:request] = request
end
end
class PostsController < ApplicationController
def update
service = PostEditService.new(Post.find(params[:id])
service.call(params[:body])
# ...
end
end
class PostEditService
def initialize(post)
@post = post
end
def call(body)
@post.body = some_normalize(body)
#...
ActiveSupport::Notification.instrument('posts.audit')
end
end
class AuditLogger < ActiveSupport::LogSubscriber
def posts(event)
request = RequestStore.store[:request]
info("[request] - ip: #{request.ip}, ua: #{request.user_agent}")
end
end
AuditLogSubscriber.attach_to :audit
これでようやくリクエスト毎にグローバルな参照でオブジェクトがやりとり出来るようになり、ログを残すなどのためだけに各種クラスに本質的ではないrequestオブジェクトを引数として引き回すなどの設計を廃止し、ActiveSupport::Notificationによる実装に変更することも可能になりました。
参考: