Workerの処理でActiveRecord::ConnectionNotEstablishedが発生した件
概要
SQSからキューを取って、DBに書き込むWorkerがActiveRecord::ConnectionNotEstablishedで落ちた件について、調査した。
SQSからのキューが一時的に止まる事象が発生して、長時間DBアクセスが起きない時に発生した。DBはMySQLを利用していた。
エラーの再現
事象からDB側の設定のwait_timeout
が問題だろうと検討をつけて、config/database.yml
にwait_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.yml
にreconnect: 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