トランザクション中にMySQLに再接続するということについて

問題

Ruby on Railsではコネクションプーリングが基本的に有効になっていて、コネクションが使い回されるという仕組みになっています。 Auroraにフェイルオーバーが発生し、マスターがレプリカに降格した場合は コネクションがレプリカに向き続けてしまってWriteを発行したらエラーが発生してしまうという問題があります。 この問題に対処するためにアプリケーション側でよしなにフェイルオーバーに気づき、再接続する仕組みを入れないといけないのです。

一般的に ActiveRecord::StatementInvalidMysql2::Error をキャッチして、--read-only なエラーだったら コネクションを再接続するというgemを使ったアプローチが取られることが多いようですが、これは本当に適切なのでしょうか? :thinking_face: トランザクション内で再接続された場合はどうなるんだろうと思い、調べてみました。

ActiveRecord::Base.transaction do
   foo = User.new(name: "foo")
   foo.save!
   ActiveRecord::Base.connection.reconnect! // 状況を再現しやすいようにわざと再接続してみる
   bar = User.new(name: "bar")
   bar.save!
end

puts User.count # 2か0を期待したいが、1が返ってくる

上記の場合ですと、コネクションが切断された段階でデータベースはロールバックされ、ロックが解除されたりします。 しかし、ActiveRecord::Base.transaction は例外を検知しているわけではないので、後続の処理が実行されます。 後続の処理はauto-commit modeで実施されるため、不整合が発生する可能性があります。 これはおそらくdatabase.yml に書く reconnect のオプションと実質同じなんじゃないかと思っています。

解決策

が正しいと思っています。

前者の仕組みとして、active_record_mysql_xverify というgemがあるようです。 active_record_mysql_xverify - so what GitHub - winebarrel/active_record_mysql_xverify このgemはコネクションをプールからcheckoutするときに 前回エラーが発生したというフラグを持っているかつinnodb_read_only が有効だったら、再接続するというアプローチを取っています。 むやみに毎回チェックするわけではないのが良いですね。 ですが、unicornみたいなprefork型のサーバーだと1つのプロセスに対して、1つのコネクションが使われ、そもそもプールに返却されることがないっぽい(?)ので 上記のgemは効果的な解決策にならないみたいです。

また、Delayed JobのバックエンドとしてActive Recordを使っている場合はどこかで再接続する仕組みを用意しなければならず面倒ですね。

Delayed Jobにはlifecycleというフックポイントがいくつか用意されており、これを活用するといいんじゃないかという結論に至りました。 lifecycle.before(:error) と書くことでジョブが再試行される前にコネクションが再接続されると思います。

class Delayed::Plugins::ClearConnectionsOnError < Delayed::Plugin
  callbacks do |lifecycle|
    lifecycle.before(:error) do |_worker, _job|
      is_read_only = ActiveRecord::Base.connection.execute('SHOW GLOBAL VARIABLES LIKE "innodb_read_only"').first.fetch(1) == "ON"
      if is_read_only
        ActiveRecord::Base.clear_all_connections!
      end
    end
  end
end

Delayed::Worker.plugins << Delayed::Plugins::ClearConnectionsOnError

終わりに

Railsノウハウ難しい・・・ 間違っていることを書いていたら、教えていただけると助かります。

合わせて読みたい

MySQL :: MySQL 5.6 リファレンスマニュアル :: 23.7.16 自動再接続動作の制御

Amazon Aurora MySQL を使用する際のベストプラクティス - Amazon Aurora