GlobalID::Locatorにfetchメソッドを追加し、欠損と障害を区別できるように
GlobalID::Locatorは、locate がブランクやパース失敗時に nil を返し、レコードが見つからない場合はバックエンド例外をそのまま伝搬させていました。この挙動では、削除済みレコードと一時的なバックエンド障害を呼び出し側が判別できませんでした。今回の PR は新メソッド GlobalID::Locator.fetch と2つの専用例外 RecordNotFound / RecordUnavailable を導入し、両者を明確に分離します。
背景
メール配信ジョブで一時的なデータベース接続エラーが起きた際、ActiveJob::DeserializationError が全ジョブを破棄してしまうケースが報告されていました。実際にはレコードが削除済みの場合だけ破棄したいが、バックエンド障害の場合はリトライが望ましいという要求がありました。既存の locate はバックエンド例外を直接スローするため、呼び出し側が例外クラスを知っていなければ判別できませんでした。
技術的な変更
GlobalID::Locator に共通基底例外 Error を追加し、RecordNotFound と RecordUnavailable がこのクラスを継承するようにしました。これにより、両例外を一括で捕捉できる統一インターフェースが提供されます。
fetch メソッドを実装し、内部で locate_many(ignore_missing: true) を呼び出してレコード取得を行います。取得結果が空の場合は RecordNotFound を、locate_many の呼び出し自体で例外が発生した場合は RecordUnavailable をそれぞれスローします。メソッドは nil を返すケース(ブランクやパース失敗)も従来通りに保持し、only: フィルタも引き継ぎます。
README へ使用例を追記し、fetch が成功、レコード削除、バックエンド障害の3つの結果をどのようにハンドリングすべきかを示しました。例示はインラインコードで示し、RecordNotFound と RecordUnavailable が GlobalID::Locator::Error を継承している点を強調しています。
設計判断
バックエンド固有の例外クラスに依存せずに状態判別を GlobalID 側で完結させる方針が採用されました。これにより、アプリケーションはバックエンド実装の変更に影響されず、fetch の戻り値だけでリトライ戦略や破棄ロジックを決定できます。locate_many が ignore_missing オプションをサポートしていることが前提となり、カスタムロケータは同オプションを実装する必要がありますが、既存ロケータへの影響は最小限です。
まとめ
GlobalID::Locator.fetch と専用例外の導入により、削除済みレコードと一時的障害を明確に区別でき、Active Job などのバックグラウンド処理で安全な破棄とリトライを実装できます。設計上は例外階層を統一し、バックエンド非依存の判別ロジックを提供した点が重要です。