ActiveStorage の Blob メタデータ更新をバックグラウンドジョブへオフロード
ActiveStorage が GCS などのストレージにメタデータを更新する際に、サーバ側リクエストとバックグラウンドワーカーが同時に操作することで 409 Conflict が発生し、ユーザーには 500 エラーが返っていました。この PR はその競合を非同期ジョブに委譲し、リクエストの安定性を向上させます。
背景
ActiveStorage が GCS 等のストレージにメタデータを更新する際、サーバ側の HTTP リクエストとバックグラウンドワーカーが同時に update_service_metadata を呼び出すとデータ競合が発生します。\n\nこの競合は GCS が 409 Conflict を返す原因となり、Rails は 500 エラーとしてユーザーに露呈します。\n\nレースは確率的で再現が難しいため、従来のテストだけでは防ぎ切れません。\n\nこの状況を根本的に解決するには、メタデータ更新処理を排他的に実行できる仕組みが必要です。
技術的な変更
ActiveStorage::Blob のコールバックが after_update_commit :update_service_metadata から after_update_commit :sync_metadata_later に置き換えられ、content_type または metadata の変更が検知された場合に非同期ジョブをキューイングします。\n\n新たに導入された sync_metadata_later メソッドは ActiveStorage::SyncMetadataJob.perform_later(self) を呼び出すだけのシンプルなラッパーです。\n\nこの変更により、リクエスト処理中にメタデータ更新がブロックされることはなくなります。\n\n同時に実装された sync_metadata メソッドはサービス層の update_metadata を直接呼び出し、実際の更新処理を担当します。
ActiveStorage::SyncMetadataJob は ActiveStorage::BaseJob を継承し、キューは ActiveStorage.queues[:sync_metadata] に設定されています。perform メソッドは受け取った blob オブジェクトに対して blob.sync_metadata を実行し、メタデータ更新をバックグラウンドで完結させます。ジョブは discard_on ActiveRecord::RecordNotFound と retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :polynomially_longer を指定し、欠損やデッドロックに対して安全にリトライできるよう設計されています。
ActiveStorage::Service の update_metadata は instrument :update_metadata でラップされ、内部で update_metadata_for へ処理を委譲するよう改修されました。これにより、既存のインストルメンテーションロジックは保持しながら、サービスごとの実装を update_metadata_for に集中させることが可能になります。
ActiveStorage::Service::GCSService は update_metadata_for を具体化し、content_type、disposition、filename、custom_metadata を GCS のオブジェクトに反映するロジックを提供します。元の update_metadata 実装は削除され、委譲先メソッドに置き換えられました。
テストスイートには ActiveStorage::SyncMetadataJobTest と BlobTest が追加され、ジョブのキューイングと実行、及びメタデータ更新の通知が正しく行われることを検証しています。AttachmentTest でも ActiveStorage::AnalyzeJob 以外のジョブが余計にエンキューされないことが確認され、変更の副作用が抑制されていることが示されています。
設計判断
今回の設計判断は、バックグラウンドジョブへのオフロード を選択することで、レースコンディションの影響を非同期化に転嫁し、リクエスト側の失敗を回避する方針です。\n\nこの方式は既存の API 呼び出し側を変更せずに導入できるため、アプリケーション側のマイグレーションコストも最小限に抑えられます。\n\nupdate_metadata の公開インターフェイスは変更せず、内部委譲先として update_metadata_for を導入した点は、既存コードとの互換性を保ちつつサービス実装の拡張性を高める意図が明確です。\n\nその結果、独自ストレージ実装が update_metadata_for を上書きするだけで新しいメタデータ同期ロジックを利用可能になります。\n\nActiveStorage::SyncMetadataJob のリトライ設定と discard_on の組み合わせにより、ジョブ実行中の一時的なデッドロックやレコード欠損に対して安全に対処でき、運用上の堅牢性が向上しています。\n\nさらに、ジョブが失敗した場合でも自動リトライが行われるため、短期的な競合が残っていても最終的にメタデータは確実に反映されます。
まとめ
この PR は、ActiveStorage の Blob メタデータ更新を非同期ジョブへ委譲し、GCS で発生していた 409 コンフリクトを実質的に解消します。インストルメンテーションは維持しつつ、サービス実装の拡張ポイントを明示した設計は、将来的なストレージ種別追加にも柔軟に対応できる基盤を提供します。\n\n開発者は既存の Blob API をそのまま使用しつつ、バックグラウンドジョブの挙動だけを意識すれば良く、導入障壁が低くなっています。