今の ActiveRecord は connection_pool ありきの実装になっているが、「永続接続が許されるのは中規模サイトまでだよねー」と誰かが言ったとか言わないとかで、永続接続を止める。対象は activerecord 4.1。

TL; DR

https://github.com/sonots/activerecord-refresh_connection を利用できる。

事前知識

Rails が起動すると、ActiveRecord::Base#establish_connection が呼ばれるが、実は establish_connection では connection は貼られず、設定情報が渡されるだけである。(正確には、すでにあった接続を破棄する処理も行われるが)

lib/active_record/connection_handling.rb#L37-L47

    def establish_connection(spec = ENV["DATABASE_URL"])
      resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations
      spec = resolver.spec

      unless respond_to?(spec.adapter_method)
        raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
      end

      remove_connection # disconnect connections in a poll
      connection_handler.establish_connection self, spec
    end

lib/active_record/connection_adapters/abstract/connection_pool.rb#L537-L541

ConnectionPool のインスタンスがセットされているけれど、connection が作られてはいないことがわかる。

      def establish_connection(owner, spec)
        @class_to_pool.clear
        raise RuntimeError, "Anonymous class is not allowed." unless owner.name
        owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec)
      end

lib/active_record/connection_adapters/abstract/connection_pool.rb#L261-L267

では、いつどこで connection が貼られるのかというと、ActiveRecord::Base.connection が呼ばれたときである。 ここで connection pool にキャッシュされ、すでにキャッシュされていればソレを利用するようになっている。

      # Retrieve the connection associated with the current thread, or call
      # #checkout to obtain one if necessary.
      #
      # #connection can be called any number of times; the connection is
      # held in a hash keyed by the thread id.
      def connection
        # this is correctly done double-checked locking
        # (ThreadSafe::Cache's lookups have volatile semantics)
        @reserved_connections[current_connection_id] || synchronize do
          @reserved_connections[current_connection_id] ||= checkout
        end
      end

lib/active_record/connection_adapters/abstract/connection_pool.rb#L349-L355

ちなみに #checkout は、connection pool に connection があれば取り出し、なければ新しい connection を作るメソッド。 新しい connection を作る処理をたどっていくと最終的に mysql2_connection などの adapter の connection メソッドにいきつく。

      def checkout
        synchronize do
          conn = acquire_connection
          conn.lease
          checkout_and_verify(conn)
        end
      end

で、その connection メソッドがいつ呼ばれるかというと、クエリを投げるときに呼ばれる。

lib/active_record/querying.rb#L37-L48

    def find_by_sql(sql, binds = [])
      result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
      ....
    end

永続接続をやめる方法

  1. アプリコードで with_connection を利用する
  2. クエリ毎に connection を貼る
  3. リクエスト毎に connection を貼る 

(1) アプリコードで with_connection を利用する

#with_connection というメソッドがあるので、それを使って接続を open しては close するようにアプリケーションコードを書くことができる。

ActiveRecord::Base.connection_pool.with_connection do
  Post.create(body: 'foo')
end

(2) クエリ毎に connection を貼る

#connection を呼ぶたびに新しく接続を open しては close すれば良い。が、close するタイミングがないな。。。

用途も思いつかないので、今回は考えないものとする。

(3) リクエスト毎に connection を貼る

Rails のリクエスト処理の最後に、プロセスが持っている connection pool の接続をすべて閉じてしまえば良い。 rails の Controller の after_action などでやっても良いが、Rack middleware にしてしまうのが、一番よさそう。 Sinatra でも使えるし。

module ActiveRecord
  module ConnectionAdapters
    class RefreshConnectionManagement
      def initialize(app)
        @app = app
      end

      def call(env)
        testing = env.key?('rack.test')

        response = @app.call(env)
        response[2] = ::Rack::BodyProxy.new(response[2]) do
          ActiveRecord::Base.clear_all_connections! unless testing
        end

        response
      rescue Exception
        ActiveRecord::Base.clear_all_connections! unless testing
        raise
      end
    end
  end
end

元々 rails がやっている ActiveRecord::ConnectionAdapters::ConnectionManagement を挿げ替える。 それのActiveRecord::Base.clear_active_connections! を ActiveRecord::Base.clear_all_connections! に置き換えただけである。

# config/application.rb
class Application < Rails::Application
  config.autoload_paths += %W(#{config.root}/lib)
  config.middleware.swap ActiveRecord::ConnectionAdapters::ConnectionManagement,
    "ActiveRecord::ConnectionAdapters::RefreshConnectionManagement"
end

サンプルの rails アプリを作って、mysql に接続、show processlist; で connection が残っていないことを確認した。

おわりに

「(3) リクエスト毎に connection を貼る」の方針でやる予定。
=> gem 作った。https://github.com/sonots/activerecord-refresh_connection