`preconnect`に`reaper_lock`を追加してリーパーとの競合状態を修正
pool.preconnectがリーパースレッドと並行実行された際に発生する断続的なテスト失敗を、reaper_lockの適用範囲を拡張することで修正しました。
背景
CIでtest_preconnectが断続的に失敗するという問題が報告されていました。リーパースレッドはメンテナンスサイクルの一部としてpreconnectを呼び出す際に reaper_lock を保持しています。一方、テストがpool.preconnectを並行して呼び出した場合、リーパーがcheckout_for_maintenanceを通じてコネクションをチェックアウト済みの状態(allow_preconnect=false、@raw_connection=nilという過渡的な状態)にしている可能性があり、テスト側のpreconnectがそのコネクションを処理できませんでした。
この問題はDocker上のMariaDB 11.4を低リソース環境(--cpus=0.02 --memory=128m)で再現検証されており、修正前は7、16、36、65イテレーション目で失敗が確認されています。修正後は100回以上のイテレーションがすべて成功し、reaper_lockの待機が86回以上トリガーされたことも確認されています。
技術的な変更
preconnectメソッドの処理全体をreaper_lockブロックで囲む、最小限の変更が加えられました。
変更前:
def preconnect
sequential_maintenance -> c { (!c.connected? || !c.verified?) && c.allow_preconnect } do |conn|
conn.connect!
rescue
# Wholesale rescue: there's nothing we can do but move on. The
# connection will go back to the pool, and the next consumer will
# presumably try to connect again -- which will either work, or
# fail and they'll be able to report the exception.
end
end
変更後:
def preconnect
reaper_lock do
sequential_maintenance -> c { (!c.connected? || !c.verified?) && c.allow_preconnect } do |conn|
conn.connect!
rescue
# Wholesale rescue: there's nothing we can do but move on. The
# connection will go back to the pool, and the next consumer will
# presumably try to connect again -- which will either work, or
# fail and they'll be able to report the exception.
end
end
end
この変更により、preconnectの実行中はリーパースレッドがcheckout_for_maintenanceでコネクションを過渡状態に置くことができなくなります。
設計判断
reaper_lockを再入可能なMonitorとして実装する既存パターンを踏襲することで、デッドロックを発生させずに排他制御を実現しています。
reaper_lockはリエントラントなMonitorを使用しているため、リーパー自身がpreconnectを呼び出す際(すでにreaper_lockを保持している状態)はブロックされることなくそのまま通過します。この設計は、e45d8acc746でdisconnect、discard!、with_exclusively_acquired_all_connectionsに適用された「グローバル操作中はプールをロックする」パターンの一貫した拡張です。preconnectのみがそのパターンから漏れていたことが今回の競合状態の根本原因でした。
変更差分は9行の追加と7行の削除という小さなものですが、sequential_maintenanceブロック全体がreaper_lockのスコープに収まるよう正確にネストされており、既存のエラーハンドリングロジックには手が加えられていません。
まとめ
本PRはpreconnectを既存のロック戦略に組み込むことで、グローバル操作に対する一貫した排他制御を実現しました。リエントラントなMonitorの特性を活かすことで、リーパー自身の動作を妨げることなく競合状態を排除しており、コネクションプールのスレッドセーフティが一段と強固になっています。