Mysql2 の "MySQL client is not connected" について
Rails アプリケーションのドメインロジックを共有するためにバッチでも Rails を使っているケースはあるかと思います。
先日、長時間稼働しているバッチで MySQL サーバの再起動後に MySQL client is not connected
が起きたんですが、数年 Rails を使っていて初めて遭遇したエラーだったので、次の 2 点について調べてみました。
- このエラーにはどう対処すべきなのか?
- バッチ特有の問題なのか?
そもそも MySQL client is not connected とは?
定義を見る限り、client が初期化されているにも関わらず、network socket (file descriptor) が無効な状態だとこのエラーになるみたいですね。
network socket が無効な状態になるケースですが、主に次の 2 つのケースが考えられるようです。
mysql_send_query でエラーになった
例えば MySQL server が停止していたり、再起動が原因で今まで使っていた network socket が使えなくなったケースです。
cf. https://github.com/brianmario/mysql2/blob/0.4.9/ext/mysql2/client.c#L776
require "mysql2"
Mysql2::VERSION #=> "0.4.9"
client = Mysql2::Client.new
client.query("SELECT 1")
system("mysql.server restart")
client.query("SELECT 1")
#=> Mysql2::Error: MySQL server has gone away
client.query("SELECT 1")
#=> Mysql2::Error: MySQL client is not connected
SQL が read_timeout 以内に実行が終わらなかった
read_timeout
は SQL を投げてから実行が終わるまで(NOT 結果を受け取るまで)に最大で何秒許容するかを指定しますが、この時間が過ぎても実行が終わらなければタイムアウトし、network socket も無効化されるみたいです。
cf. https://github.com/brianmario/mysql2/blob/0.4.9/ext/mysql2/client.c#L784
client = Mysql2::Client.new(read_timeout: 0)
client.query("SELECT 1")
#=> Mysql2::Error: Timeout waiting for a response from the last query. (waited 0 seconds)
client.query("SELECT 1")
#=> Mysql2::Error: MySQL client is not connected
MySQL client is not connected の対処方法
Mysql2
クライアントを作り直すのが良さそうです。
client.close
client = Mysql2::Client.new
client.query("SELECT 1")
Active Record
Active Record では Mysql2Adapter#reconnect!
でクライアントを作り直してくれます。
require "active_record"
ActiveRecord::VERSION::STRING #=> "5.1.4"
ActiveRecord::Base.establish_connection(adapter: "mysql2")
ActiveRecord::Base.connection.execute("SELECT 1")
system("mysql.server restart")
ActiveRecord::Base.connection.execute("SELECT 1")
#=> ActiveRecord::StatementInvalid: Mysql2::Error: MySQL server has gone away: SELECT 1
ActiveRecord::Base.connection.execute("SELECT 1")
#=> ActiveRecord::StatementInvalid: Mysql2::Error: MySQL client is not connected: SELECT 1
ActiveRecord::Base.connection.reconnect!
ActiveRecord::Base.connection.execute("SELECT 1")
MySQL API の reconnect オプションではダメなのか?
MySQL の API には接続が切れた場合に自動で再接続してくれるオプションがあるんですが、Active Record を利用する場合はセッションの初期化等色んな処理が走っているので、どういう処理が行われているか正確に理解していて Rails のアップグレードにも追従できる自信がない限りは使わない方が無難と思われます。
cf. MySQL :: MySQL 5.7 Reference Manual :: 27.8.20 C API Automatic Reconnection Control
例えば、AbstractMysqlAdapter
ではセッションの最初に sql_mode
をセットしているんですが、recoonect を有効にすると再接続の際に sql_mode
がリセットされます。
ActiveRecord::Base.establish_connection(adapter: "mysql2", reconnect: true)
ActiveRecord::Base.connection.execute("SHOW VARIABLES LIKE 'sql_mode'").to_a.dig(0, 1).split(",")
#=> ["ONLY_FULL_GROUP_BY",
# "NO_AUTO_VALUE_ON_ZERO",
# "STRICT_TRANS_TABLES",
# "STRICT_ALL_TABLES",
# "NO_ZERO_IN_DATE",
# "NO_ZERO_DATE",
# "ERROR_FOR_DIVISION_BY_ZERO",
# "NO_AUTO_CREATE_USER",
# "NO_ENGINE_SUBSTITUTION"]
system("mysql.server restart")
ActiveRecord::Base.connection.execute("SHOW VARIABLES LIKE 'sql_mode'").to_a.dig(0, 1).split(",")
#=> ["ONLY_FULL_GROUP_BY",
# "STRICT_TRANS_TABLES",
# "NO_ZERO_IN_DATE",
# "NO_ZERO_DATE",
# "ERROR_FOR_DIVISION_BY_ZERO",
# "NO_AUTO_CREATE_USER",
# "NO_ENGINE_SUBSTITUTION"]
MySQL client is not connected はバッチ特有の問題なのか?
Rails を Rack アプリケーションとして利用し、普通の運用を行っていればバッチ特有のエラーと言えそうです。しかも、バッチの中でも大量のデータを処理し、一部のデータでエラーが発生しても警告を出すだけで処理を継続させるようなバッチ特有のエラーと言えます。
というのも、普通のバッチであれば MySQL server has gone away
や Lost connection to MySQL server during query
のようなエラーになった時点で異常終了するので、それでも処理を継続しない限り MySQL client is not connected
にはならないからです。
また、Rack アプリケーションとしての Rails であれば、HTTP リクエストごとに ConnectionPool#connection
内の ConnectionPool#checkout
で新規または既存のコネクションを取得し、その状態を確認し、接続が切れていれば再接続するようになっているので、MySQL server has gone away
や Lost connection to MySQL server during query
のようなエラーになった時点で 500 を返し、次の HTTP リクエストで再接続することになります。
ConnectionPool#connection
は同じ HTTP リクエストの間はキャッシュされるんですが、HTTP リクエストの最後でキャッシュを破棄しているので、HTTP リクエストごとに ConnectionPool#checkout
が実行されます。
コネクションのキャッシュを破棄する流れは非常に追いにくいのですが、次のような仕組みのようです。
ActionDispatch::Executor
が Rails アプリケーション (Rack アプリケーション) の middleware として登録される- これによってリクエストの前後に
ActiveSupport::Executor
のサブクラスのインスタンスに登録されている callbacks が呼ばれるようになる - リクエストの最初には run callbacks が呼ばれ、最後には complete callbacks が呼ばれる
- これによってリクエストの前後に
- active_record がロードされた時に
ActiveRecord::QueryCache
の callbacks をActionDispatch::Executor
に登録する