2017-11-14

RequestStoreコードリーディング

steveklabnik/request_storeのコードリーディング。

Thread.currentはスレッド間では状態を共有しないものの、同一スレッドの異なるリクエスト間では状態を共有してしまいます。

例えばアプリサーバのスレッドを1にして、以下のようなThread.currentのグローバル変数の値を増加させる処理を書いたとすると、Thread.current[:cnt]はリクエストがある度に増加してしまいます。

class HogeController < ApplicationController
  def index
    Thread.current[:cnt] ||= 0
    Thread.current[:cnt] += 1
    puts Thread.current[:cnt]
  end
end

そういう意図の処理であれば良いですが、リクエストごとのグローバル変数を書きたい場合は意図に反した動きになります。

RequestStoreを使うとRackミドルウェアの仕組みを使ってThread.currentの値をリクエストごとにリセットするため、安全にThread.currentを利用することが出来ます。

コードを読むと、Railtieでミドルウェアを設定しており、gemを読み込んだだけでRackミドルウェアが差し込まれます。

module RequestStore
  class Railtie < ::Rails::Railtie
    initializer "request_store.insert_middleware" do |app|
      if ActionDispatch.const_defined? :RequestId
        app.config.middleware.insert_after ActionDispatch::RequestId, RequestStore::Middleware
      else
        app.config.middleware.insert_after Rack::MethodOverride, RequestStore::Middleware
      end
...
    end
  end
end

差し込まれるRackミドルウェアはRequestStore.begin!→アプリケーションの処理→end!, clear!という流れになっています。

module RequestStore
  class Middleware
    def initialize(app)
      @app = app
    end

    def call(env)
      RequestStore.begin!
      @app.call(env)
    ensure
      RequestStore.end!
      RequestStore.clear!
    end
  end
end

clear!ではThread.current[:request_store]を {} でリセットしており、begin!、end!はThread.current[:request_store_active]の状態を変えています。

module RequestStore
  def self.store
    Thread.current[:request_store] ||= {}
  end

  def self.clear!
    Thread.current[:request_store] = {}
  end

  def self.begin!
    Thread.current[:request_store_active] = true
  end

  def self.end!
    Thread.current[:request_store_active] = false
  end

  def self.active?
    Thread.current[:request_store_active] || false
  end

...

  def self.fetch(key, &block)
    store[key] = yield unless exist?(key)
    store[key]
  end

  def self.delete(key, &block)
    store.delete(key, &block)
  end
end
このエントリーをはてなブックマークに追加