[rails] ActiveRecord3の connection_pool の挙動を調べた

基本的にRailsでは単一のDBを使用するように設計されています。とはいえ負荷軽減のためだったり、あるいは様々なしがらみのために複数のDBに接続しなければいけない場合がたまにあったりすると思います。しかも残念な事に違うDBに同じ名前のテーブルがあったりしてどうすんだよコレとなることも無いとは言えないでしょう。

ActiveRecord が connection pool に対応したのは知識としては知っていましたが複数のDBに実際につなぎにいったばあい connection はどうなるの?と疑問に思ったので調べてみました。

# activerecord-3.0.3/lib/active_record/connection_adapters/abstract/connection_specification.rb 51行目 - 82行目
    def self.establish_connection(spec = nil)
      case spec
        when nil
          raise AdapterNotSpecified unless defined?(Rails.env)
          establish_connection(Rails.env)
        when ConnectionSpecification
          self.connection_handler.establish_connection(name, spec)
        when Symbol, String
          if configuration = configurations[spec.to_s]
            establish_connection(configuration)
          else
            raise AdapterNotSpecified, "#{spec} database is not configured"
          end
        else
          spec = spec.symbolize_keys
          unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end

          begin
            require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
          rescue LoadError => e
            raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})"
          end

          adapter_method = "#{spec[:adapter]}_connection"
          if !respond_to?(adapter_method)
            raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter"
          end

          remove_connection
          establish_connection(ConnectionSpecification.new(spec, adapter_method))
      end
    end

ActiveRecord::Base.establish_connection の実体は lib/active_record/connection_adapters/abstract/connection_specification.rb にありました。

establish_connection に文字列やシンボルを渡した場合それに相当する configuration から ConnectionSpecifiction のインスタンスを作成し、再び establish_connection を呼び出します。 そして establish_connection が ConnectionSpecification を受けとりますと今度は connection_handler の establish_connection を読んでいます。途中の過程はさておき connection_handler の establish_connection を呼びたいようです。

じゃあ connection_handler って何よ?と深追いしてみます。

# activerecord-3.0.3/lib/active_record/connection_adapters/abstract/connection_specification.rb 10行目 - 14行目
    ##
    # :singleton-method:
    # The connection handler
    class_attribute :connection_handler, :instance_writer => false
    self.connection_handler = ConnectionAdapters::ConnectionHandler.new

同じファイルの14行めにそれはありました。ご丁寧にシングルトンですよとコメント付きです。そしてその実体は ConnectionAdapters::ConnectionHandler のインスタンスでした。
ということで舞台は ConnectionAdapters::ConnectionHandler クラスへ移ります。

# activerecord-3.0.3/lib/active_record/connection_adapters/abstract/connection_pool.rb 278行目 - 287行目
    class ConnectionHandler
      attr_reader :connection_pools

      def initialize(pools = {})
        @connection_pools = pools
      end

      def establish_connection(name, spec)
        @connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec)
      end

ConnectionAdapters::ConnectionHandler は new すると @connection_pools という変数をハッシュで初期化します。シングルトンインスタンスインスタンス変数なのでこれもシングルトンです。そして establish_conection メソッドは ConnectionAdapters::ConnectionPool のインスタンスを作成し、name と紐付けて @connection_pools に格納しています。


はて? name って何だっけ? ということで ActiveRecord::Base.establish_connection に戻ってみますと name は仮引数や変数としては使われていないのでメソッド呼び出しであろうことがわかります。調べてみても name というメソッドは定義されていないので、 Module#name のことでしょう。リファレンスマニュアルによると「モジュールやクラスの名前を文字列で返します。」とありますので、establish_connectionをcallしたレシーバ名前の文字列です。


ということでシングルトンの @connection_pools に establish_connection のレシーバの数だけ connection_pool ができることになるということがわかりました。


自分で establish_connection をしない限りは rails の初期化時に ActiveRecord::Base.establish_connection が呼ばれるので通常はすべてのテーブルのアクセスが ActiveRecord::Base の connection_pool を使用することになります。

で、それが本当にそうなのか検証してみました。

require 'rubygems'
require 'active_record'

yml = <<EOS
common: &common
  adapter: sqlite3
  pool: 5
  timeout: 5000
base:
  <<: *common
  database: base.sqlite3
one:
  <<: *common
  database: one.sqlite3
EOS

ActiveRecord::Base.configurations = YAML.load(yml)


class CreateTable < ActiveRecord::Migration
  def self.up
    unless  connection.table_exists?(:users)
      create_table :users do |t|
        t.string :name
        t.integer :name

        t.timestamps
      end
    end
  end
end

[:base, :one].each{|env|
  ActiveRecord::Base.establish_connection(env)
  ActiveRecord::Migration.verbose = false
  CreateTable.up
}


class User < ActiveRecord::Base
end

class Ua < ActiveRecord::Base
  set_table_name 'users'
  establish_connection(:one)
end

class Ub < ActiveRecord::Base
  set_table_name 'users'
  establish_connection(:one)
end

class One < ActiveRecord::Base
  establish_connection(:one)
end

class U1 < One
  set_table_name 'users'
end

class U2 < One
  set_table_name 'users'
end

[User, Ua, Ub, U1, U2].each{|klass| p klass.name + ": "  + klass.connection_pool.object_id.to_s }
p ActiveRecord::Base.connection_handler.connection_pools.keys
p Hash[ActiveRecord::Base.connection_handler.connection_pools.map{|k, v| [k, v.object_id]}]

実行結果

$ ruby connection_pool.rb
"User: 75816690"
"Ua: 75730560"
"Ub: 75717530"
"U1: 75799790"
"U2: 75799790"
["ActiveRecord::Base", "One", "Ua", "Ub"]
{"ActiveRecord::Base"=>75816690, "One"=>75799790, "Ua"=>75730560, "Ub"=>75717530}

establish_connection を行わない場合は ActiveRecord::Base の connection_pool を使用します。
クラスで直接 establish_connection を行った場合はそのクラスが独自に connection_pool を持つことになります。 establish_connection を行ったクラスを継承した場合は establish_connection を行ったスーパークラスの connection_pool を使うことになります。

ということで各クラスで establish_connection をしてしまうと connection_pool の最大値(デフォルトでは5) x クラスの数 だけconnectionが保持できてしまうので非常によろしくない状態になります。
何かしらのやんごとない理由で別DBに接続しなければいけない場合は connection_pool 用のクラスを用意してそれを継承する形で使ったほうがよいです。

はぁ疲れた。