MySQL エラー 1046 を `ConnectionFailed` として分類し、自動リトライを可能に
TCPプロキシによる接続リセット後に発生するMySQL エラー 1046(ER_NO_DB_ERROR: No database selected)が、ConnectionFailed として分類されるようになりました。これにより、with_raw_connection の組み込みリトライループが正常に機能し、接続の自動復旧が期待できます。
背景
mysql2がTCP接続をサイレントに再確立した場合、新しい接続では USE database_name が再発行されません。プロキシによる接続リセットが発生したとき、次のクエリは MySQL エラー 1046 No database selected で失敗しますが、このエラーはこれまで適切に処理されていませんでした。
従来の translate_exception にはエラー 1046 に対応するケースがなく、StatementInvalid にフォールスルーしていました。StatementInvalid は retryable_connection_error? に認識されないため、with_raw_connection のリトライループが発火せず、接続の自動復旧が行われないという問題がありました。
この問題は、エラー 2006(CR_SERVER_GONE_ERROR)およびエラー 2013(CR_SERVER_LOST)と同じクラスの問題です。これら2つはすでに ConnectionFailed として分類されており、エラー 1046 も同様に扱われるべきでした。
技術的な変更
abstract_mysql_adapter.rb の2箇所に最小限の変更が加えられました。まずエラー定数の定義に ER_NO_DB_ERROR が追加され、次に translate_exception の ConnectionFailed ブランチにそのエラーが追加されています。
変更前:
# See https://dev.mysql.com/doc/mysql-errors/en/server-error-reference.html
ER_DB_CREATE_EXISTS = 1007
ER_FILSORT_ABORT = 1028
ER_DUP_ENTRY = 1062
# ...
when ER_CONNECTION_KILLED, ER_SERVER_SHUTDOWN, CR_SERVER_GONE_ERROR, CR_SERVER_LOST, ER_CLIENT_INTERACTION_TIMEOUT
ConnectionFailed.new(message, sql: sql, binds: binds, connection_pool: @pool)
変更後:
# See https://dev.mysql.com/doc/mysql-errors/en/server-error-reference.html
ER_DB_CREATE_EXISTS = 1007
ER_FILSORT_ABORT = 1028
ER_NO_DB_ERROR = 1046
ER_DUP_ENTRY = 1062
# ...
when ER_CONNECTION_KILLED, ER_SERVER_SHUTDOWN, CR_SERVER_GONE_ERROR, CR_SERVER_LOST, ER_CLIENT_INTERACTION_TIMEOUT, ER_NO_DB_ERROR
ConnectionFailed.new(message, sql: sql, binds: binds, connection_pool: @pool)
テストでは、Mysql2::Error のスタブを使って ER_NO_DB_ERROR が ActiveRecord::ConnectionFailed に変換されることを検証しています。connection_pool 属性が正しく設定されている点も確認されており、エラーハンドリングのチェーンが機能していることを示します。
def test_no_database_selected_error_translates_to_connection_failed
raw_conn = @conn.raw_connection
error = assert_raises(ActiveRecord::ConnectionFailed) do
raw_conn.stub(:query, ->(_sql) { raise Mysql2::Error.new("No database selected", 50700, ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::ER_NO_DB_ERROR) }) {
@conn.execute("SELECT 1")
}
end
assert_equal @conn.pool, error.connection_pool
end
設計判断
既存の ConnectionFailed 分類の拡張 というアプローチが採用されました。エラー 1046 は一見「データベースが選択されていない」というアプリケーション側の設定ミスのように見えますが、コネクションプールを介した接続リセット後に発生するケースでは、これは本質的に接続状態の喪失を意味します。このため、ステートメントエラーではなく接続エラーとして扱うことが正確です。
ER_NO_DB_ERROR を新たな定数として定義し、when 節に追加する構成は、既存の他のエラー定数(CR_SERVER_GONE_ERROR、CR_SERVER_LOST など)と完全に対称的です。新たな抽象化や分岐ロジックを導入せず、確立されたパターンに従うことで変更範囲を最小限に抑えています。
まとめ
この変更は、コネクションプールと中間プロキシを使うMySQLアプリケーションにおいて、サイレントな接続リセット後の障害回復を自動化します。エラー分類のパターンを正しく適用することで、既存のリトライ機構がそのまま機能するようになり、新たなリカバリロジックを追加する必要がありません。