並行実行制御における競合状態の修正

rails/solid_queue

Solid Queueの並行実行制御機構で、ジョブ完了時のブロック解除処理が新たに作成中のBlockedExecutionを見逃す競合状態が修正されました。この問題により、ブロックされたジョブが定期メンテナンスタスクの実行(最大で数分後)まで滞留する事象が発生していました。

背景

#456 で報告された問題は、並行実行数の上限に達していないにもかかわらず、ジョブが不必要にブロックされる現象でした。あるジョブが完了してから30分以上経過した後に、次のジョブがようやく実行される事例が観測されています。limits_concurrency to: 1 の設定で、先行ジョブが100ミリ秒で完了しているにもかかわらず、後続ジョブが30分後まで実行されない状況が発生しました。

この問題の根本原因は、ジョブのエンキュー処理とブロック解除処理の間で発生する競合状態にありました。Semaphore::Proxy#wait がセマフォの値を確認する際に行ロックを取得しないため、並行して実行される signal 処理との間にタイミングの問題が生じていました。

競合状態の発生メカニズム

問題は以下の手順で発生します:

  1. ジョブAが実行中(セマフォ値=0)
  2. ジョブBのエンキュー開始:セマフォを読み取り(値=0、行ロックなし)→ブロックを決定
  3. ジョブAが完了:Semaphore.signal → 値を1に UPDATE(ロックがないため即座に成功)
  4. ジョブA:BlockedExecution.release_oneSELECT で何も見つからない(ジョブBの BlockedExecution はまだコミットされていない)
  5. ジョブBのエンキューがコミット:BlockedExecution が作成されるが、もう誰もブロック解除しない

この競合ウィンドウは、エンキュー処理がセマフォの確認からトランザクションのコミットまでの間に、並行する signal が完了してしまうことで発生します。release_one が実行される時点では、BlockedExecution のレコードがまだトランザクション内にあるため、可視性がありません。

技術的な変更

app/models/solid_queue/semaphore.rbSemaphore::Proxy#wait メソッドが修正され、セマフォの確認時に FOR UPDATE による行ロックを取得するようになりました。

変更前:

def wait
  if semaphore = Semaphore.find_by(key: key)
    semaphore.value > 0 && attempt_decrement
  else
    attempt_creation
  end
end

変更後:

def wait
  if semaphore = Semaphore.lock.find_by(key: key)
    semaphore.value > 0 && attempt_decrement
  else
    attempt_creation
  end
end

.lock の追加により、エンキュー処理がセマフォの確認から BlockedExecution の作成とコミットまでの間、セマフォ行のロックを保持します。これにより、並行する signalUPDATE は待機を強制され、release_one が実行される時点で BlockedExecution の可視性が保証されます。

新たに追加された test/models/solid_queue/semaphore_test.rb には、この修正を検証するテストが含まれています。wait acquires a row lock that blocks concurrent signal テストでは、FOR UPDATE ロックが保持されている間、並行する UPDATE が実際にブロックされることを確認しています。別スレッドでセマフォ行の FOR UPDATE ロックを保持し、メインスレッドでの UPDATE が最低0.5秒間ブロックされることを検証します。

デッドロックの考察

PR本文では、この変更がデッドロックを引き起こさないことが説明されています。2つの処理パスにおけるロック取得順序を分析すると、循環依存が発生しないことが確認できます:

  • エンキューパス: Semaphore 行をロック → BlockedExecutionINSERT(既存行のロックなし)
  • release_one パス: BlockedExecution 行をロック(SKIP LOCKED)→ Semaphore 行をロック(release内のwaitを経由)

BlockedExecution のロック取得に SKIP LOCKED が使用されているため、複数のワーカーが同じブロック済みジョブを取得しようとして競合することはありません。また、エンキューパスは既存の BlockedExecution 行をロックしないため、release_one パスとの間で循環依存は発生しません。

まとめ

本PRは、セマフォの確認時に FOR UPDATE を追加することで、並行実行制御の競合状態を解決しました。この1行の変更により、エンキュー処理がトランザクション全体でセマフォ行のロックを保持し、signal 処理との原子性が保証されます。ロック取得順序の設計により、新たなデッドロックのリスクを導入することなく、ブロックされたジョブが確実に解除される仕組みが実現されています。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という「総論→各論→結論」の構成が明確に適用されており、非常に分かりやすいです。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:app/models/solid_queue/semaphore.rb)やGitHubのPR・Issueへのリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

競合状態、行ロック、デッドロックといったトピックを扱い、専門知識を持つエンジニアを対象とした適切な技術レベルで記述されています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。可読性が非常に高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

`app/models/solid_queue/semaphore.rb`のコード変更(`.lock`の追加)がDiffと正確に一致しています。また、追加されたテストコードの内容についても言及されており、変更の検証方法まで含めて正確に解説できています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「競合状態(race condition)」「行ロック(row lock)」「FOR UPDATE」「デッドロック」など、PRで使われている技術用語を正確に使用し、適切に解説しています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

`.lock`(FOR UPDATE)の追加がなぜ競合状態を解決するのか、そのメカニズムに関する説明は技術的に正確かつ論理的です。デッドロックに関する考察もPRの内容を正しく反映しています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのDescription、Diff、関連Issue(#456)の内容に基づいており、根拠のない憶測や創作(ハルシネーション)は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#712)、Issue番号(#456)などの数値や、`Solid Queue`, `BlockedExecution`などの固有名詞はすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「並行実行制御における競合状態の修正」は、PRのタイトル「Fix race condition between job enqueue and concurrency unblock」の内容を的確に反映しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

バージョンサポート状況やリリース日程といった、PR情報に基づかない外部知識の追記はなく、信頼性が保たれています。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

記事内の時間表現はPRで報告された事象を客観的に記述しており、時間的な歪曲は見られません。