2017-01-26

Arproxyコードリーディング

cookpadさんのArproxyのソースコードリーディングをしました。

機能の概要

ActiveRecordのDBへのアクセス前後に処理を入れることができるようになるプロキシ的なライブラリです。

具体的な用途としては以下のように、ロガー、セキュリティ系で利用されるイメージです。

プロキシ自体は多段にすることができ、処理内容の責務を分けることが可能です。

使い方

READMEのコピペですが、プロキシクラスを定義してconfigureで設定してenable!で有効化します。
class QueryTracer < Arproxy::Base
  def execute(sql, name=nil)
    Rails.logger.debug sql
    Rails.logger.debug caller(1).join("\n")
    super(sql, name)
  end
end

Arproxy.configure do |config|
  config.adapter = "mysql2" # A DB Apdapter name which is used in your database.yml
  config.use QueryTracer
end
Arproxy.enable!

Arproxy::Baseはproxy_chain、next_proxyのプロパティとexecuteメソッドを持ちます。

module Arproxy
  class Base
    attr_accessor :proxy_chain, :next_proxy

    def execute(sql, name=nil)
      next_proxy.execute sql, name
    end
  end
end

Baseを継承したプロキシクラスはexecuteメソッドを実装しますが、その際に、super、つまりnext_proxy.executeを呼び出します。これによってプロキシクラスのexecuteの処理 => next_proxyのexecuteの処理、というように連鎖していき、最後にはActiveRecordのアダプタのexecuteメソッドにたどりつくという仕組みです。

Arproxy.configureではConfigクラスをイニシャライズしてからブロックに記述した通りに設定します。

module Arproxy
  class Config
    attr_accessor :adapter, :logger
    attr_reader :proxies

    def initialize
      @proxies = []
    end

    def use(proxy_class, *options)
      ::Arproxy.logger.debug("Arproxy: Mounting #{proxy_class.inspect} (#{options.inspect})")
      @proxies << [proxy_class, options]
    end

    def plugin(name, *options)
      plugin_class = Plugin.get(name)
      use(plugin_class, *options)
    end

    def adapter_class
      raise Arproxy::Error, "config.adapter must be set" unless @adapter
      case @adapter
      when String, Symbol
        camelized_adapter_name = @adapter.to_s.split("_").map(&:capitalize).join
        eval "::ActiveRecord::ConnectionAdapters::#{camelized_adapter_name}Adapter"
      when Class
        @adapter
      else
        raise Arproxy::Error, "unexpected config.adapter: #{@adapter}"
      end
    end
  end
end

Config内にはproxiesとadapterプロパティを持っており、proxiesはプロキシのクラスとオプションのリストとなっており、adapter_classは任意のDBに対するActiveRecordのアダプタのクラスを返すメソッドとなっており、@adapterにはアダプタクラスに対応する名前をセットすることになります。

enable!メソッド内ではProxyChainをイニシャライズして、@configを引数にしてenable!メソッドを呼び出します

@proxy_chain = ProxyChain.new @config
@proxy_chain.enable!

ProxyChainはイニシャライズ時にChainTailインスタンスを生成します。その名の通り、このインスタンスがexecuteチェーンの最後のインスタンスとなり、executeメソッドではActiveRecordのDBアダプタのexecuteメソッドを呼び出します。proxy_chain.connectionにはActiveRecordのDBアダプタのインスタンスがセットされます。

module Arproxy
  class ChainTail < Base
    def initialize(proxy_chain)
      self.proxy_chain = proxy_chain
    end

    def execute(sql, name=nil)
      self.proxy_chain.connection.execute_without_arproxy sql, name
    end
  end
end

以下でconfigに入れたproxiesをセットしていきます。

@head = @config.proxies.reverse.inject(@tail) do |next_proxy, proxy_config|
  cls, options = proxy_config
  proxy = cls.new(*options)
  proxy.proxy_chain = self
  proxy.next_proxy = next_proxy
  proxy
end

Proxy1、Proxy2という順番で設定した場合、reverseにより逆順で処理されます。injectの初期値(最初のnext_proxy)はChainTailで、proxy_configはproxiesの最後に設定したプロキシクラスになります。順にプロキシクラスをインスタンス化してnext_proxyによって後ろからつなげていき最後に一番最初に設定したプロキシクラスを@headとしてセットします。

enable!メソッドではアダプタクラスのclass_evalで以下の処理を実行します。

これによりアダプタクラスのexecuteが呼び出されるとexecute_with_arproxyが呼び出され、プロキシクラス内に定義されたexecuteを順に呼び出していき、最後にChainTailのself.proxy_chain.connection.execute_without_proxyによって本来のexecute(エイリアスでスイッチする前)を呼び出します。

わからなかったとこ

ProxyChainのconnectionのアクセサメソッドで保持先がThread.current[:arproxy_connection]にしてるのが、よくわからず。Threadごとに固有の値にすることがメリットなのか、スレッドセーフにすることがメリットなのか…。プラグインクラス利用時のメリデメとかも。

所感

ActiveRecordの前後処理ができるのでActiveRecord関連のプロファイリングするときにはめっちゃ便利そう。
このエントリーをはてなブックマークに追加