プロンプトとスコープ
この質問では、バックエンドエンジニアがHTTP境界において並行性制御を適切に実装できるかをテストします。キャッシュ検証と書き込み保護の違い、サーバーによるエンティティタグの比較方法、返却すべきステータスコード、およびデータベースの書き込みとレスポンスヘッダーの整合性を維持する方法について説明してください。
面接官が評価しているポイント
- ETag、If-Match、If-None-Matchの異なる意味を理解しているか。
- 強力な検証子(strong validator)によって古いバージョンからの書き込みが保護されているか。
- 412、428、409が前提条件エラーとビジネスコンフリクトの境界を明確に区別しているか。
- プロキシキャッシュ、リトライ、認証・認可、レプリカの可視性を考慮しているか。
最初に確認すべき明確化の質問
更新がリソース全体を置換するのか一部のフィールドのみか、クライアントによるバージョンの送信が必須か、オフライン編集やマージがサポートされているか、ETagが完全なリソース表現(representation)またはビジネスバージョンを表しているか、読み取りと書き込みがキャッシュ、ロードバランサー、複数のデータベースレプリカを経由するかどうかを確認します。
30秒で答える要約
すべてのリソース表現とともにETagを返します。更新処理(mutation)ではIf-Matchを必須とし、書き込み境界内で現在のバージョンと比較して、不一致の場合は412、必要な前提条件が欠落している場合は428を返します。成功時にはバージョンをインクリメントし、新しいETagを返します。If-None-Matchを伴うGETは304を返す場合がありますが、キャッシュ検証は書き込みを保護するものではありません。
詳細な回答
1. キャッシュ検証と書き込み保護の分離
If-None-MatchはGETのキャッシュ検証に有用であり、変更のない表現に対して304を返すことができます。If-Matchは現在の表現が一致することを要求し、PUT、PATCH、DELETEで一般的に使用されます。キャッシュヒットは現在のリソースを上書きする権限を与えるものではありません。比較の方向性と失敗時の挙動が異なります。
2. 安全に比較できるETagの選択
書き込み保護には、バイト列または定義されたバージョンが完全に一致するように強ETag(strong ETag)を使用します。弱ETag(weak ETag)は意味的に等価なキャッシュ表現に適しており、厳密な更新の認可には使用すべきではありません。タグは内部のシーケンス番号や機密データを公開すべきではないため、認可フィルタリングを適用した後、正規化された表現とバージョンに予測不能なソルトを組み合わせて導出します。
3. 比較と更新を同一境界内に保持
現在のバージョンを読み取り、If-Matchを比較し、UPDATE ... WHERE id = ? AND version = ?のような1つの条件付きデータベース書き込みで更新を実行します。影響を受けた行数が0の場合は412を意味し、更新が成功するとバージョンがインクリメントされ新しいETagが生成されます。アプリケーションコード内でクエリを実行した後に無条件の書き込みを発行してはいけません。
4. 失敗レスポンスとリトライパスの設計
必要な前提条件がない場合は428、ETagが一致しない場合は412、個別のビジネス状態の競合には409を返します。現在の表現や再取得のヒントを含めますが、クライアントのためにサイレントに上書きしてはなりません。412の受信後、クライアントは再度GETを実行して差分を表示するか、明示的なフィールドマージを適用して新しいETagでリトライすべきです。自動リトライには上限回数と明確な冪等性が必要です。
5. キャッシュ、レプリカ、運用の処理
ETagを生成する読み取りは最新の書き込みを参照している必要があり、そうでなければシステムは制限されたread-after-write遅延を定義する必要があります。プロキシは、機密リソースに対する適切なキャッシュ制御とともに、ETag、If-Match、If-None-Matchを正しく転送しなければなりません。リクエストID、新旧バージョン、結果、コンフリクト率をログに記録し、古い表現を使用しているクライアントや遅延しているレプリカを特定します。
優れた回答例
すべてのGETに対して、正規化された表現とバージョンから導出された強ETagを返します。PUT、PATCH、DELETEではIf-Matchを必須とします。データベースはバージョン条件付き更新を実行し、一致する行がない場合は412を返します。前提条件が不足している場合はインターフェースポリシーに従って428を返し、成功時はバージョンをインクリメントして新しいETagを返します。一致するIf-None-Matchを伴うGETは、キャッシュ節約のみを目的として304を返します。412の後、クライアントは再読み取りを行い、差分を表示するか明示的なフィールドマージを実行して、新しいタグでリトライします。レプリカ間の読み取りは現在のバージョンを参照する必要があり、プロキシは条件付きヘッダーを転送しなければなりません。コンフリクト、古いバージョンのリクエスト、レプリカの遅延を監視します。
よくある間違い
- クライアントのタイムスタンプのみを比較し、クロックスキューや表現の違いを無視すること。
- 正確な書き込みの認可に弱ETagを使用すること。
- 最初にバージョンをクエリし、条件なしで書き込みを行って競合状態(race condition)を残すこと。
- 412、409、428をクライアントに次のステップを提供しない単一の汎用エラーとして扱うこと。
- サーバーがコンフリクトをサイレントに上書きしてクライアントの編集内容を消失させること。
- キャッシュやレプリカの可視性を考慮せずにアプリケーションノードでのみタグを生成すること。
フォローアップ質問
ETagをデータベースの自動インクリメントバージョンにすることはできますか?
内部的な入力値とすることは可能ですが、HTTP向けにエンコードし、機密性の高いビジネス情報の露出を避けてください。フィールドフィルタリングや認可によってリソース表現が変わる場合は、その表現境界でタグを生成します。
重複しない2つのPATCHフィールドを自動的にマージすることはできますか?
明示的なフィールドレベルのマージプロトコルを提供することは可能ですが、それでもクライアントがどのETagを使用したかを検証する必要があります。マージおよび非マージのルールを公開し、その決定を監査可能にします。フィールドが異なっていても、すべてのビジネスコンフリクトを無視して安全というわけではありません。
なぜ409だけを返さないのですか?
412はIf-Match形式の前提条件が失敗したことを意味し、428はクライアントに前提条件の提供を求め、409は現在のリソース状態またはビジネスオペレーションとの個別の競合を示します。この区別により、クライアントは再取得すべきか、ヘッダーを追加すべきか、操作を変更すべきかを判断できます。
CDN経由での古い書き込み(stale writes)を防ぐにはどうすればよいですか?
更新リクエスト(mutation)は信頼できるオリジン(authoritative origin)にルーティングし、読み取りにはキャッシュを使用します。仕様に従って条件付きヘッダーを転送し、更新成功後に影響を受けるエントリを無効化するか鮮度の上限を設定し、オリジンバージョンとエッジタグの差異を監視します。