既存のFailedExecution記録がある場合のクラッシュを修正

rails/solid_queue

ジョブの失敗記録時にTypeError: can't cast ProcessMissingErrorが発生し、起動できなくなる問題が修正されました。この修正により、理論上は発生しないはずの状態でもシステムが復旧可能になります。

背景

Job#failed_withメソッドはcreate_or_find_by!を使用してFailedExecutionレコードを作成していましたが、このメソッドにexceptionオブジェクトを直接渡していたことが問題の原因でした。ユニーク制約違反が発生した場合、フォールバックのfind_by!がexceptionオブジェクトをSQLバインドパラメータとして使用しようとし、型エラーが発生していました。

#699で報告されたこの問題は、ジョブがClaimedExecutionFailedExecutionの両方を持つという、本来ClaimedExecution#failed_withのトランザクション保証では起こりえない状態で発生していました。この状態になると、起動時のfail_orphaned_executions処理がクラッシュし、Solid Queueが全く起動できなくなります。

PR本文では、Mission Controlのジョブリトライ処理に競合状態やバグがある可能性が指摘されています。しかし、原因が何であれ、システムが復旧できる仕組みを持つことは重要です。

技術的な変更

Job#failed_withメソッドの実装が、create_or_find_by!から明示的な例外処理に変更されました。

変更前:

def failed_with(exception)
  FailedExecution.create_or_find_by!(job_id: id, exception: exception)
end

変更後:

def failed_with(exception)
  FailedExecution.transaction(requires_new: true) do
    FailedExecution.create!(job_id: id, exception: exception)
  end
rescue ActiveRecord::RecordNotUnique
  if (failed_execution = FailedExecution.find_by(job_id: id))
    failed_execution.exception = exception
    failed_execution.save!
  else
    retry
  end
end

新しい実装では、まずcreate!を試み、RecordNotUniqueエラーが発生した場合は既存レコードを検索して新しいエラー情報で更新します。create!失敗からfind_byの間にレコードが削除された場合(並行リトライとの競合)は、retryで再度create!を試みます。

さらに、FailedExecutionexpand_error_details_from_exceptionコールバックが変更されました。

変更前:

before_create :expand_error_details_from_exception

変更後:

before_save :expand_error_details_from_exception, if: :exception

before_createからbefore_saveに変更し、if: :exceptionガードを追加することで、既存のFailedExecutionを新しい例外で更新する際にもエラー詳細が正しくシリアライズされるようになります。

設計判断

楽観的な作成と例外ハンドリングのパターンが採用されました。create_or_find_by!のような便利メソッドではなく、明示的なcreate!rescueの組み合わせを選択しています。

このアプローチには2つの利点があります。第一に、create_or_find_by!は内部でfind_by!にフォールバックする際、渡された属性をすべてクエリ条件として使用しようとするため、RubyオブジェクトであるexceptionをSQLパラメータに変換できずエラーになっていました。明示的な例外処理では、このような予期しない型変換の問題を回避できます。

第二に、既存レコードの更新処理を明示的に記述することで、新しいエラー情報で上書きする意図が明確になります。create_or_find_by!では既存レコードをそのまま返すだけですが、新しい実装では最新のエラー情報を確実に記録します。

トランザクションのrequires_new: trueオプションは、外側のトランザクションと分離し、RecordNotUniqueを確実にキャッチできるようにしています。

まとめ

本PRは、理論上起こりえないデータ状態でもシステムが復旧できるよう、防御的なエラーハンドリングを追加した変更です。create!と明示的な例外処理の組み合わせにより、型変換エラーを回避しつつ、既存レコードの更新という意図を明確に表現しています。Mission Controlとの連携における潜在的な競合状態への対応としても機能します。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

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

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```言語:ファイルパス)およびGitHubのIssue/PRリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Active Recordのコールバックや例外処理など、専門知識を持つエンジニアを対象とした適切な技術レベルで記述されています。

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

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

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

Diff内容との照合 ✓ PASS

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

記事内で引用されている変更前後のコードは、提供されたDiff情報と完全に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`create_or_find_by!`, `RecordNotUnique`, `SQL bind parameter`などの技術用語が文脈に応じて正確に使用されています。

説明の技術的正確性 ✓ PASS

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

「`create_or_find_by!`が例外オブジェクトをSQLパラメータとして扱おうとして`TypeError`を引き起こす」という問題の根本原因や、コールバックを`before_save`に変更した意図など、技術的な説明が正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張(クラッシュの状況、Mission Controlへの言及、復旧性の重要性など)は、PRのDescriptionで裏付けられており、ハルシネーションは検出されませんでした。

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

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

PR番号(#713)および関連Issue番号(#699)が正確に記載されています。

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

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

記事のタイトル「既存のFailedExecution記録がある場合のクラッシュを修正」は、PRのタイトル「Fix crash when recording a failed execution for a job that already has one」の内容を正確に反映しています。

外部知識の正確性 ✓ PASS

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

PR情報に記載のない外部知識(バージョンのサポート状況、リリース日程など)の追加はなく、すべての情報が提供された資料に基づいています。

時間表現の正確性 ✓ PASS

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

問題が発生していた過去の状況と、今回の修正内容についての時間表現は、PR情報と一致しており正確です。