代表的な面接トピック

バックエンド面接:長時間実行オペレーション(Long-Running Operations)のAPIをどう設計するか?

バックエンド難しい
Offer.cc 編集チーム公開日 更新日

質問

マルチテナントのレポートエクスポートに5分から3時間かかります。クライアントはタイムアウト後にリトライする可能性があり、進捗の確認、処理のキャンセル、結果のダウンロードを行う必要があります。HTTPセマンティクス、オペレーションリソース、冪等性、状態遷移、通知、認可、障害復旧、検証を網羅した非同期APIを設計してください。

プロンプトとスコープ

B2Bアナリティクスプラットフォームにおいて、レポートエクスポートAPIが必要とされています。1回のエクスポートには5分から3時間かかり、最大20 GiBのファイルが生成される可能性があります。サービスは毎日09:00頃に10,000件の送信を受け取る場合があります。呼び出し元にはブラウザや他社が運用するサービスが含まれます。APIゲートウェイは30秒後に同期リクエストを切断するため、レスポンスを受信できなかった呼び出し元がリトライを行う可能性があります。

ユーザーは、処理がキュー待機中、実行中、成功、失敗のいずれであるかを確認できる必要があります。また、可能な限りキャンセルのリクエストを行い、成功した結果をダウンロードできる必要もあります。処理時間、ファイルサイズ、ピーク時の負荷、タイムアウト値は面接のケースにおける前提条件であり、業界の標準しきい値ではありません。キューのパーティショニングはこの質問の中心から外れます。ここでの中核タスクは、リトライ、プロセスクラッシュ、状態の競合が発生しても一貫性を保ち理解しやすい非同期HTTPコントラクトを定義することです。

RFC 9110では、202 Accepted はリクエストが処理のために受理されたものの、処理が未完了であり、場合によっては実行されない可能性があることを意味すると規定されています。レスポンスは現在のステータスを記述し、ステータスモニターを指し示す必要があります。現行のシニアREST API面接ガイドでも、ステータス、失敗、キャンセル、冪等性を含む長時間実行オペレーションの設計を候補者に求めています。この質問は、サービスコントラクトを設計するバックエンドエンジニア、APIプラットフォームエンジニア、シニアフルスタックエンジニアに適しており、その中核カテゴリーは backend です。

面接官が評価するポイント

第一に、候補者が 202 の境界を理解しているかという点です。これは「バックグラウンドジョブが成功する」ことを意味するわけではなく、未完了の処理は最終的な 200 OK の結果ではありません。優れた回答では、即座に検出可能な無効なリクエストを同期的に拒絶し、永続的に受理した後に安定したオペレーションリソースを返し、実行結果を後続の状態として公開します。

第二に、候補者がキューメッセージIDのみを返すのではなく、長時間実行処理をリソースとしてモデル化できるかという点です。オペレーションには、テナントオーナー、リクエストフィンガープリント、状態、進捗、結果またはエラー、バージョン、作成日時、有効期限が必要です。その状態マシンは、許可された遷移、終端状態、キャンセルの競合を定義しなければなりません。

第三に、サーバーが202を返したものの処理がキューに入らないというギャップが存在しないかという点です。オペレーションレコードとパブリッシュ対象の処理は1つのトランザクション内で永続化されるべきであり、その後にアウトボックスパブリッシャーがそれをキューイングできます。少なくとも1回の配信(at-least-once)、ワーカーのクラッシュ、リースの引き継ぎによって処理が重複する可能性があるため、コンシューマーと実行ステージには引き続き冪等性が必要です。

第四に、クライアントコントラクトが実際のトラフィックに耐えられるかという点です。Location はクライアントにステータスの確認先を伝えます。Retry-After、バックオフ、ジッター、条件付きリクエストによってポーリングを制御します。署名付きWebhookはサーバー間(Server-to-Server)の呼び出し元に通知でき、SSEはブラウザを更新できますが、いずれも復旧のためのクエリ可能なオペレーションリソースの代替にはなりません。

最後に、面接官はセキュリティと検証のストーリーを聞く必要があります。推測困難なIDはオブジェクトレベルの認可にはならず、結果URLがテナント境界をバイパスしてはなりません。優れた回答では、受理レスポンスの喪失、重複送信、パブリッシュの失敗、ワーカーのクラッシュ、キャンセルと完了の競合、ポーリングのバースト、有効期限切れのクリーンアップがテストされます。

最初に明確にすべき質問

  • 何を同期的にバリデーションすべきか? アイデンティティ、テナント権限、リクエストの形式、入力の存在確認、明らかなクォータ違反は、受理前にチェックする必要があります。データの完全性が数時間のスキャン後にしか判明しない場合、それは非同期実行の失敗であり、受理時に行われた約束ではありません。
  • 重複リクエストはリトライを意味するのか、それとも独立した2回目のオペレーションなのか? 呼び出し元が Idempotency-Key を提供する場合、同じテナント、エンドポイント、キー、リクエストフィンガープリントは1つのオペレーションを再実行(リプレイ)すべきです。意図的に2つの同一エクスポートを必要とする呼び出し元は、2つのキーを使用する必要があります。
  • 進捗は測定可能か? パーティションの総数が分かっている場合は、完了したユニットと総ユニットを報告します。見積もりが不可能な場合は、99%までドリフトするようなパーセンテージを捏造するのではなく、フェーズと最終ハートビートを報告します。
  • 結果はどのようなリソースか? 小さな結果はオペレーションレスポンス内に埋め込むことができます。大きなファイルは個別の保護されたリソースとするべきです。このケースでは、短期間有効なダウンロード認証情報を使用し、オペレーションメタデータ、結果オブジェクト、ダウンロード認証情報にそれぞれ個別の保持期間を設けます。
  • キャンセルは何を保証するのか? 将来の処理を停止するだけなのか、それともコミット済みの副作用を元に戻す必要があるのか? ステップが不可逆である場合、コントラクトはベストエフォート型のキャンセル、補償処理、部分的な出力、および取り得る最終状態を定義する必要があります。
  • 呼び出し元はどの通知チャネルを受信できるか? ブラウザは通常コールバックエンドポイントをホストできないため、ポーリングまたはSSEが適しています。サーバー間の呼び出し元はWebhookを使用できます。ネットワークの制約やレイテンシの目標によって通知チャネルは変わりますが、オペレーションリソースが信頼できる唯一の情報源(Single Source of Truth)であり続けます。
  • 並行するオペレーションは競合する可能性があるか? 1つのレポート設定を同時にエクスポートできるか? 実行中にその設定が更新または削除された場合はどうなるか? この回答によって、直列化するのか、入力をスナップショット化するのか、競合を拒絶するのか、それとも古いバージョンの完了を許可するのかが決まります。
  • 状態はどのくらいの期間保持されるか? このケースでは、終端状態のオペレーションを7日間、結果オブジェクトを24時間、各ダウンロード認証情報を15分間保持します。これらはプロダクトコントラクトの選択事項であり、監査要件、コスト、結果の再生成能力に応じて変更されるべきです。

30秒の回答フレームワーク

「私はHTTPリクエストから実行を分離しますが、単にジョブIDを返すだけにはしません。送信エンドポイントは認可と即座に検出可能なエラーを検証し、1つのトランザクションでオペレーションとアウトボックスレコードを書き込みます。コミット後、202Location、および推奨されるポーリング間隔を返します。認可されたオペレーションリソースは、安定した状態、実際の進捗、構造化されたエラー、結果リンクを公開します。同一の冪等性キーとリクエストは同一のオペレーションを再生します。ワーカーはオペレーションIDごとに冪等に処理し、バージョン条件によって状態遷移を保護します。ポーリングは Retry-After、バックオフ、ジッターを使用し、サーバー呼び出し元は署名付きWebhookを追加でき、キャンセルは cancel_requested に移行して完了との競合を解決します。その後、失われたレスポンス、重複メッセージ、ワーカクラッシュ、キャンセルの競合、有効期限切れを注入し、ゴーストジョブ、重複した可視結果、不正ダウンロードが発生しないことを証明します。」

ステップバイステップの詳細解説

第一に、2つのリソースを定義します。エクスポートリクエストはユーザーが作成したい結果を表現し、オペレーションリソースはこの実行のライフサイクルを表現します。送信は POST /v1/report-exports、ステータスは GET /v1/report-operations/{operation_id} とすることができます。受理レスポンスは次のようになります:

http
HTTP/1.1 202 Accepted
Location: /v1/report-operations/op_7f3a
Retry-After: 5
Content-Type: application/json

{
  "id": "op_7f3a",
  "status": "queued",
  "statusUrl": "/v1/report-operations/op_7f3a",
  "cancelUrl": "/v1/report-operations/op_7f3a"
}

202 は処理が受理されたことのみを約束します。不正な形式のリクエスト、認可されていない呼び出し元、存在しない入力は適切な4xxで拒絶し、オペレーションを作成しません。高コストな計算を必要とするビジネス上の失敗はオペレーションリソースに永続化します。LocationRetry-After はこのAPIのクライアントコントラクトの一部です。RFC 9110はすべての202レスポンスが両方のヘッダーを使用することを要求しているわけではありません。

第二に、オペレーションレコードと状態マシンを定義します。最小限のレコードには、idtenant_ididempotency_keyrequest_fingerprintstatus、進捗、結果の参照、構造化エラー、version、作成および更新のタイムスタンプ、expires_at が含まれます。推奨される遷移セットは次のとおりです:

text
queued -> running -> succeeded
                 -> failed
queued  -> cancel_requested -> canceled
running -> cancel_requested -> canceled | succeeded | failed

キャンセルと完了は競合する可能性があるため、cancel_requested は終端状態ではありません。ワーカーは version と許可された以前の状態に対する条件付き更新を用いて結果をコミットします。1つの遷移のみが勝ちます。副作用がすでに不可逆である場合、キャンセルは最終的に succeeded または failed になる可能性があります。単にボタンのラベルに合わせるためだけに canceled を捏造してはなりません。ステータス表現は次のようになります:

json
{
  "id": "op_7f3a",
  "status": "running",
  "progress": {
    "completedUnits": 37,
    "totalUnits": 100
  },
  "result": null,
  "error": null,
  "lastUpdatedAt": "2026-07-18T23:18:11Z",
  "expiresAt": "2026-07-25T23:08:11Z"
}

この進捗は、作業単位に実際の分母が存在する場合にのみ返します。失敗した実行であっても、オペレーション自体が正常に読み取られた場合は 200 を返し、失敗は終端状態と構造化された error によって表現されます。つまり、ステータスの読み取りは成功し、表現されている実行は失敗したということです。代わりに実行の失敗をステータスエンドポイントからの4xxにマッピングするチームは、両方の意味を混在させるのではなく、すべてのSDKでその規約を一貫して使用する必要があります。

第三に、受理、リトライ、実行を正しく動作させます。(tenant_id, route, idempotency_key) にユニーク制約を設定し、正規化されたリクエストのフィンガープリントを保存します。同じキーとフィンガープリントは既存のオペレーションと現在の状態を返します。同じキーで異なるフィンガープリントの場合は明示的な競合(Conflict)を返し、別のレポートに対する誤ったキーの再利用を防ぎます。冪等性レコードは、クライアントが正当にリトライできる期間以上保持し、オペレーションの保持期間と整合させます。

オペレーションとアウトボックスイベントを1つのデーターベーストランザクションで挿入し、202を返します。独立したパブリッシャーがアウトボックスイベントをキューに送信しますが、複数回送信される可能性があります。コンシューマーはオペレーションIDによって重複排除します。また、外部への書き込み後のワーカクラッシュとそれに続く処理の引き継ぎによって2つの可視結果が生成されないよう、各実行ステージにも冪等な書き込みまたはフェンシングトークンが必要です。APIの回答としては、受理からキューイングへの境界を証明できればよく、完全なスケジューラー設計を再現する必要はありません。

第四に、ステータスと通知のトラフィックを制御します。初回およびその後のステータスレスポンスは妥当な Retry-After を提供します。クライアントは上限とジッターを備えた指数バックオフを使用し、サーバーは ETag と条件付きリクエストをサポートして、変更のないボディの再送信を回避します。ステータスの読み取りがクォータを超えた場合は、10,000の呼び出し元が毎秒ポーリングするのを放置するのではなく、レート制限情報を返します。

低レイテンシの進捗を必要とするブラウザはSSEをサブスクライブでき、切断後もオペレーションIDによってクエリできます。サーバーの呼び出し元は署名付きWebhookを登録できます。送信側はリトライし、受信側は重複を排除します。両方のプッシュチャネルは失われたり、遅延したり、重複したりする可能性があるため、復旧とリコンシリエーション(整合性確認)のための信頼できる情報源はオペレーションリソースであり続けます。成功時、オペレーションボディは結果にリンクできます。APIが代わりに別の結果リソースにリダイレクトする場合は、303のセマンティクスをドキュメント化し、SDKが結果の場所に対して元のPOSTを再実行しないことを検証します。

第五に、認可、キャンセル、保持を処理します。すべてのステータス読み取り、キャンセル、結果取得において、tenant_id、呼び出し元のアイデンティティ、オペレーション権限に対するオブジェクトレベルの認可を実行します。ランダムなIDは列挙を困難にしますが、認可の代わりにはなりません。ダウンロードサービスは結果の所有権を再度検証し、このケース用に選択された15分間有効な認証情報を発行します。オペレーションレスポンスに長期間有効なパブリックURLを保存することは決してありません。

DELETE /v1/report-operations/{id} はキャンセルリクエストを表現できます。キャンセルが可能な場合は、現在の cancel_requested 表現を返して受理されたことを示します。不可能な場合やオペレーションがすでに終端状態にある場合は、リトライしても安全な安定したレスポンスを返します。ワーカーはステージの境界でキャンセルマーカーを確認し、将来のステップをスキップして一時オブジェクトを削除します。コミット済みの外部効果は、事前定義された補償ルールに従います。終端状態のオペレーションは7日後に削除します。既知の期限切れIDは 410 Gone を返し、未知または未認可のIDは情報開示ポリシーに従って 404 を返すことができます。

第六に、ハッピーパスだけでなく障害系を検証します。少なくとも次のケースをカバーします:サーバーがコミットしたものの202レスポンスが失われ、リトライによって同一のオペレーションのみが取得できること、アウトボックスパブリッシャーが送信前後にクラッシュしても最終的に1つの可視結果を伴う処理が存在すること、ワーカーが結果書き込み後に確認応答(ACK)を失っても後続ワーカーが終端状態を上書きできないこと、キャンセルと完了が同時に到着しても正当な終端状態が1つだけ現れること、変更のないステータスに対するポーリングがバックオフと条件付きリクエストに従うこと、テナントを越えたステータス確認、キャンセル、ダウンロードがすべて失敗すること、メタデータ、結果、冪等性キーがコントラクトに従って期限切れになること。

再利用可能な決定ルールは次のとおりです:202は接続待機を解決し、オペレーションリソースは可観測性を解決し、アトミックな受理と冪等な状態マシンは正確性を解決する。

質の高い模範解答

「私はまず、受理の成功と実行の成功を分離します。オペレーションには3時間かかる場合があり、30秒で切断されるゲートウェイ接続を占有することはできません。したがって、POST /v1/report-exports はアイデンティティ、テナント権限、リクエストの形式、入力の存在確認、明らかなクォータ違反をチェックします。その後、1つのトランザクションでオペレーションとアウトボックスレコードを作成し、コミット後にのみ202を返します。レスポンスには、オペレーションリソース用の Location と、初回のステータス読み取り用の Retry-After が含まれます。202はレポートが成功することを保証するものではありません。

オペレーションには、テナント所有権、冪等性キー、リクエストフィンガープリント、状態、検証可能な進捗、結果またはエラー、バージョン、有効期限が保存されます。状態はキュー待機中から実行中へ、そして成功または失敗へと遷移します。ワーカーが同時に結果をコミットしている可能性があるため、キャンセルはまずcancel_requestedに入ります。同一テナントおよびエンドポイントのもとでは、同一の冪等性キーとリクエストは1つのオペレーションを返します。同一キーで異なるリクエストの場合は競合となります。したがって、クライアントがリトライした際に、失われた202レスポンスによって2つ目のレポートが作成されることはありません。

キューの配信は少なくとも1回(at-least-once)行われるものと想定します。アウトボックスは2回パブリッシュする可能性があり、コンシューマーはオペレーションIDで重複排除を行い、各ステージにおける外部への書き込みは冪等であるかフェンシングされます。状態の更新にはバージョン条件が含まれるため、リースが引き継がれた後に古いワーカーが結果を上書きすることはできません。受理前の失敗は直ちに4xxを返します。実行中の失敗は終端状態および構造化エラーとして保存され、クライアントはネットワーク障害、ステータス読み取り障害、レポート実行障害を明確に区別できます。

クライアントはRetry-Afterに従い、指数バックオフとジッターを用いてポーリングを行い、ステータスエンドポイントはETagをサポートします。ブラウザはリアルタイムの進捗確認にSSEを使用でき、パートナーサービスは署名付きWebhookを使用できますが、いずれも切断や重複通知が発生した場合はオペレーションリソースを介して復旧します。結果ファイルにパブリックURLが付与されることはありません。ダウンロードエンドポイントで再度認可を行い、15分間有効な認証情報を発行します。このケースでは、終端状態のオペレーションは7日間、ファイルは24時間保持され、これらの有効期限ルールはパブリックコントラクトとなります。

最後に、コミット後に失われたレスポンス、重複したアウトボックス配信、結果書き込み後のワーカクラッシュ、キャンセルと完了の競合、10,000の呼び出し元による一斉ポーリング、テナント間アクセスをテストします。テストの合格はバックグラウンドで1回完了すること以上の意味を持ちます。これらの障害によってゴーストジョブ、重複した可視結果、不正な遷移、未認可のダウンロードが決して発生してはなりません。」

よくある間違い

  • 202を返した後にインメモリスレッドを開始する → プロセスの再起動によって二度と見つけられない処理が残り、受理と起動がアトミックでなくなる → 受理を確認する前にオペレーションとアウトボックスを永続化する。
  • 202を最終的な成功として扱う → HTTPは処理が実行されないことや失敗することを明示的に許容している → 後続のコントラクトで最終結果、エラー、ステータスモニターを公開する。
  • キューメッセージIDのみを返す → テナント所有権、安定した状態、エラー、結果、保持期間が欠落する → 独立した認可済みオペレーションリソースを作成する。
  • 無期限に毎秒ポーリングを続ける → 送信ピークが持続的な読み取りピークに変わる → Retry-Afterを提供し、バックオフ、ジッター、ETag、クォータを使用する。
  • ランダムなオペレーションIDを認可として使用する → ログの漏洩、ブラウザ履歴のエントリ、内部リンクによってテナント間アクセスが許可されてしまう → すべてのステータス読み取り、キャンセル、結果取得を個別に認可する。
  • 冪等性キーを短いRedisロック内でのみ保持する → ロックの期限切れ、クラッシュ、結果のリプレイによってオペレーションが重複して作成される可能性がある → 永続的なユニーク制約、リクエストフィンガープリント、再実行可能なレスポンスを使用する。
  • ユーザーがクリックした瞬間にキャンセル済みとマークする → ワーカーがすでに不可逆な効果をコミットしている可能性がある → まずcancel_requestedに移行し、条件付き遷移と補償処理によって正当な終端状態を決定させる。
  • 常にパーセンテージを捏造する → 予測不能なフェーズで99%のまま停止し、呼び出し元に誤解を与える → 測定可能な場合は完了ユニットを報告し、それ以外はフェーズと更新時刻を報告する。
  • Webhookが成功した後にオペレーションを削除する → 通知が失われたり、重複したり、一時的に故障している受信側に受理されたりする可能性がある → オペレーションを期限付きの復旧用情報源(Source of Truth)として保持する。

フォローアップ質問

フォローアップ1:データベーストランザクションはコミットされたが、202レスポンスが失われました。クライアントが再度送信するとどうなりますか?

クライアントは元の Idempotency-Key を再利用します。サーバーはテナント、エンドポイント、キーによってオペレーションを特定し、リクエストフィンガープリントが一致することを確認した上で、別のオペレーションやアウトボックスレコードを挿入することなく現在の表現と Location を再生します。キーが一致してもリクエストが異なる場合は、競合を返して新しいキーを要求します。テストでは、キューの配信が重複した場合でも、オペレーション行が1つであり、可視結果が1つだけであることを証明する必要があります。

フォローアップ2:オペレーションが90%完了しています。キャンセルと結果のコミットが同時に到着しました。どちらの状態が勝ちますか?

事前に正当な遷移を定義し、バージョン条件付き更新を使用して1つの勝者を選択します。結果トランザクションが先に running から succeeded へのコミットを行った場合、後続のキャンセルは終端状態である succeeded を読み取って返します。キャンセルが先に cancel_requested に到達した場合、ワーカーはコミット前に完了がまだ許可されているかどうかを確認します。不可逆なステージの場合、cancel_requested は正当に succeeded または failed で終了する可能性があります。コントラクトは絶対的なロールバックを約束することはできません。

フォローアップ3:進捗が見積もり不可能ですが、プロダクト側がパーセンテージの表示を求めています。何を返しますか?

捏造されたパーセンテージは誤った期待を生むことを説明します。完了したフェーズ、現在のフェーズ、最終ハートビート、および過去の実行から導出された拘束力のない目安の範囲を表示します。completedUnits / totalUnits は、総作業負荷が安定している場合にのみ返します。個々のフェーズが測定可能な場合は、コストの異なるフェーズを平均化するのではなく、各フェーズ内での進捗を表示します。

フォローアップ4:パートナーがポーリングを拒否しています。APIはWebhookのみを公開すべきですか?

Webhookは通常パスのレイテンシと読み取り回数を削減できますが、唯一の復旧メカニズムにすることはできません。コールバックはDNS、証明書、ファイアウォール、署名キーのローテーション、重複、順序の乱れといった障害に直面します。Webhook配信を署名してリトライし、オペレーションIDとバージョンを含め、受信側での重複排除を必須とします。パートナーはイベントを取りこぼした後にオペレーションリソースを介して整合性を確認できます。安定したコールバックエンドポイントを持たないブラウザは、引き続きポーリングまたはSSEを使用します。

フォローアップ5:1つのレポートで20 GiBのファイルが生成されます。オペレーションAPIはダウンロードURLを直接返すべきですか?

オペレーションは結果リソースの参照を返すべきです。このケース用に選択された15分間有効なダウンロード認証情報を発行する前に、テナントと呼び出し元を再度認可します。長期間有効なオブジェクトストレージのURLをオペレーション内に永続化してはなりません。ファイルは24時間保持され、オペレーションメタデータは7日間保持されるため、ファイルが期限切れになった後でも、オペレーションは実行が成功したこと、アーティファクトが期限切れになったこと、および再生成が可能であることを提示できます。

フォローアップ6:ステータスクエリのトラフィックが実行トラフィックよりも大きくなりました。最初に何を変更しますか?

まず、クライアントがRetry-After、指数バックオフ、上限値、ジッターに従っているかを確認します。次に、ETagベースの条件付きリクエスト、テナントクォータ、レート制限を有効にします。低レイテンシを必要とするブラウザはSSEを通じて更新を集約でき、サーバー呼び出し元はWebhookを使用できますが、低頻度のステータス読み取りは引き続き利用可能な状態にしておきます。推奨する間隔にもジッターを適用し、09:00の送信ピークがステータスエンドポイントにおける同期された周期的なピークにならないようにします。

公開情報ソース

関連する質問