テストスキーマの検証時に一時的な接続プールを単一化して高速化
ActiveRecord::Migration.maintain_test_schema! の実行時に、データベースごとに2回発生していた接続プールの作成が1回に削減されました。これにより、複数データベースを使用するアプリケーションのテスト起動時間が改善されます。
背景
従来の load_schema_if_pending! は、スキーマ更新の必要性を確認する際とマイグレーション保留状態を確認する際で、それぞれ別の一時的な接続プールを作成していました。各データベース設定に対して合計2回の接続プールを作成するこの実装は、特に複数データベースを使用する環境で無駄なオーバーヘッドとなっていました。#56695 がこの非効率性を解消しています。
テストスイート実行時の maintain_test_schema! は頻繁に呼び出されるメソッドであり、この重複した接続処理はテスト全体の起動時間に累積的な影響を与えていました。
技術的な変更
activerecord/lib/active_record/migration.rb の load_schema_if_pending! メソッドが再構成され、各データベース設定に対して単一の一時的接続プールで両方のチェックを実行するようになりました。
変更前:
def load_schema_if_pending!
if any_schema_needs_update?
load_schema!
end
check_pending_migrations
end
変更後:
def load_schema_if_pending!
any_schema_needs_update = false
pending_migrations = []
db_configs_in_current_env.each do |db_config|
ActiveRecord::PendingMigrationConnection.with_temporary_pool(db_config) do |pool|
any_schema_needs_update ||= schema_needs_update?(db_config, pool)
pending = pool.migration_context.open.pending_migrations
pending_migrations.concat(pending)
end
end
if any_schema_needs_update
load_schema!
pending_migrations = []
db_configs_in_current_env.each do |db_config|
ActiveRecord::PendingMigrationConnection.with_temporary_pool(db_config) do |pool|
pending = pool.migration_context.open.pending_migrations
pending_migrations.concat(pending)
end
end
end
check_pending_migrations(pending_migrations)
end
新しい実装では、各データベース設定に対して with_temporary_pool のブロック内で schema_needs_update? と pending_migrations の両方を取得しています。スキーマの再読み込みが発生した場合のみ、再度接続プールを作成してマイグレーション状態を確認します。
check_pending_migrations メソッドも変更され、外部から渡された pending_migrations を受け取れるようになりました。これにより、メソッド内で再度マイグレーションを取得する必要がなくなっています。
データベースタスクの最適化
activerecord/lib/active_record/tasks/database_tasks.rb の schema_up_to_date? メソッドが拡張され、既存の接続プールを再利用できるようになりました。
def schema_up_to_date?(configuration, _ = nil, file = nil, pool: nil)
db_config = resolve_configuration(configuration)
file ||= schema_dump_path(db_config)
return true unless file && File.exist?(file)
if pool
check_schema_sha1(pool, file)
else
with_temporary_pool(db_config) do |pool|
check_schema_sha1(pool, file)
end
end
end
新しい pool: キーワード引数により、呼び出し元が既に接続プールを保持している場合はそれを再利用できます。スキーマSHA1の検証ロジックは check_schema_sha1 プライベートメソッドに抽出され、コードの重複が解消されました。
reconstruct_from_schema メソッドでは、この変更を活用して既存の接続プールを schema_up_to_date? に渡すようになっています:
with_temporary_pool(db_config, clobber: true) do |pool|
if schema_up_to_date?(db_config, nil, file, pool: pool)
empty_all_tables(db_config)
else
purge(db_config)
end
end
設計判断
既存メソッドの引数拡張によるプール再利用 の方針が採用されました。
check_pending_migrations と schema_up_to_date? の両方に、外部から値やリソースを受け取るオプショナル引数が追加されています。check_pending_migrations(migrations = nil) は渡された配列を使用し、schema_up_to_date?(pool: nil) は渡された接続プールを再利用します。
このアプローチにより、既存の呼び出し側との後方互換性を保ちながら、最適化が可能な状況では接続プールの作成を回避できます。メソッドシグネチャの変更は最小限に抑えられており、外部APIへの影響はありません。
パフォーマンスへの影響
CHANGELOGには「Speedup ActiveRecord::Migration.maintain_test_schema! when using multiple databases」と記載されています。データベース接続の確立はコストの高い操作であり、複数データベース環境ではこの効果が倍増します。
3つのデータベースを使用するアプリケーションの場合、従来は6回の接続プール作成が発生していましたが、この変更により3回に削減されます。テストスイートの実行時には maintain_test_schema! が頻繁に呼び出されるため、この削減は実用的な影響を持ちます。
まとめ
本PRは、一時的な接続プールの作成回数を削減することで、テストスキーマ検証の効率を向上させました。既存のメソッドにオプショナル引数を追加する保守的な設計により、後方互換性を維持しながら複数データベース環境でのパフォーマンスを改善しています。接続プールの再利用という単純な最適化が、テスト起動時間の累積的な改善をもたらします。