Mysql2Adapter#discard! が子プロセスで親接続を破損するバグを修正
Mysql2Adapter#discard! がフォーク後の子プロセスで残存するステートメントのファイナライズにより、親プロセスの MySQL 接続が切断される問題を、ソケットを IO::NULL にリダイレクトする形で解決しました。
背景
この変更は、activerecord mysql2 (master) [prepared_statements] の Nightly テストが、フォークされた子プロセスが親接続を破壊するケースで失敗したことが発端です。テストは ActiveRecord::ConnectionFailed を示し、MySQL サーバ側で "Lost connection to MySQL server during query" が記録されました。
MYSQL_PREPARED_STATEMENTS=true が有効な状態では、親プロセスの @statements キャッシュに Mysql2::Statement オブジェクトが保持され、fork 後に子プロセスが同一のソケットファイルディスクリプタと Ruby のステートメント参照を継承します。その結果、子プロセスの終了時にステートメントのファイナライザが COM_STMT_CLOSE を送信し、親プロセスの接続がサーバ側で閉じられてしまいます。
技術的な変更
Mysql2Adapter#discard! に一行の追加が行われ、子プロセスでのソケットリダイレクトを実装しました。具体的には IO.for_fd(@raw_connection.socket, autoclose: false).reopen(IO::NULL) を super の呼び出し直後に挿入し、@raw_connection が存在すれば安全に /dev/null へ再オープンします。
変更前:
def discard! # :nodoc:
@lock.synchronize do
super
@raw_connection&.automatic_close = false
@raw_connection = nil
end
end
変更後:
def discard! # :nodoc:
@lock.synchronize do
super
IO.for_fd(@raw_connection.socket, autoclose: false).reopen(IO::NULL) if @raw_connection rescue nil
@raw_connection&.automatic_close = false
@raw_connection = nil
end
end
この一行は、子プロセスが継承したソケットに対して書き込み系のファイナライザが実行されても、実体が IO::NULL であるため親接続へ影響を与えません。rescue nil によりソケットが取得できないケースでも例外が抑制され、既存の動作に干渉しない安全策となっています。
設計判断
本修正は PostgreSQLAdapter#discard! が採用している "socket を IO::NULL に再オープンする" 手法を Mysql2Adapter にも適用したものです。PR のコメントからは、設定キーを追加せず、既存メソッドに最小限のロジック追加で対応する方針が選択されたことが読み取れます。
この設計は 後方互換性 と 侵入性の低さ を重視しています。discard! のシグネチャや呼び出し側コードは一切変更せず、内部実装だけを差し替えることで、既存アプリケーションへの影響を回避しています。また、標準ライブラリのみで完結するため、追加の依存やプラットフォーム固有のコードを導入しない点も評価できます。
まとめ
Mysql2Adapter#discard! にソケットを IO::NULL へリダイレクトする処理を加えることで、フォークされた子プロセスが親の MySQL 接続を破壊するバグを解消しました。テストスイートは prepared_statements 環境下でも全て成功し、fork safety が確保されたことが確認されています。