MySQLのクエリプランナーミスを回避するインデックスヒントの追加
Solid Queueの並行実行制御において、ブロックされたジョブの解放クエリにインデックスヒントを追加することで、MySQLのクエリプランナーによる不適切な実行計画選択を防ぎ、不要な行ロックを回避できるようになりました。これにより、並行実行制限を設定したジョブが期待通りの並列度で処理される問題が解決されます。
背景
並行実行制限を設定したジョブで、期待される並列度よりも少ないワーカーしか動作しない問題が報告されていました。#694では、並行度5に設定したジョブが最初の5件処理後、1ワーカーのみが処理を継続する現象が確認されています。
この問題は、ブロックされた実行を取得するクエリが引き起こす競合状態に起因していました。以下のクエリは、concurrency_key列のインデックスにより、単一行ではなく同じconcurrency_keyを持つ行の範囲全体をロックします:
SELECT `solid_queue_blocked_executions`.*
FROM `solid_queue_blocked_executions`
WHERE `solid_queue_blocked_executions`.`concurrency_key` = 'DummyJob/DummyJob'
ORDER BY `solid_queue_blocked_executions`.`priority` ASC, `solid_queue_blocked_executions`.`job_id` ASC
LIMIT 1 FOR UPDATE SKIP LOCKED;
2つのワーカーがこのクエリを同時に実行すると、2番目のワーカーはレコードを取得できず、ブロックされたジョブがないと判断して処理を終了します。セマフォ値は正しく4を保持しているにもかかわらず、10分ごとのオーケストレーターによる解放まで、ブロックされたジョブが取得されない状態が続きます。
技術的な変更
BlockedExecution.release_oneメソッドに、複合インデックスindex_solid_queue_blocked_executions_for_releaseを使用するヒントが追加されました。
変更前:
def release_one(concurrency_key)
transaction do
if execution = ordered.where(concurrency_key: concurrency_key).limit(1).non_blocking_lock.first
execution.release
end
end
end
変更後:
def release_one(concurrency_key)
transaction do
if execution = ordered.where(concurrency_key: concurrency_key).limit(1)
.use_index(:index_solid_queue_blocked_executions_for_release)
.non_blocking_lock.first
execution.release
end
end
end
新たに追加されたuse_indexメソッドは、SolidQueue::Recordクラスメソッドとして実装され、Railsのoptimizer_hintsを利用してインデックスヒントを生成します:
def use_index(*indexes)
optimizer_hints "INDEX(#{quoted_table_name} #{indexes.join(', ')})"
end
このメソッドは、MySQL 8のオプティマイザーヒント形式/*+ INDEX(...) */でSQLコメントを生成します。MySQLのクエリプランナーはこのコメントをヒントとして解釈し、SQLiteとPostgreSQLは通常のコメントとして無視するため、データベース間の互換性が保たれます。
設計判断
インデックスヒントをSQLコメントとして実装する方式が採用されました。
MySQL固有のUSE INDEX構文ではなく、オプティマイザーヒントコメント/*+ ... */を使用することで、他のデータベースエンジンとの互換性を維持しています。SQLiteとPostgreSQLはこのコメントを無視するため、データベースごとに異なるクエリを生成する必要がありません。
optimizer_hintsは既にActive Recordに存在するAPIであり、新たな抽象化を導入することなく、Railsのクエリビルダーの標準的な拡張ポイントを活用した判断といえます。use_indexメソッドをSolidQueue::Recordに実装することで、Solid Queue全体で再利用可能なインターフェースを提供しています。
まとめ
本PRは、MySQLのクエリプランナーが不適切なインデックスを選択する問題に対し、明示的なインデックスヒントを追加することで解決しています。SQLコメント形式のヒントを使用することで、MySQL環境での問題を解決しながら、他のデータベースエンジンとの互換性を保持する設計です。並行実行制御における競合状態の根本原因に対処し、期待通りの並列度でジョブ処理が行われるようになります。