Workerの処理でActiveRecord::ConnectionNotEstablishedが発生した件

概要

SQSからキューを取って、DBに書き込むWorkerがActiveRecord::ConnectionNotEstablishedで落ちた件について、調査した。

SQSからのキューが一時的に止まる事象が発生して、長時間DBアクセスが起きない時に発生した。DBはMySQLを利用していた。

エラーの再現

事象からDB側の設定のwait_timeoutが問題だろうと検討をつけて、config/database.ymlwait_timeout: 5の設定を追加して、 以下のようなworkerを動かしてみた。

module Workers
  class TestWorker
    def random_update_user!
      while true
        begin
          # キューを取る時間が`wait_timeout`の設定である5秒をランダムで上回る設定
          sleep 3 + rand(5)

          puts "ActiveRecord::Base.connection_pool.stat: #{ActiveRecord::Base.connection_pool.stat}"
          user = User.all.sample
          user.update!(name: "taro#{Time.current.nsec}")
        rescue => e
          puts "rescued ActiveRecord::Base.connection_pool.stat: #{ActiveRecord::Base.connection_pool.stat}"
          pp e
        end
      end
    end
  end
end

たしかにActiveRecord::ConnectionNotEstablishedが発生することを確かめることができた。

rescued ActiveRecord::Base.connection_pool.stat: {:size=>5, :connections=>1, :busy=>1, :dead=>0, :idle=>0, :waiting=>0, :checkout_timeout=>5.0}
#<ActiveRecord::ConnectionNotEstablished: MySQL client is not connected>

ActiveRecord::ConnectionNotEstablishedが一度発生すると、ずっと発生し続けた。 別のコネクションに切り替えたりするのかな?と思ったけれど、そうではないみたい。

ActiveRecord::Base.connection_pool.statでコネクションプール全体の利用状況が把握できるが、1つのコネクションを利用し続けていることを確認できる。

解決策: reconnectの設定をする

config/database.ymlreconnect: trueを追加するだけ。

rescueされて1回だけActiveRecord::ConnectionNotEstablishedが発生するが、それ以降はエラーは発生せず、コネクションプール全体の利用状況も1つだけ使っている状況は変わらなかった。

ただし、この方法は常に利用できるわけではなく、

ActiveRecordのDBコネクションの接続切れと再接続について。reconnectオプションは危険だなーとかも

変数を使ったりした場合等の接続セッションに依存するような場合のコードを書くと、途中で接続がきれて変数等がリセットされてるのにきづかずに次の変数等に依存したsqlを実行してしまう可能性があるからです。 トランザクション周りも危険っぽいですね。ロールバックしてるのに気づかず自動コミットに戻って実行されちゃうとかかなー 怖い怖い。

なので、気をつけて使う。できれば使わない。

解決策: 処理の初めに明示的にリコネクトする

ActiveRecord::Base.connection.reconnect!を利用して、明示的にリコネクトすればActiveRecord::ConnectionNotEstablished`から回復できることを確認した。 今回のような、SQSからキューを取って、DBに書き込むWorkerの処理では、これで良さそう。

module Workers
  class TestWorker
    def random_update_user!
      while true
        begin
          sleep 3 + rand(5)

          puts "ActiveRecord::Base.connection_pool.stat: #{ActiveRecord::Base.connection_pool.stat}"
          user = User.all.sample
          user.update!(name: "taro#{Time.current.nsec}")
        rescue => e
          puts "rescued ActiveRecord::Base.connection_pool.stat: #{ActiveRecord::Base.connection_pool.stat}"
          ActiveRecord::Base.connection.reconnect!
          pp e
        end
      end
    end
  end
end